diff --git a/modules/react/_examples/stories/AriaLiveRegions.stories.mdx b/modules/react/_examples/stories/AriaLiveRegions.stories.mdx new file mode 100644 index 0000000000..446b9881f4 --- /dev/null +++ b/modules/react/_examples/stories/AriaLiveRegions.stories.mdx @@ -0,0 +1,76 @@ +import {AriaLiveRegion} from '@workday/canvas-kit-react/common'; +import {FilterListWithLiveStatus} from './examples/common/FilterListWithLiveStatus'; +import {VisibleLiveRegion} from './examples/common/VisibleLiveRegion'; +import {HiddenLiveRegion} from './examples/common/HiddenLiveRegion'; +import {TextInputWithLiveError} from './examples/common/TextInputWithLiveError'; +import {IconButtonsWithLiveBadges} from './examples/common/IconButtonsWithLiveBadges'; + + + +# ARIA Live Regions + +These examples are provided to demonstrate a variety of different use cases for the `AriaLiveRegion` +component. For the full experience, get started by first turning on your favorite screen reading +software. On Windows, we recommend the open source +[NVDA (Non-Visual Desktop Access)](https://www.nvaccess.org/download/) software, or +[JAWS (Job Access With Speech)](https://support.freedomscientific.com/Downloads/JAWS) if you have +purchased a license. MacOS and iOS include VoiceOver, which can be turned on in your settings. + +Live regions work by designating specific DOM nodes for screen readers to monitor for any content +updates inside the node. When an update occurs, screen readers will announce the update to users in +real time, based on a few rules: + +1. `polite` will “politely” wait for users to finish what they are doing before announcing an update +2. `assertive` will interrupt what users are doing (or reading) by immediately announcing an update + +### CAUTION: Don't get carried away + +Key things to understand about live regions: + +1. A live region update will only be announced once. Users are unable to repeat them or re-examine + them if the announcement was not understood. +2. Users may be able to pause a live region announcement, but they cannot prevent a live region + announcement from occurring. Sending frequent, repetitive, or simply too much information to a + live region can be very disruptive to users. +3. Users cannot act on, or navigate to, a live region. Live regions must only contain plain text. + (No images, links, buttons, or other input.) +4. Support for live regions is limited across platforms, browsers, and screen reader software. Real + time announcements may not be perfectly reliable. + +## Visible Live Regions + +Live regions can be applied to dynamic text on the UI. When the dynamic text is updated, screen +readers can describe the text update in live time as it occurs. In the example below, type text into +the input field and activate the "Send Message" button. Listen and observe the screen reader +automatically announce the text update. + + + +## Hidden Live Regions + +Live regions don't need to be visible UI text, they can be used to assist the non-visual listening +experience when moving the keyboard focus to a new element on screen isn't feasible. + + + +## Filtering lists with a live status + +In this example, a live region is applied to a short UI text describing the number of items shown in +the list. As you type characters into the input, listen for the screen reader to automatically +describe how many items in the list or shown. + + + +## Text input with live inline error + +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. + +**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. + + diff --git a/modules/react/_examples/stories/examples/common/FilterListWithLiveStatus.tsx b/modules/react/_examples/stories/examples/common/FilterListWithLiveStatus.tsx new file mode 100644 index 0000000000..2568bfdf48 --- /dev/null +++ b/modules/react/_examples/stories/examples/common/FilterListWithLiveStatus.tsx @@ -0,0 +1,65 @@ +import React, {useState} from 'react'; +import {TextInput} from '@workday/canvas-kit-preview-react/text-input'; +import {BodyText, Heading} from '@workday/canvas-kit-react/text'; +import {AriaLiveRegion} from '@workday/canvas-kit-react/common'; +import {Flex} from '@workday/canvas-kit-react/layout'; +import {system, base} from '@workday/canvas-tokens-web'; +import {createStyles, px2rem} from '@workday/canvas-kit-styling'; + +const fruits = [ + 'Apples', + 'Oranges', + 'Bananas', + 'Lemons', + 'Limes', + 'Strawberries', + 'Raspberries', + 'Blackberries', +]; + +const liveRegionStyle = createStyles({ + border: `${px2rem(1)} solid ${base.cantaloupe400}`, + backgroundColor: base.cantaloupe100, + padding: system.space.x2, +}); + +const listStyles = {paddingLeft: '0px'}; + +const listItemStyles = createStyles({ + listStyle: 'none', + paddingLeft: system.space.zero, +}); + +let filteredFruits = fruits; + +export const FilterListWithLiveStatus = () => { + const [filter, setFilter] = useState(''); + function handleFilter(e) { + filteredFruits = fruits.filter(i => i.toUpperCase().indexOf(e.target.value.toUpperCase()) >= 0); + setFilter(e.target.value); + } + + return ( + <> + + Fruits + + + {`Showing ${filteredFruits.length} of ${fruits.length}`} + + + + + Filter Items: + + +
    + {filteredFruits.map(i => ( + + {i} + + ))} +
