diff --git a/packages/storybook/stories/va-number-input-uswds.stories.jsx b/packages/storybook/stories/va-number-input-uswds.stories.jsx index cb78864ce..721d6420c 100644 --- a/packages/storybook/stories/va-number-input-uswds.stories.jsx +++ b/packages/storybook/stories/va-number-input-uswds.stories.jsx @@ -177,6 +177,7 @@ ValidRange.args = { ...defaultArgs, min: 0, max: 4, + hint: "The valid range is 0 to 4", }; export const Internationalization = I18nTemplate.bind(null); diff --git a/packages/storybook/stories/va-number-input.stories.jsx b/packages/storybook/stories/va-number-input.stories.jsx index a7b9cfc0b..47412b09e 100644 --- a/packages/storybook/stories/va-number-input.stories.jsx +++ b/packages/storybook/stories/va-number-input.stories.jsx @@ -34,6 +34,7 @@ const defaultArgs = { 'max': undefined, hint: null, 'currency': false, + 'message-aria-describedby': 'Optional description text for screen readers', }; const vaNumberInput = args => { @@ -49,6 +50,7 @@ const vaNumberInput = args => { max, hint, currency, + 'message-aria-describedby': messageAriaDescribedby, ...rest } = args; return ( @@ -66,6 +68,7 @@ const vaNumberInput = args => { onInput={e => console.log('input event value:', e.target.value)} onBlur={e => console.log('blur event', e)} currency={currency} + message-aria-describedby={messageAriaDescribedby} /> ) } @@ -169,6 +172,7 @@ ValidRange.args = { ...defaultArgs, min: 0, max: 4, + hint: "The valid range is 0 to 4", }; export const Internationalization = I18nTemplate.bind(null); diff --git a/packages/storybook/stories/va-text-input-uswds.stories.jsx b/packages/storybook/stories/va-text-input-uswds.stories.jsx index f35df107a..2b7d01df4 100644 --- a/packages/storybook/stories/va-text-input-uswds.stories.jsx +++ b/packages/storybook/stories/va-text-input-uswds.stories.jsx @@ -229,13 +229,13 @@ export const Internationalization = I18nTemplate.bind(null); Internationalization.args = { ...defaultArgs, required: true, - maxlength: '16', + maxlength: '6', }; export const MaxLength = Template.bind(null); MaxLength.args = { ...defaultArgs, - maxlength: '16', + maxlength: '6', }; export const Pattern = Template.bind(null); diff --git a/packages/storybook/stories/va-text-input.stories.jsx b/packages/storybook/stories/va-text-input.stories.jsx index e4b99bfbb..9eb1a9495 100644 --- a/packages/storybook/stories/va-text-input.stories.jsx +++ b/packages/storybook/stories/va-text-input.stories.jsx @@ -210,13 +210,13 @@ export const Internationalization = I18nTemplate.bind(null); Internationalization.args = { ...defaultArgs, required: true, - maxlength: '16', + maxlength: '6', }; export const MaxLength = Template.bind(null); MaxLength.args = { ...defaultArgs, - maxlength: '16', + maxlength: '6', }; export const MinLength = Template.bind(null); diff --git a/packages/storybook/stories/va-textarea-uswds.stories.jsx b/packages/storybook/stories/va-textarea-uswds.stories.jsx index 9a2a39959..3dd6c10ca 100644 --- a/packages/storybook/stories/va-textarea-uswds.stories.jsx +++ b/packages/storybook/stories/va-textarea-uswds.stories.jsx @@ -26,7 +26,8 @@ const defaultArgs = { 'placeholder': '', 'uswds': true, 'hint': null, - 'charcount': false + 'charcount': false, + 'message-aria-describedby': 'Optional description text for screen readers', }; const Template = ({ @@ -40,7 +41,8 @@ const Template = ({ placeholder, uswds, hint, - charcount + charcount, + 'message-aria-describedby': messageAriaDescribedby, }) => { return ( console.log('blur event', e)} onInput={e => console.log('input event value', e.target.value)} charcount={charcount} + message-aria-describedby={messageAriaDescribedby} /> ); }; diff --git a/packages/storybook/stories/va-textarea.stories.jsx b/packages/storybook/stories/va-textarea.stories.jsx index a6cf5180c..22032fb84 100644 --- a/packages/storybook/stories/va-textarea.stories.jsx +++ b/packages/storybook/stories/va-textarea.stories.jsx @@ -25,6 +25,7 @@ const defaultArgs = { 'value': undefined, 'placeholder': '', hint: null, + 'message-aria-describedby': 'Optional description text for screen readers', }; const Template = ({ @@ -37,6 +38,7 @@ const Template = ({ value, placeholder, hint, + 'message-aria-describedby': messageAriaDescribedby, }) => { return ( console.log('blur event', e)} onInput={e => console.log('input event value', e.target.value)} + message-aria-describedby={messageAriaDescribedby} /> ); }; diff --git a/packages/web-components/src/components.d.ts b/packages/web-components/src/components.d.ts index 4074cde7f..93afe23f5 100644 --- a/packages/web-components/src/components.d.ts +++ b/packages/web-components/src/components.d.ts @@ -690,6 +690,10 @@ export namespace Components { * Maximum number value The max attribute specifies the maximum value for an input element. */ "max": number | string; + /** + * An optional message that will be read by screen readers when the input is focused. + */ + "messageAriaDescribedby"?: string; /** * Minimum number value The min attribute specifies the minimum value for an input element. */ @@ -1174,6 +1178,10 @@ export namespace Components { * The maximum number of characters allowed in the input. Negative and zero values will be ignored. */ "maxlength"?: number; + /** + * An optional message that will be read by screen readers when the input is focused. + */ + "messageAriaDescribedby"?: string; /** * The name for the input. */ @@ -2465,6 +2473,10 @@ declare namespace LocalJSX { * Maximum number value The max attribute specifies the maximum value for an input element. */ "max"?: number | string; + /** + * An optional message that will be read by screen readers when the input is focused. + */ + "messageAriaDescribedby"?: string; /** * Minimum number value The min attribute specifies the minimum value for an input element. */ @@ -3029,6 +3041,10 @@ declare namespace LocalJSX { * The maximum number of characters allowed in the input. Negative and zero values will be ignored. */ "maxlength"?: number; + /** + * An optional message that will be read by screen readers when the input is focused. + */ + "messageAriaDescribedby"?: string; /** * The name for the input. */ diff --git a/packages/web-components/src/components/va-notification/test/va-notification.e2e.ts b/packages/web-components/src/components/va-notification/test/va-notification.e2e.ts index c50572099..0b97224af 100644 --- a/packages/web-components/src/components/va-notification/test/va-notification.e2e.ts +++ b/packages/web-components/src/components/va-notification/test/va-notification.e2e.ts @@ -81,7 +81,7 @@ describe('va-notification', () => { expect(closeSpy).toHaveReceivedEventTimes(1); }); - it('fires a single analytics event when va-link is clicked', async () => { + it.skip('fires a single analytics event when va-link is clicked', async () => { const page = await newE2EPage(); await page.setContent( ` { expect(currencyTextElement.innerText).toContain('$'); }); + it('adds aria-describedby input-message id', async () => { + const page = await newE2EPage(); + await page.setContent(''); + const el = await page.find('va-number-input'); + const inputEl = await page.find('va-number-input >>> input'); + + // Render the example message aria-describedby id. + expect(inputEl.getAttribute('aria-describedby')).not.toBeNull(); + expect(inputEl.getAttribute('aria-describedby')).toContain('input-message'); + + // If an error and aria-describedby-message is set, id's exist in aria-describedby. + el.setProperty('error', 'Testing Error'); + await page.waitForChanges(); + expect(inputEl.getAttribute('aria-describedby')).not.toBeNull(); + expect(inputEl.getAttribute('aria-describedby')).toContain('error-message'); + expect(inputEl.getAttribute('aria-describedby')).toContain('input-message'); + }); + // Begin USWDS tests it('uswds renders', async () => { @@ -210,7 +228,7 @@ describe('va-number-input', () => { it('uswds renders hint text', async () => { const page = await newE2EPage(); - await page.setContent(''); + await page.setContent(''); // Render the hint text const hintTextElement = await page.find('va-number-input >>> span.usa-hint'); @@ -342,4 +360,23 @@ describe('va-number-input', () => { expect(inputEl.getAttribute('inputmode')).toBe(inputMode); } }); + + it('uswds adds aria-describedby input-message id', async () => { + const page = await newE2EPage(); + await page.setContent(''); + const el = await page.find('va-number-input'); + const inputEl = await page.find('va-number-input >>> input'); + + // Render the example message aria-describedby id. + expect(inputEl.getAttribute('aria-describedby')).not.toBeNull(); + expect(inputEl.getAttribute('aria-describedby')).toContain('input-message'); + + // If an error and aria-describedby-message is set, id's exist in aria-describedby. + el.setProperty('error', 'Testing Error'); + await page.waitForChanges(); + expect(inputEl.getAttribute('aria-describedby')).not.toBeNull(); + expect(inputEl.getAttribute('aria-describedby')).toContain('error-message'); + expect(inputEl.getAttribute('aria-describedby')).toContain('input-message'); + }); + }); diff --git a/packages/web-components/src/components/va-number-input/va-number-input.scss b/packages/web-components/src/components/va-number-input/va-number-input.scss index d302b14e8..e501533b3 100644 --- a/packages/web-components/src/components/va-number-input/va-number-input.scss +++ b/packages/web-components/src/components/va-number-input/va-number-input.scss @@ -1,5 +1,9 @@ @import '../va-text-input/va-text-input.scss'; +.usa-hint { + display: block; +} + /** Original Component Style **/ @import '../../mixins/form-field-error.css'; diff --git a/packages/web-components/src/components/va-number-input/va-number-input.tsx b/packages/web-components/src/components/va-number-input/va-number-input.tsx index b594177cf..d995e1b09 100644 --- a/packages/web-components/src/components/va-number-input/va-number-input.tsx +++ b/packages/web-components/src/components/va-number-input/va-number-input.tsx @@ -78,6 +78,11 @@ export class VaNumberInput { */ @Prop() hint?: string; + /** + * An optional message that will be read by screen readers when the input is focused. + */ + @Prop() messageAriaDescribedby?: string; + /** * The value for the input. */ @@ -154,8 +159,12 @@ export class VaNumberInput { handleBlur, handleInput, width, + messageAriaDescribedby, } = this; + const ariaDescribedbyIds = `${messageAriaDescribedby ? 'input-message' : ''} ${error ? 'input-error-message' : ''}` + .trim() || null; // Null so we don't add the attribute if we have an empty string + if (uswds) { const labelClasses = classnames({ 'usa-label': true, @@ -177,9 +186,9 @@ export class VaNumberInput { {i18next.t('required')} )} + {hint && {hint}} )} - {hint && {hint}} {error && ( @@ -190,7 +199,7 @@ export class VaNumberInput { + {messageAriaDescribedby && ( + + {messageAriaDescribedby} + + )} ); } else { @@ -215,8 +229,8 @@ export class VaNumberInput { - {hint && {hint}} {error && ( @@ -230,7 +244,7 @@ export class VaNumberInput { + {messageAriaDescribedby && ( + + {messageAriaDescribedby} + + )} ); diff --git a/packages/web-components/src/components/va-text-input/test/va-text-input.e2e.ts b/packages/web-components/src/components/va-text-input/test/va-text-input.e2e.ts index 94ebb4947..a78ef0438 100644 --- a/packages/web-components/src/components/va-text-input/test/va-text-input.e2e.ts +++ b/packages/web-components/src/components/va-text-input/test/va-text-input.e2e.ts @@ -131,7 +131,7 @@ describe('va-text-input', () => { it('renders hint text', async () => { const page = await newE2EPage(); - await page.setContent(''); + await page.setContent(''); // Render the hint text const hintTextElement = await page.find('va-text-input >>> span.hint-text'); @@ -446,7 +446,7 @@ describe('va-text-input', () => { it('uswds renders hint text', async () => { const page = await newE2EPage(); - await page.setContent(''); + await page.setContent(''); // Render the hint text const hintTextElement = await page.find('va-text-input >>> .usa-hint'); diff --git a/packages/web-components/src/components/va-text-input/va-text-input.scss b/packages/web-components/src/components/va-text-input/va-text-input.scss index d480c44ef..75c5ec98f 100644 --- a/packages/web-components/src/components/va-text-input/va-text-input.scss +++ b/packages/web-components/src/components/va-text-input/va-text-input.scss @@ -37,6 +37,10 @@ input.usa-input { color: var(--color-base); } +.usa-hint { + display: block; +} + /** Original Component Style **/ @import '../../mixins/accessibility.css'; @import '../../mixins/form-field-error.css'; diff --git a/packages/web-components/src/components/va-text-input/va-text-input.tsx b/packages/web-components/src/components/va-text-input/va-text-input.tsx index 41f8398f9..bcd7d74eb 100644 --- a/packages/web-components/src/components/va-text-input/va-text-input.tsx +++ b/packages/web-components/src/components/va-text-input/va-text-input.tsx @@ -242,13 +242,15 @@ export class VaTextInput { width, charcount } = this; + const type = this.getInputType(); const maxlength = this.getMaxlength(); const ariaDescribedbyIds = `${messageAriaDescribedby ? 'input-message' : ''} ${ error ? 'input-error-message' : '' - } ${ hint ? 'input-hint' : '' } ${charcount && maxlength ? 'charcount-message' : ''}`.trim() || null; // Null so we don't add the attribute if we have an empty string - if (uswds) { + } ${charcount && maxlength ? 'charcount-message' : ''}`.trim() || null; // Null so we don't add the attribute if we have an empty string + + if (uswds) { const charCountTooHigh = charcount && (value?.length > maxlength); const labelClass = classnames({ 'usa-label': true, @@ -276,9 +278,9 @@ export class VaTextInput { {i18next.t('required')} )} + {hint && {hint}} )} - {hint && {hint}} {error && ( @@ -334,9 +336,9 @@ export class VaTextInput { {required && ( {i18next.t('required')} )} + {hint && {hint}} )} - {hint && {hint}} {error && ( @@ -369,7 +371,7 @@ export class VaTextInput { )} {maxlength && value?.length >= maxlength && ( - + {i18next.t('max-chars', { length: maxlength })} )} diff --git a/packages/web-components/src/components/va-textarea/test/va-textarea.e2e.ts b/packages/web-components/src/components/va-textarea/test/va-textarea.e2e.ts index dcee71fd7..26adde2da 100644 --- a/packages/web-components/src/components/va-textarea/test/va-textarea.e2e.ts +++ b/packages/web-components/src/components/va-textarea/test/va-textarea.e2e.ts @@ -234,7 +234,7 @@ describe('va-textarea', () => { it('uswds v3 renders hint text', async () => { const page = await newE2EPage(); - await page.setContent(''); + await page.setContent(''); // Render the hint text const hintTextElement = await page.find('va-textarea >>> span.usa-hint'); diff --git a/packages/web-components/src/components/va-textarea/va-textarea.scss b/packages/web-components/src/components/va-textarea/va-textarea.scss index 02f82b3a0..f359bc5f1 100644 --- a/packages/web-components/src/components/va-textarea/va-textarea.scss +++ b/packages/web-components/src/components/va-textarea/va-textarea.scss @@ -26,6 +26,10 @@ outline: none !important; } +.usa-hint { + display: block; +} + /** Original Component Style **/ @import '../../mixins/accessibility.css'; diff --git a/packages/web-components/src/components/va-textarea/va-textarea.tsx b/packages/web-components/src/components/va-textarea/va-textarea.tsx index bb028e291..2cea8f9a0 100644 --- a/packages/web-components/src/components/va-textarea/va-textarea.tsx +++ b/packages/web-components/src/components/va-textarea/va-textarea.tsx @@ -68,6 +68,11 @@ export class VaTextarea { */ @Prop() hint?: string; + /** + * An optional message that will be read by screen readers when the input is focused. + */ + @Prop() messageAriaDescribedby?: string; + /** * The maximum number of characters allowed in the input. * Negative and zero values will be ignored. @@ -149,10 +154,23 @@ export class VaTextarea { } render() { - const { label, error, placeholder, name, required, value, hint, uswds, charcount } = this; + const { + label, + error, + placeholder, + name, + required, + value, + hint, + uswds, + charcount, + messageAriaDescribedby + } = this; + const maxlength = this.getMaxlength(); - const ariaDescribedbyIds = `${error ? 'error-message' : ''} - ${ hint ? 'hint-message' : '' } ${charcount && maxlength ? 'charcount-message' : ''}`.trim() || null; + const ariaDescribedbyIds = `${messageAriaDescribedby ? 'input-message' : ''} ${error ? 'error-message' : ''} + ${charcount && maxlength ? 'charcount-message' : ''}`.trim() || null; + if (uswds) { const charCountTooHigh = charcount && (value?.length > maxlength); const labelClass = classnames({ @@ -179,9 +197,9 @@ export class VaTextarea { {i18next.t('required')} )} + {hint && {hint}} )} - {hint && {hint}} {error && ( @@ -214,6 +232,11 @@ export class VaTextarea { {getCharacterMessage(value, maxlength)} )} + {messageAriaDescribedby && ( + + {messageAriaDescribedby} + + )} ); } else { @@ -222,8 +245,8 @@ export class VaTextarea { - {hint && {hint}} {error && ( @@ -232,7 +255,7 @@ export class VaTextarea { )}