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` (`