diff --git a/cypress/component/FormField.spec.tsx b/cypress/component/FormField.spec.tsx index 47f05c44e4..c3a78339cf 100644 --- a/cypress/component/FormField.spec.tsx +++ b/cypress/component/FormField.spec.tsx @@ -52,7 +52,7 @@ describe('Form Field', () => { }); }); - context(`given the 'Alert' story is rendered`, () => { + context(`given the 'Caution' story is rendered`, () => { beforeEach(() => { cy.mount(); }); @@ -63,7 +63,10 @@ describe('Form Field', () => { it('should connect the input with the hint text', () => { cy.get('input').should('have.attr', 'aria-describedby'); - cy.get('input').should('have.ariaDescription', 'Cannot contain numbers'); + cy.get('input').should( + 'have.ariaDescription', + 'Alert: Password strength is weak, using more characters is recommended.' + ); }); }); @@ -78,7 +81,10 @@ describe('Form Field', () => { it('should connect the input with the hint text', () => { cy.get('input').should('have.attr', 'aria-describedby'); - cy.get('input').should('have.ariaDescription', 'Must Contain a number and a capital letter'); + cy.get('input').should( + 'have.ariaDescription', + 'Error: Must Contain a number and a capital letter' + ); }); }); diff --git a/cypress/component/TextArea.spec.tsx b/cypress/component/TextArea.spec.tsx index 71ba71aa68..2cee35ac5f 100644 --- a/cypress/component/TextArea.spec.tsx +++ b/cypress/component/TextArea.spec.tsx @@ -1,6 +1,4 @@ import {Basic} from '../../modules/react/text-area/stories/examples/Basic'; -import {Caution} from '../../modules/react/text-area/stories/examples/Caution'; -import {Error} from '../../modules/react/text-area/stories/examples/Error'; import {Disabled} from '../../modules/react/text-area/stories/examples/Disabled'; import {Placeholder} from '../../modules/react/text-area/stories/examples/Placeholder'; @@ -9,34 +7,32 @@ const getTextArea = () => { }; describe('Text Area', () => { - [Basic, Caution, Error].forEach(Example => { - context(`given the '${Example.name}' story is rendered`, () => { + context(`given the 'Basic' story is rendered`, () => { + beforeEach(() => { + cy.mount(); + }); + + it('should not have any axe errors', () => { + cy.checkA11y(); + }); + + context('when clicked', () => { beforeEach(() => { - cy.mount(); + getTextArea().click(); }); - it('should not have any axe errors', () => { - cy.checkA11y(); + it('should be focused', () => { + getTextArea().should('be.focused'); }); + }); - context('when clicked', () => { - beforeEach(() => { - getTextArea().click(); - }); - - it('should be focused', () => { - getTextArea().should('be.focused'); - }); + context('when text is entered', () => { + beforeEach(() => { + getTextArea().clear().type('Test'); }); - context('when text is entered', () => { - beforeEach(() => { - getTextArea().clear().type('Test'); - }); - - it('should reflect the text typed', () => { - getTextArea().should('have.value', 'Test'); - }); + it('should reflect the text typed', () => { + getTextArea().should('have.value', 'Test'); }); }); }); diff --git a/cypress/component/TextInput.spec.tsx b/cypress/component/TextInput.spec.tsx index c6f3620b5e..32cf9c113b 100644 --- a/cypress/component/TextInput.spec.tsx +++ b/cypress/component/TextInput.spec.tsx @@ -1,6 +1,4 @@ import {Basic} from '../../modules/react/text-input/stories/examples/Basic'; -import {Caution} from '../../modules/react/text-input/stories/examples/Caution'; -import {Error} from '../../modules/react/text-input/stories/examples/Error'; import {Disabled} from '../../modules/react/text-input/stories/examples/Disabled'; import {Placeholder} from '../../modules/react/text-input/stories/examples/Placeholder'; @@ -9,34 +7,32 @@ const getTextInput = () => { }; describe('TextInput', () => { - [Basic, Caution, Error].forEach(Example => { - context(`given the '${Example.name}' story is rendered`, () => { + context(`given the 'Basic' story is rendered`, () => { + beforeEach(() => { + cy.mount(); + }); + + it('should not have any axe errors', () => { + cy.checkA11y(); + }); + + context('when clicked', () => { beforeEach(() => { - cy.mount(); + getTextInput().click(); }); - it('should not have any axe errors', () => { - cy.checkA11y(); + it('should be focused', () => { + getTextInput().should('be.focused'); }); + }); - context('when clicked', () => { - beforeEach(() => { - getTextInput().click(); - }); - - it('should be focused', () => { - getTextInput().should('be.focused'); - }); + context('when text is entered', () => { + beforeEach(() => { + getTextInput().clear().type('Test'); }); - context('when text is entered', () => { - beforeEach(() => { - getTextInput().clear().type('Test'); - }); - - it('should reflect the text typed', () => { - getTextInput().should('have.value', 'Test'); - }); + it('should reflect the text typed', () => { + getTextInput().should('have.value', 'Test'); }); }); }); diff --git a/modules/docs/mdx/accessibility/AriaLiveRegions.mdx b/modules/docs/mdx/accessibility/AriaLiveRegions.mdx index 4c1e617e4b..ccde41050f 100644 --- a/modules/docs/mdx/accessibility/AriaLiveRegions.mdx +++ b/modules/docs/mdx/accessibility/AriaLiveRegions.mdx @@ -3,7 +3,7 @@ import {AriaLiveRegion} from '@workday/canvas-kit-react/common'; import {FilterListWithLiveStatus} from './examples/AriaLiveRegions/FilterListWithLiveStatus'; import {VisibleLiveRegion} from './examples/AriaLiveRegions/VisibleLiveRegion'; import {HiddenLiveRegion} from './examples/AriaLiveRegions/HiddenLiveRegion'; -import {TextInputWithLiveError} from './examples/AriaLiveRegions/TextInputWithLiveError'; +import {CommentBoxWithCharLimit} from './examples/AriaLiveRegions/CommentBoxWithCharLimit'; @@ -61,21 +61,20 @@ describe how many items in the list are shown. -## Text input with live inline error +## Debouncing an `AriaLiveRegion`: `TextArea` with character limit -In this example, a live region is applied to the inline error message that will appear below the -text input. Listen for the screen reader to automatically describe the error message as you leave -the input field blank. +Using a live region to announce the character count of a text area can help screen reader users +track their progress. However, announcing on every keystroke would be extremely disruptive—imagine +hearing "5 of 200 characters... 6 of 200 characters... 7 of 200 characters" for each letter typed! +In this example, we've implemented debouncing to wait 2 seconds after the user stops typing before +announcing the count. -**Note:** Use this example with discretion. Using live regions for automatically announcing form -errors to screen reader users can be a nice experience for simple forms with a very limited number -of error conditions. As forms increase in complexity, live regions on each error message can become -increasingly distracting and disruptive to the experience, especially if users are trying to first -understand the information that is required of them to complete the task. +**Note:** Turn on a screen reader for this experience. -**Note:** The `` component is used inside of the `Hint` to ensure the live region -remains in the browser DOM at all times. The `Hint` is only rendered in the DOM when it contains -content, so it will not work reliably as a live region for screen readers using the -`as={AriaLiveRegion}` prop. +- Used the `as={AccessibleHide}` prop to hide the live region from view with CSS +- The live region will only update when a 2 second timer expires after the last keystroke +- If users have reached the maximum number of characters, the live region will update immediately to + inform users that they have reached the limit +- The live region will be cleared on blur events when users leave the field - + diff --git a/modules/docs/mdx/accessibility/AccessibilityTesting.mdx b/modules/docs/mdx/accessibility/TestingTableWithFormFields.mdx similarity index 100% rename from modules/docs/mdx/accessibility/AccessibilityTesting.mdx rename to modules/docs/mdx/accessibility/TestingTableWithFormFields.mdx diff --git a/modules/docs/mdx/accessibility/examples/AriaLiveRegions/CommentBoxWithCharLimit.tsx b/modules/docs/mdx/accessibility/examples/AriaLiveRegions/CommentBoxWithCharLimit.tsx new file mode 100644 index 0000000000..65efd12ba0 --- /dev/null +++ b/modules/docs/mdx/accessibility/examples/AriaLiveRegions/CommentBoxWithCharLimit.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import {FormField} from '@workday/canvas-kit-react/form-field'; +import {TextArea} from '@workday/canvas-kit-react/text-area'; +import {AriaLiveRegion, AccessibleHide} from '@workday/canvas-kit-react/common'; + +const MAX_CHARACTERS = 200; +const DEBOUNCE_DELAY = 2000; // 2 seconds after user stops typing + +export const CommentBoxWithCharLimit = () => { + const [value, setValue] = React.useState(''); + const [liveUpdateStr, setLiveUpdateStr] = React.useState(''); + const hintTextStr = `${value.length} of ${MAX_CHARACTERS} characters`; + + const handleChange = (event: React.ChangeEvent) => { + setValue(event.target.value); + }; + + const handleBlur = () => { + setLiveUpdateStr(''); + }; + + React.useEffect(() => { + // Immediately announce when limit is reached (bypass debounce) + if (value.length === MAX_CHARACTERS) { + setLiveUpdateStr(`Character limit reached. ${value.length} of ${MAX_CHARACTERS} characters`); + return; + } + + // Otherwise, debounce the updates + const timer = setTimeout(() => { + setLiveUpdateStr(`${value.length} of ${MAX_CHARACTERS} characters`); + }, DEBOUNCE_DELAY); + return () => clearTimeout(timer); + }, [value.length]); + + return ( + + Comments + + + {hintTextStr} + {liveUpdateStr} + + + ); +}; diff --git a/modules/react/form-field/stories/FormField.mdx b/modules/react/form-field/stories/FormField.mdx index 2bac266a32..e696998933 100644 --- a/modules/react/form-field/stories/FormField.mdx +++ b/modules/react/form-field/stories/FormField.mdx @@ -1,22 +1,22 @@ import {ExampleCodeBlock, Specifications, SymbolDoc} from '@workday/canvas-kit-docs'; import * as FormFieldStories from './FormField.stories'; -import { Basic } from './examples/Basic'; -import { Caution } from './examples/Caution'; -import { Error } from './examples/Error'; -import { Disabled } from './examples/Disabled'; -import { HiddenLabel } from './examples/HiddenLabel'; -import { LabelPositionHorizontalStart } from './examples/LabelPositionHorizontalStart'; -import { LabelPositionHorizontalEnd } from './examples/LabelPositionHorizontalEnd'; -import { RefForwarding } from './examples/RefForwarding'; -import { Required } from './examples/Required'; -import { Custom } from './examples/Custom'; -import { CustomId } from './examples/CustomId'; -import { AllFields } from './examples/AllFields'; -import { Hint } from './examples/Hint'; -import { Grow } from './examples/Grow'; -import { ThemedError } from './examples/ThemedErrors'; -import { GroupedInputs } from './examples/GroupedInputs'; +import {Basic} from './examples/Basic'; +import {Caution} from './examples/Caution'; +import {Error} from './examples/Error'; +import {Disabled} from './examples/Disabled'; +import {HiddenLabel} from './examples/HiddenLabel'; +import {LabelPositionHorizontalStart} from './examples/LabelPositionHorizontalStart'; +import {LabelPositionHorizontalEnd} from './examples/LabelPositionHorizontalEnd'; +import {RefForwarding} from './examples/RefForwarding'; +import {Required} from './examples/Required'; +import {Custom} from './examples/Custom'; +import {CustomId} from './examples/CustomId'; +import {AllFields} from './examples/AllFields'; +import {Hint} from './examples/Hint'; +import {Grow} from './examples/Grow'; +import {ThemedError} from './examples/ThemedErrors'; +import {GroupedInputs} from './examples/GroupedInputs'; @@ -31,43 +31,6 @@ by passing in `TextInput`, `Select`, `RadioGroup` and other form elements to `Fo yarn add @workday/canvas-kit-react ``` -## Accessibility - -The `FormField` adds a `for` attribute to the `FormField.Label` (`