Skip to content

Commit

Permalink
docs: Add new examples of new AriaLiveRegion component (#2672)
Browse files Browse the repository at this point in the history
With the release of the new `AriaLiveRegion` component, I wanted to build a few simple examples to introduce to developers what an ARIA live region is, and how they can be used to positively impact the accessible experience with screen readers.

[category:Documentation]

Co-authored-by: @RayRedGoose <48605821+RayRedGoose@users.noreply.github.com>
  • Loading branch information
williamjstanton and RayRedGoose committed Apr 12, 2024
1 parent 743cbfd commit fc96f1e
Show file tree
Hide file tree
Showing 6 changed files with 334 additions and 0 deletions.
76 changes: 76 additions & 0 deletions 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';

<Meta title="Examples/AriaLiveRegion" component={AriaLiveRegion} />

# 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.

<ExampleCodeBlock code={VisibleLiveRegion} />

## 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.

<ExampleCodeBlock code={HiddenLiveRegion} />

## 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.

<ExampleCodeBlock code={FilterListWithLiveStatus} />

## 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.

<ExampleCodeBlock code={TextInputWithLiveError} />
@@ -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 (
<>
<Flex gap="1rem">
<Heading size="small">Fruits</Heading>
<AriaLiveRegion>
<BodyText size="small" cs={liveRegionStyle}>
{`Showing ${filteredFruits.length} of ${fruits.length}`}
</BodyText>
</AriaLiveRegion>
</Flex>
<TextInput orientation="vertical">
<TextInput.Label>Filter Items:</TextInput.Label>
<TextInput.Field value={filter} onChange={handleFilter} />
</TextInput>
<ul style={listStyles}>
{filteredFruits.map(i => (
<BodyText size="small" as="li" cs={listItemStyles} key={i}>
{i}
</BodyText>
))}
</ul>
</>
);
};
@@ -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 (
<>
<Flex gap={`var(${system.space.x4})`} alignItems="flex-end">
<TextInput orientation="vertical">
<TextInput.Label>Type your message:</TextInput.Label>
<TextInput.Field ref={inputRef} />
</TextInput>
<PrimaryButton onClick={handleSendMessage}>Send Message</PrimaryButton>
</Flex>
<AriaLiveRegion>
<AccessibleHide>{message}</AccessibleHide>
</AriaLiveRegion>
</>
);
};
@@ -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 (
<Tooltip title="My Tasks">
<TertiaryButton icon={inboxIcon} id={myTasksID} aria-describedby={badgeID}>
<AriaLiveRegion id={badgeID} aria-labelledby={myTasksID}>
<CountBadge count={cnt} />
<AccessibleHide>New</AccessibleHide>
</AriaLiveRegion>
</TertiaryButton>
</Tooltip>
);
};

// 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}) => (
<AriaLiveRegion>
<Tooltip title={`Notifications ${cnt} new`}>
<TertiaryButton icon={notificationsIcon}>
<CountBadge count={cnt} />
</TertiaryButton>
</Tooltip>
</AriaLiveRegion>
);

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 (
<AriaLiveRegion>
<Tooltip title={lbl} type="muted">
<TertiaryButton icon={assistantIcon}>
<AccessibleHide>{lbl}</AccessibleHide>
<CountBadge count={cnt} />
<AccessibleHide>New</AccessibleHide>
</TertiaryButton>
</Tooltip>
</AriaLiveRegion>
);
};

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 (
<>
<Flex padding={space.s} gap={space.s} as="header">
<AssistantLiveBadge cnt={assistant} />
<NotificationsLiveBadge cnt={notifications} />
<MyTasksLiveBadge cnt={counter} />
</Flex>
<Flex padding={space.s} gap={space.s} as="main">
<SecondaryButton onClick={handleAssistant}>Add a Message</SecondaryButton>
<SecondaryButton onClick={handleAddNotification}>Add a Notification</SecondaryButton>
<SecondaryButton onClick={handleAddTask}>Add an item to My Tasks</SecondaryButton>
</Flex>
</>
);
};
@@ -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 (
<>
<TextInput orientation="vertical" hasError={hasError} isRequired={true}>
<TextInput.Label>First Name:</TextInput.Label>
<TextInput.Field onBlur={handleBlur} ref={inputRef} />
<TextInput.Hint height={'16px'}>
<AriaLiveRegion>{hasError && errMsg}</AriaLiveRegion>
</TextInput.Hint>
</TextInput>
<PrimaryButton onClick={handleSubmit}>Continue</PrimaryButton>
</>
);
};
@@ -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 (
<>
<AriaLiveRegion>
<Text cs={liveRegionStyle}>{message}</Text>
</AriaLiveRegion>
<Flex gap={`var(${system.space.x4})`} alignItems="flex-end">
<TextInput orientation="vertical">
<TextInput.Label>Type your message:</TextInput.Label>
<TextInput.Field ref={inputRef} />
</TextInput>
<PrimaryButton onClick={handleSendMessage}>Send Message</PrimaryButton>
</Flex>
</>
);
};

0 comments on commit fc96f1e

Please sign in to comment.