From 898042a7a49069b2467ab9cd93de790dde3e6799 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 3 Oct 2025 16:23:40 -0400 Subject: [PATCH 1/8] init --- .../clerk-js/src/ui/elements/FormControl.tsx | 32 +++++++++++-------- packages/clerk-js/src/ui/primitives/Input.tsx | 10 +++--- .../src/ui/primitives/hooks/useFormField.tsx | 9 ++++-- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/packages/clerk-js/src/ui/elements/FormControl.tsx b/packages/clerk-js/src/ui/elements/FormControl.tsx index 83ff6826dd5..170d6bd8226 100644 --- a/packages/clerk-js/src/ui/elements/FormControl.tsx +++ b/packages/clerk-js/src/ui/elements/FormControl.tsx @@ -40,7 +40,7 @@ function useFormTextAnimation() { transitionTimingFunction: t.transitionTiming.$common, }); }, - [prefersReducedMotion], + [prefersReducedMotion, appearanceAnimations], ); return { @@ -48,17 +48,14 @@ function useFormTextAnimation() { }; } -const useCalculateErrorTextHeight = ({ feedback }: { feedback: string }) => { +const useCalculateErrorTextHeight = ({ feedback: _feedback }: { feedback: string }) => { const [height, setHeight] = useState(0); - const calculateHeight = useCallback( - (element: HTMLElement | null) => { - if (element) { - setHeight(element.scrollHeight + element.offsetTop * 2); - } - }, - [feedback], - ); + const calculateHeight = useCallback((element: HTMLElement | null) => { + if (element) { + setHeight(element.scrollHeight + element.offsetTop * 2); + } + }, []); return { height, @@ -143,10 +140,9 @@ export const FormFeedback = (props: FormFeedbackProps) => { return { elementDescriptor: descriptor, elementId: id ? descriptor?.setId?.(id) : undefined, - // We only want the id applied when the feedback type is an error - // to avoid having multiple elements in the dom with the same id attribute. - // We also only have aria-describedby applied to the input when it is an error. - id: type === 'error' ? errorMessageId : undefined, + // Generate unique IDs for all feedback types to enable aria-describedby + // Use errorMessageId for errors to maintain existing behavior, otherwise generate a unique ID + id: type === 'error' ? errorMessageId : `${id}-${type}-feedback`, }; }; @@ -163,6 +159,13 @@ export const FormFeedback = (props: FormFeedbackProps) => { const InfoComponentA = FormInfoComponent[feedbacks.a?.feedbackType || 'info']; const InfoComponentB = FormInfoComponent[feedbacks.b?.feedbackType || 'info']; + // Determine which feedback is currently visible and get its ID + const currentFeedbackId = feedbacks.a?.shouldEnter + ? getElementProps(feedbacks.a?.feedbackType).id + : feedbacks.b?.shouldEnter + ? getElementProps(feedbacks.b?.feedbackType).id + : undefined; + return ( { }} center={center} sx={[getFormTextAnimation(!!feedback), sx]} + data-current-feedback-id={currentFeedbackId} > & StyleVariants((props, ref) => { const fieldControl = useFormField() || {}; - // @ts-expect-error Typescript is complaining that `errorMessageId` does not exist. We are clearly passing them from above. - const { errorMessageId, ignorePasswordManager, feedbackType, ...fieldControlProps } = sanitizeInputProps( - fieldControl, - ['errorMessageId', 'ignorePasswordManager', 'feedbackType'], - ); + // @ts-expect-error Typescript is complaining that `errorMessageId` and `feedbackMessageId` do not exist. We are clearly passing them from above. + const { errorMessageId, feedbackMessageId, ignorePasswordManager, feedbackType, ...fieldControlProps } = + sanitizeInputProps(fieldControl, ['errorMessageId', 'feedbackMessageId', 'ignorePasswordManager', 'feedbackType']); const propsWithoutVariants = filterProps({ ...props, @@ -106,7 +104,7 @@ export const Input = React.forwardRef((props, ref) required={_required} id={props.id || fieldControlProps.id} aria-invalid={_hasError} - aria-describedby={errorMessageId ? errorMessageId : undefined} + aria-describedby={[errorMessageId, feedbackMessageId].filter(Boolean).join(' ') || undefined} aria-required={_required} aria-disabled={_disabled} data-feedback={feedbackType} diff --git a/packages/clerk-js/src/ui/primitives/hooks/useFormField.tsx b/packages/clerk-js/src/ui/primitives/hooks/useFormField.tsx index 662fa6a75ee..674c7b8e448 100644 --- a/packages/clerk-js/src/ui/primitives/hooks/useFormField.tsx +++ b/packages/clerk-js/src/ui/primitives/hooks/useFormField.tsx @@ -11,6 +11,7 @@ type FormFieldProviderProps = ReturnType>['pr type FormFieldContextValue = Omit & { errorMessageId?: string; + feedbackMessageId?: string; id?: string; fieldId?: FieldId; hasError: boolean; @@ -49,9 +50,10 @@ export const FormFieldContextProvider = (props: React.PropsWithChildren ({ isRequired, @@ -60,6 +62,7 @@ export const FormFieldContextProvider = (props: React.PropsWithChildren Date: Fri, 3 Oct 2025 16:50:13 -0400 Subject: [PATCH 2/8] conditionally apply aria-live --- packages/clerk-js/src/ui/elements/FormControl.tsx | 2 ++ packages/clerk-js/src/ui/primitives/FormErrorText.tsx | 3 +-- packages/clerk-js/src/ui/primitives/FormInfoText.tsx | 1 - packages/clerk-js/src/ui/primitives/FormSuccessText.tsx | 3 +-- packages/clerk-js/src/ui/primitives/FormWarningText.tsx | 1 - 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/clerk-js/src/ui/elements/FormControl.tsx b/packages/clerk-js/src/ui/elements/FormControl.tsx index 170d6bd8226..36f637462b1 100644 --- a/packages/clerk-js/src/ui/elements/FormControl.tsx +++ b/packages/clerk-js/src/ui/elements/FormControl.tsx @@ -186,6 +186,7 @@ export const FormFeedback = (props: FormFeedbackProps) => { getFormTextAnimation(!!feedbacks.a?.shouldEnter, { inDelay: true }), ]} localizationKey={titleize(feedbacks.a?.feedback)} + aria-live={feedbacks.a?.shouldEnter ? 'polite' : 'off'} /> { getFormTextAnimation(!!feedbacks.b?.shouldEnter, { inDelay: true }), ]} localizationKey={titleize(feedbacks.b?.feedback)} + aria-live={feedbacks.b?.shouldEnter ? 'polite' : 'off'} /> ); diff --git a/packages/clerk-js/src/ui/primitives/FormErrorText.tsx b/packages/clerk-js/src/ui/primitives/FormErrorText.tsx index 4850365f4a5..ebdf83aa3b4 100644 --- a/packages/clerk-js/src/ui/primitives/FormErrorText.tsx +++ b/packages/clerk-js/src/ui/primitives/FormErrorText.tsx @@ -20,7 +20,7 @@ const { applyVariants } = createVariants(theme => ({ variants: {}, })); -type FormErrorTextProps = React.PropsWithChildren>; +type FormErrorTextProps = React.PropsWithChildren & { role?: string }>; export const FormErrorText = forwardRef((props, ref) => { const { hasError, errorMessageId } = useFormField() || {}; @@ -35,7 +35,6 @@ export const FormErrorText = forwardRef((props, ((props, ref) ({ variants: {}, })); -export type FormTextProps = React.PropsWithChildren>; +export type FormTextProps = React.PropsWithChildren & { role?: string }>; export const FormSuccessText = forwardRef((props, ref) => { const { children, ...rest } = props; @@ -28,7 +28,6 @@ export const FormSuccessText = forwardRef((props, re diff --git a/packages/clerk-js/src/ui/primitives/FormWarningText.tsx b/packages/clerk-js/src/ui/primitives/FormWarningText.tsx index 381d356fbb8..9132881b954 100644 --- a/packages/clerk-js/src/ui/primitives/FormWarningText.tsx +++ b/packages/clerk-js/src/ui/primitives/FormWarningText.tsx @@ -13,7 +13,6 @@ export const FormWarningText = forwardRef((props, re From 402fbf48991d9b6e61b70f69f585fb834cbde308 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 3 Oct 2025 17:09:12 -0400 Subject: [PATCH 3/8] wip --- packages/clerk-js/src/ui/elements/FormControl.tsx | 5 ++--- packages/clerk-js/src/ui/primitives/FormErrorText.tsx | 2 +- packages/clerk-js/src/ui/primitives/FormSuccessText.tsx | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/clerk-js/src/ui/elements/FormControl.tsx b/packages/clerk-js/src/ui/elements/FormControl.tsx index 36f637462b1..c92b83c67a2 100644 --- a/packages/clerk-js/src/ui/elements/FormControl.tsx +++ b/packages/clerk-js/src/ui/elements/FormControl.tsx @@ -140,8 +140,8 @@ export const FormFeedback = (props: FormFeedbackProps) => { return { elementDescriptor: descriptor, elementId: id ? descriptor?.setId?.(id) : undefined, - // Generate unique IDs for all feedback types to enable aria-describedby - // Use errorMessageId for errors to maintain existing behavior, otherwise generate a unique ID + // Generate unique IDs for all feedback types to enable aria-describedby usage. + // Use errorMessageId for errors to maintain existing behavior, otherwise generate a unique ID. id: type === 'error' ? errorMessageId : `${id}-${type}-feedback`, }; }; @@ -159,7 +159,6 @@ export const FormFeedback = (props: FormFeedbackProps) => { const InfoComponentA = FormInfoComponent[feedbacks.a?.feedbackType || 'info']; const InfoComponentB = FormInfoComponent[feedbacks.b?.feedbackType || 'info']; - // Determine which feedback is currently visible and get its ID const currentFeedbackId = feedbacks.a?.shouldEnter ? getElementProps(feedbacks.a?.feedbackType).id : feedbacks.b?.shouldEnter diff --git a/packages/clerk-js/src/ui/primitives/FormErrorText.tsx b/packages/clerk-js/src/ui/primitives/FormErrorText.tsx index ebdf83aa3b4..7b57776c34f 100644 --- a/packages/clerk-js/src/ui/primitives/FormErrorText.tsx +++ b/packages/clerk-js/src/ui/primitives/FormErrorText.tsx @@ -20,7 +20,7 @@ const { applyVariants } = createVariants(theme => ({ variants: {}, })); -type FormErrorTextProps = React.PropsWithChildren & { role?: string }>; +type FormErrorTextProps = React.PropsWithChildren>; export const FormErrorText = forwardRef((props, ref) => { const { hasError, errorMessageId } = useFormField() || {}; diff --git a/packages/clerk-js/src/ui/primitives/FormSuccessText.tsx b/packages/clerk-js/src/ui/primitives/FormSuccessText.tsx index e168ec01acf..cb85a621e25 100644 --- a/packages/clerk-js/src/ui/primitives/FormSuccessText.tsx +++ b/packages/clerk-js/src/ui/primitives/FormSuccessText.tsx @@ -19,7 +19,7 @@ export const { applyVariants } = createVariants(theme => ({ variants: {}, })); -export type FormTextProps = React.PropsWithChildren & { role?: string }>; +export type FormTextProps = React.PropsWithChildren>; export const FormSuccessText = forwardRef((props, ref) => { const { children, ...rest } = props; From 8192f55f430b2a94e1bb8e5e09f067686fe56ed9 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 3 Oct 2025 17:16:05 -0400 Subject: [PATCH 4/8] wip --- .../clerk-js/src/ui/elements/FormControl.tsx | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/clerk-js/src/ui/elements/FormControl.tsx b/packages/clerk-js/src/ui/elements/FormControl.tsx index c92b83c67a2..40c72f5b5ff 100644 --- a/packages/clerk-js/src/ui/elements/FormControl.tsx +++ b/packages/clerk-js/src/ui/elements/FormControl.tsx @@ -48,14 +48,17 @@ function useFormTextAnimation() { }; } -const useCalculateErrorTextHeight = ({ feedback: _feedback }: { feedback: string }) => { +const useCalculateErrorTextHeight = ({ feedback }: { feedback: string }) => { const [height, setHeight] = useState(0); - const calculateHeight = useCallback((element: HTMLElement | null) => { - if (element) { - setHeight(element.scrollHeight + element.offsetTop * 2); - } - }, []); + const calculateHeight = useCallback( + (element: HTMLElement | null) => { + if (element) { + setHeight(element.scrollHeight + element.offsetTop * 2); + } + }, + [feedback], + ); return { height, @@ -159,12 +162,6 @@ export const FormFeedback = (props: FormFeedbackProps) => { const InfoComponentA = FormInfoComponent[feedbacks.a?.feedbackType || 'info']; const InfoComponentB = FormInfoComponent[feedbacks.b?.feedbackType || 'info']; - const currentFeedbackId = feedbacks.a?.shouldEnter - ? getElementProps(feedbacks.a?.feedbackType).id - : feedbacks.b?.shouldEnter - ? getElementProps(feedbacks.b?.feedbackType).id - : undefined; - return ( { }} center={center} sx={[getFormTextAnimation(!!feedback), sx]} - data-current-feedback-id={currentFeedbackId} > Date: Tue, 7 Oct 2025 14:01:29 -0400 Subject: [PATCH 5/8] add test cases --- .../ui/elements/__tests__/PlainInput.test.tsx | 109 +++++++++++++++++- 1 file changed, 104 insertions(+), 5 deletions(-) diff --git a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx index f068914d784..22339c6ff5d 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it } from 'vitest'; @@ -21,6 +21,9 @@ const createField = (...params: Parameters) => { {...props} /> + + + ); }); @@ -132,15 +135,21 @@ describe('PlainInput', () => { placeholder: 'some placeholder', }); - const { getByRole, getByLabelText, findByText } = render(, { wrapper }); + const { getByRole, getByLabelText, findByText, container } = render(, { wrapper }); await userEvent.click(getByRole('button', { name: /set error/i })); expect(await findByText(/Some Error/i)).toBeInTheDocument(); - const label = getByLabelText(/some label/i); - expect(label).toHaveAttribute('aria-invalid', 'true'); - expect(label).toHaveAttribute('aria-describedby', 'error-firstname'); + const input = getByLabelText(/some label/i); + expect(input).toHaveAttribute('aria-invalid', 'true'); + // The input should have both error IDs in aria-describedby + expect(input).toHaveAttribute('aria-describedby', 'error-firstname firstname-error-feedback'); + + // Verify the error message element has the correct ID + const errorElement = container.querySelector('#error-firstname'); + expect(errorElement).toBeInTheDocument(); + expect(errorElement).toHaveTextContent(/Some Error/i); }); it('with info', async () => { @@ -157,4 +166,94 @@ describe('PlainInput', () => { fireEvent.focus(await findByLabelText(/some label/i)); expect(await findByText(/some info/i)).toBeInTheDocument(); }); + + it('with success feedback and aria-describedby', async () => { + const { wrapper } = await createFixtures(); + const { Field } = createField('firstname', 'init value', { + type: 'text', + label: 'some label', + placeholder: 'some placeholder', + }); + + const { getByRole, getByLabelText, findByText, container } = render(, { wrapper }); + + await userEvent.click(getByRole('button', { name: /set success/i })); + + expect(await findByText(/Some Success/i)).toBeInTheDocument(); + + const input = getByLabelText(/some label/i); + expect(input).toHaveAttribute('aria-invalid', 'false'); + expect(input).toHaveAttribute('aria-describedby', 'firstname-success-feedback'); + + // Verify the success message element has the correct ID + const successElement = container.querySelector('#firstname-success-feedback'); + expect(successElement).toBeInTheDocument(); + expect(successElement).toHaveTextContent(/Some Success/i); + }); + + it('transitions between error and success feedback types', async () => { + const { wrapper } = await createFixtures(); + const { Field } = createField('firstname', 'init value', { + type: 'text', + label: 'some label', + placeholder: 'some placeholder', + }); + + const { getByRole, getByLabelText, findByText, container } = render(, { wrapper }); + + // Start with error + await userEvent.click(getByRole('button', { name: /set error/i })); + expect(await findByText(/Some Error/i)).toBeInTheDocument(); + + let input = getByLabelText(/some label/i); + expect(input).toHaveAttribute('aria-invalid', 'true'); + // Error includes both IDs for backwards compatibility and new behavior + expect(input).toHaveAttribute('aria-describedby', 'error-firstname firstname-error-feedback'); + + // Transition to success + await userEvent.click(getByRole('button', { name: /set success/i })); + expect(await findByText(/Some Success/i)).toBeInTheDocument(); + + input = getByLabelText(/some label/i); + expect(input).toHaveAttribute('aria-invalid', 'false'); + expect(input).toHaveAttribute('aria-describedby', 'firstname-success-feedback'); + + // Verify success element exists with proper ID + const successElement = container.querySelector('#firstname-success-feedback'); + expect(successElement).toBeInTheDocument(); + expect(successElement).toHaveTextContent(/Some Success/i); + }); + + it('aria-live attribute is correctly applied', async () => { + const { wrapper } = await createFixtures(); + const { Field } = createField('firstname', 'init value', { + type: 'text', + label: 'some label', + placeholder: 'some placeholder', + }); + + const { getByRole, findByText, container } = render(, { wrapper }); + + // Set error feedback + await userEvent.click(getByRole('button', { name: /set error/i })); + expect(await findByText(/Some Error/i)).toBeInTheDocument(); + + // Verify the visible error message has aria-live="polite" + const errorElement = container.querySelector('#error-firstname'); + expect(errorElement).toHaveAttribute('aria-live', 'polite'); + + // Transition to success + await userEvent.click(getByRole('button', { name: /set success/i })); + expect(await findByText(/Some Success/i)).toBeInTheDocument(); + + // Verify the visible success message has aria-live="polite" + const successElement = container.querySelector('#firstname-success-feedback'); + expect(successElement).toHaveAttribute('aria-live', 'polite'); + + // The previous error message should now have aria-live="off" (though it might still exist in DOM but hidden) + // We can verify the currently visible element has aria-live="polite" + const allAriaLivePolite = container.querySelectorAll('[aria-live="polite"]'); + // At least the success message should have aria-live="polite" + expect(allAriaLivePolite.length).toBeGreaterThanOrEqual(1); + }); }); From 651f11b4602235e7b7526ebca26976625b687470 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 7 Oct 2025 15:15:16 -0400 Subject: [PATCH 6/8] handle feedback --- packages/clerk-js/src/ui/elements/FieldControl.tsx | 3 +-- packages/clerk-js/src/ui/elements/FormControl.tsx | 8 +++----- .../src/ui/elements/__tests__/PlainInput.test.tsx | 11 ++++------- .../clerk-js/src/ui/primitives/FormErrorText.tsx | 3 +-- packages/clerk-js/src/ui/primitives/FormInfoText.tsx | 3 +-- packages/clerk-js/src/ui/primitives/Input.tsx | 10 ++++++---- .../src/ui/primitives/hooks/useFormField.tsx | 12 ++++++------ 7 files changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/clerk-js/src/ui/elements/FieldControl.tsx b/packages/clerk-js/src/ui/elements/FieldControl.tsx index 8675181485a..f6166cad5ce 100644 --- a/packages/clerk-js/src/ui/elements/FieldControl.tsx +++ b/packages/clerk-js/src/ui/elements/FieldControl.tsx @@ -154,12 +154,11 @@ const FieldLabelRow = (props: PropsWithChildren) => { }; const FieldFeedback = (props: Pick) => { - const { fieldId, debouncedFeedback, errorMessageId } = useFormField(); + const { fieldId, debouncedFeedback } = useFormField(); return ( ['debounced'] & { id: FieldId }> & { - errorMessageId?: string; elementDescriptors?: Partial>; center?: boolean; sx?: ThemableCssProp; }; export const FormFeedback = (props: FormFeedbackProps) => { - const { id, elementDescriptors, sx, feedback, feedbackType = 'info', center = false, errorMessageId } = props; + const { id, elementDescriptors, sx, feedback, feedbackType = 'info', center = false } = props; const feedbacksRef = useRef<{ a?: Feedback; b?: Feedback; @@ -143,9 +142,8 @@ export const FormFeedback = (props: FormFeedbackProps) => { return { elementDescriptor: descriptor, elementId: id ? descriptor?.setId?.(id) : undefined, - // Generate unique IDs for all feedback types to enable aria-describedby usage. - // Use errorMessageId for errors to maintain existing behavior, otherwise generate a unique ID. - id: type === 'error' ? errorMessageId : `${id}-${type}-feedback`, + // Use legacy pattern for errors (backwards compatible), new pattern for other types + id: type === 'error' ? `error-${id}` : `${id}-${type}-feedback`, }; }; diff --git a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx index 22339c6ff5d..9f6043ee7d1 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx @@ -143,8 +143,7 @@ describe('PlainInput', () => { const input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'true'); - // The input should have both error IDs in aria-describedby - expect(input).toHaveAttribute('aria-describedby', 'error-firstname firstname-error-feedback'); + expect(input).toHaveAttribute('aria-describedby', 'error-firstname'); // Verify the error message element has the correct ID const errorElement = container.querySelector('#error-firstname'); @@ -207,8 +206,7 @@ describe('PlainInput', () => { let input = getByLabelText(/some label/i); expect(input).toHaveAttribute('aria-invalid', 'true'); - // Error includes both IDs for backwards compatibility and new behavior - expect(input).toHaveAttribute('aria-describedby', 'error-firstname firstname-error-feedback'); + expect(input).toHaveAttribute('aria-describedby', 'error-firstname'); // Transition to success await userEvent.click(getByRole('button', { name: /set success/i })); @@ -251,9 +249,8 @@ describe('PlainInput', () => { expect(successElement).toHaveAttribute('aria-live', 'polite'); // The previous error message should now have aria-live="off" (though it might still exist in DOM but hidden) - // We can verify the currently visible element has aria-live="polite" + // Verify exactly one element has aria-live="polite" at a time const allAriaLivePolite = container.querySelectorAll('[aria-live="polite"]'); - // At least the success message should have aria-live="polite" - expect(allAriaLivePolite.length).toBeGreaterThanOrEqual(1); + expect(allAriaLivePolite.length).toBe(1); }); }); diff --git a/packages/clerk-js/src/ui/primitives/FormErrorText.tsx b/packages/clerk-js/src/ui/primitives/FormErrorText.tsx index 7b57776c34f..d00b7acdfdc 100644 --- a/packages/clerk-js/src/ui/primitives/FormErrorText.tsx +++ b/packages/clerk-js/src/ui/primitives/FormErrorText.tsx @@ -23,7 +23,7 @@ const { applyVariants } = createVariants(theme => ({ type FormErrorTextProps = React.PropsWithChildren>; export const FormErrorText = forwardRef((props, ref) => { - const { hasError, errorMessageId } = useFormField() || {}; + const { hasError } = useFormField() || {}; if (!hasError && !props.children) { return null; @@ -35,7 +35,6 @@ export const FormErrorText = forwardRef((props, diff --git a/packages/clerk-js/src/ui/primitives/FormInfoText.tsx b/packages/clerk-js/src/ui/primitives/FormInfoText.tsx index 36f02d98854..9ee7365d5b3 100644 --- a/packages/clerk-js/src/ui/primitives/FormInfoText.tsx +++ b/packages/clerk-js/src/ui/primitives/FormInfoText.tsx @@ -6,7 +6,7 @@ import { useFormField } from './hooks/useFormField'; import { Text } from './Text'; export const FormInfoText = forwardRef((props, ref) => { - const { hasError, errorMessageId } = useFormField() || {}; + const { hasError } = useFormField() || {}; if (!hasError && !props.children) { return null; @@ -16,7 +16,6 @@ export const FormInfoText = forwardRef((props, ref) diff --git a/packages/clerk-js/src/ui/primitives/Input.tsx b/packages/clerk-js/src/ui/primitives/Input.tsx index 8673a13fc67..ad1395c2143 100644 --- a/packages/clerk-js/src/ui/primitives/Input.tsx +++ b/packages/clerk-js/src/ui/primitives/Input.tsx @@ -63,9 +63,11 @@ export type InputProps = PrimitiveProps<'input'> & StyleVariants((props, ref) => { const fieldControl = useFormField() || {}; - // @ts-expect-error Typescript is complaining that `errorMessageId` and `feedbackMessageId` do not exist. We are clearly passing them from above. - const { errorMessageId, feedbackMessageId, ignorePasswordManager, feedbackType, ...fieldControlProps } = - sanitizeInputProps(fieldControl, ['errorMessageId', 'feedbackMessageId', 'ignorePasswordManager', 'feedbackType']); + // @ts-expect-error Typescript is complaining that `feedbackMessageId` does not exist. We are clearly passing them from above. + const { feedbackMessageId, ignorePasswordManager, feedbackType, ...fieldControlProps } = sanitizeInputProps( + fieldControl, + ['feedbackMessageId', 'ignorePasswordManager', 'feedbackType'], + ); const propsWithoutVariants = filterProps({ ...props, @@ -104,7 +106,7 @@ export const Input = React.forwardRef((props, ref) required={_required} id={props.id || fieldControlProps.id} aria-invalid={_hasError} - aria-describedby={[errorMessageId, feedbackMessageId].filter(Boolean).join(' ') || undefined} + aria-describedby={feedbackMessageId || undefined} aria-required={_required} aria-disabled={_disabled} data-feedback={feedbackType} diff --git a/packages/clerk-js/src/ui/primitives/hooks/useFormField.tsx b/packages/clerk-js/src/ui/primitives/hooks/useFormField.tsx index 674c7b8e448..03856d2473f 100644 --- a/packages/clerk-js/src/ui/primitives/hooks/useFormField.tsx +++ b/packages/clerk-js/src/ui/primitives/hooks/useFormField.tsx @@ -10,7 +10,6 @@ type FormFieldProviderProps = ReturnType>['pr }; type FormFieldContextValue = Omit & { - errorMessageId?: string; feedbackMessageId?: string; id?: string; fieldId?: FieldId; @@ -52,8 +51,12 @@ export const FormFieldContextProvider = (props: React.PropsWithChildren ({ isRequired, @@ -61,7 +64,6 @@ export const FormFieldContextProvider = (props: React.PropsWithChildren Date: Tue, 7 Oct 2025 15:27:15 -0400 Subject: [PATCH 7/8] removed unused import --- packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx index 9f6043ee7d1..6adaec9b8f2 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it } from 'vitest'; From f74bb94bc525ce4b45b590d139d0a8d97774f10d Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 8 Oct 2025 10:03:19 -0400 Subject: [PATCH 8/8] add changeset --- .changeset/empty-laws-boil.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/empty-laws-boil.md diff --git a/.changeset/empty-laws-boil.md b/.changeset/empty-laws-boil.md new file mode 100644 index 00000000000..508f1b4d3ce --- /dev/null +++ b/.changeset/empty-laws-boil.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Ensure inputs are properly connected to feedback messages via `aria-describedby` usage.