+ + ); +}; diff --git a/modules/react/_examples/stories/examples/common/HiddenLiveRegion.tsx b/modules/react/_examples/stories/examples/common/HiddenLiveRegion.tsx new file mode 100644 index 0000000000..355a621546 --- /dev/null +++ b/modules/react/_examples/stories/examples/common/HiddenLiveRegion.tsx @@ -0,0 +1,30 @@ +import React, {useState, useRef} from 'react'; +import {AriaLiveRegion, AccessibleHide} from '@workday/canvas-kit-react/common'; +import {PrimaryButton} from '@workday/canvas-kit-react/button'; +import {TextInput} from '@workday/canvas-kit-preview-react/text-input'; +import {Flex} from '@workday/canvas-kit-react/layout'; +import {system} from '@workday/canvas-tokens-web'; + +export const HiddenLiveRegion = () => { + const [message, setMessage] = useState('This is an ARIA Live Region!'); + const inputRef = useRef(); + function handleSendMessage() { + setMessage(inputRef.current.value); + inputRef.current.value = ''; + } + + return ( + <> + + + Type your message: + + + Send Message + + + {message} + + + ); +}; diff --git a/modules/react/_examples/stories/examples/common/IconButtonsWithLiveBadges.tsx b/modules/react/_examples/stories/examples/common/IconButtonsWithLiveBadges.tsx new file mode 100644 index 0000000000..4f326d7660 --- /dev/null +++ b/modules/react/_examples/stories/examples/common/IconButtonsWithLiveBadges.tsx @@ -0,0 +1,98 @@ +import React, {useState} from 'react'; +import {AccessibleHide, AriaLiveRegion, useUniqueId} from '@workday/canvas-kit-react/common'; +import {notificationsIcon, inboxIcon, assistantIcon} from '@workday/canvas-system-icons-web'; +import {space} from '@workday/canvas-kit-react/tokens'; +import {SecondaryButton, TertiaryButton} from '@workday/canvas-kit-react/button'; +import {Flex} from '@workday/canvas-kit-react/layout'; +import {Tooltip} from '@workday/canvas-kit-react/tooltip'; +import {CountBadge} from '@workday/canvas-kit-react/badge'; + +const MyTasksLiveBadge = ({cnt}) => { + // use tooltip to assign name, + // use AriaLiveRegion inside button, + // assign name to live region referencing the button, + // use BadgeCount inside live region, + // use AccessibleHide to create invisible word "new" after badge + // use aria-describedby on button, referencing live region container to set description + // Safari + VO => not working at all + // JAWS 2024 + Chrome / Edge => works as expected :) + // NVDA + Chrome / Edge => works as expected :) + // Firefox => isn't announcing description on focus, only announces "X New" live (missing button name) + const badgeID = useUniqueId(); + const myTasksID = useUniqueId(); + + return ( + + + + + New + + + + ); +}; + +// use AriaLiveRegion around the button, +// use Tooltip to assign the name of the button, +// make sure Tooltip title string includes count value +// Chrome + VO => Announces name "notifications X new" and innerText 'X' +// Safari + VO => Works as expected :) +// JAWS 2024 => Announces full button name twice (previous state, then new state) +// JAWS 2024 + Firefox => Works as expected :) +// NVDA (All Browsers) => Atomic property isn't working, only announcing number change, announces twice +const NotificationsLiveBadge = ({cnt}) => ( + + + + + + + +); + +const AssistantLiveBadge = ({cnt}) => { + // use AriaLiveRegion around the button + // use muted type Tooltip (avoid using aria-label to name button) + // use AccessibleHide inside of button to compose name + // Chrome + VO => announces twice + // Safari + VO => works as expected :) + const lbl = 'Workday Assistant'; + + return ( + + + + {lbl} + + New + + + + ); +}; + +export const IconButtonsWithLiveBadges = () => { + const [counter, setCounter] = useState(0); + const [notifications, setNotifications] = useState(0); + const [assistant, setAssistant] = useState(0); + + const handleAddTask = () => setCounter(prev => prev + 1); + const handleAddNotification = () => setNotifications(prev => prev + 1); + const handleAssistant = () => setAssistant(prev => prev + 1); + + return ( + <> + + + + + + + Add a Message + Add a Notification + Add an item to My Tasks + + + ); +}; diff --git a/modules/react/_examples/stories/examples/common/TextInputWithLiveError.tsx b/modules/react/_examples/stories/examples/common/TextInputWithLiveError.tsx new file mode 100644 index 0000000000..fb50e44385 --- /dev/null +++ b/modules/react/_examples/stories/examples/common/TextInputWithLiveError.tsx @@ -0,0 +1,25 @@ +import React, {useState, useRef} from 'react'; +import {TextInput} from '@workday/canvas-kit-preview-react/text-input'; +import {AriaLiveRegion, changeFocus} from '@workday/canvas-kit-react/common'; +import {PrimaryButton} from '@workday/canvas-kit-react/button'; + +export const TextInputWithLiveError = () => { + const errMsg = 'Error: First name is required.'; + const [hasError, setHasError] = useState(false); + const inputRef = useRef(); + const handleBlur = e => setHasError(e.target.value.trim().length === 0); + const handleSubmit = () => hasError && changeFocus(inputRef.current); + + return ( + <> + + First Name: + + + {hasError && errMsg} + + + Continue + + ); +}; diff --git a/modules/react/_examples/stories/examples/common/VisibleLiveRegion.tsx b/modules/react/_examples/stories/examples/common/VisibleLiveRegion.tsx new file mode 100644 index 0000000000..0ec7bee9f5 --- /dev/null +++ b/modules/react/_examples/stories/examples/common/VisibleLiveRegion.tsx @@ -0,0 +1,40 @@ +import React, {useState, useRef} from 'react'; +import {AriaLiveRegion} from '@workday/canvas-kit-react/common'; +import {PrimaryButton} from '@workday/canvas-kit-react/button'; +import {TextInput} from '@workday/canvas-kit-preview-react/text-input'; +import {Flex} from '@workday/canvas-kit-react/layout'; +import {Text} from '@workday/canvas-kit-react/text'; +import {system, base} from '@workday/canvas-tokens-web'; +import {createStyles, px2rem} from '@workday/canvas-kit-styling'; + +const liveRegionStyle = createStyles({ + border: `${px2rem(1)} solid ${base.cantaloupe400}`, + backgroundColor: base.cantaloupe100, + padding: system.space.x4, + display: 'block', + margin: system.space.x4 + ' 0', +}); + +export const VisibleLiveRegion = () => { + const [message, setMessage] = useState('This is an ARIA Live Region!'); + const inputRef = useRef(); + function handleSendMessage() { + setMessage(inputRef.current.value); + inputRef.current.value = ''; + } + + return ( + <> + + {message} + + + + Type your message: + + + Send Message + + + ); +};