Skip to content
50 changes: 48 additions & 2 deletions modules/react/expandable/stories/Expandable.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ to `Expandable.Icon` depending on whether the `Expandable.Icon` is placed before

<ExampleCodeBlock code={Avatar} />

> **Accessibility Note:** In this situation, the Avatar is decorative and should not be announced to
> screen readers. The `isDecorative` prop is passed to `<Expandable.Avatar>` to hide it from screen
> readers.

### Right to Left (RTL)

Expandable container has bidirectional support and should function as expected with RTL languages as
Expand All @@ -75,11 +79,53 @@ You can also have direct access to the model if

### Hoisted Model

If you you need direct access to the model, you can hoist it with the `useExpandableModel` hook. In
the example below, we're hoisting the models to expand and collapse all three containers at once.
If you need direct access to the model, you can hoist it with the `useExpandableModel` hook. In the
example below, we're hoisting the models to expand and collapse all three containers at once.

<ExampleCodeBlock code={HoistedModel} />

> **Accessibility Note:** When using multiple Expandable Containers on a page, use the `as` prop to
> render the `<Expandable.Content>` sub-component as an HTML `<section>` element. Then, use
> `aria-labelledby` to reference the unique `id` of the `<Expandable.Title>` element. This practice
> can be useful to screen reader users when multiple Expandable Containers are opened at one time
> for uniquely describing the boundaries of the expandable content.

## Accessibility

Our Expandable component renders a semantic HTML `<button>` element to the DOM, with an optional
parent heading element as defined by the `headingLevel` prop. The `aria-expanded` property is
included on the button to indicate the state of the content to screen readers.

[Accordion Pattern | APG | WAI | W3C](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/)

- Use the `headingLevel` prop to assign an appropriate heading level based on the context of the
page content.
- When using Expandable Container for navigation elements, then we don't recommend using the
`headingLevel` prop. This will render only expandable buttons to the DOM, reserving headings for
organizing content in the main body of the page.
- The `as` prop may also be used on `<Expandable.Content>` to render an HTML `<ul>` element for
displaying a list of items. For example, check out
[Side Panel with Navigation](?path=/docs/guides-accessibility-examples-side-panel-navigation--docs).

### Navigation

- **Tab key**: Moves focus to the next expandable button or focusable element
- **Shift + Tab**: Moves focus to the previous focusable element
- **Enter or Space**: Toggles the expanded/collapsed state

### Screen Reader Experience

- The expandable button will be announced with its title text followed by the button role
- The current state will be announced as either "collapsed" or "expanded" (For example: "Usage
Guidance, button, collapsed" or "Usage Guidance, button, expanded")
- **State Changes:** When activating the button to expand content, screen readers will announce the
new "expanded" state and vice versa when collapsing content.
- **Content Regions:** Screen reader users can use landmark navigation to jump between sections and
each section will be announced with its associated title (For example: "Usage Guidance, landmark
region")
- **Heading Structure:** Using heading levels with expandable buttons allows screen reader users to
navigate by headings, making the document structure and hierarchy easier to understand.

## Component API

<SymbolDoc name="Expandable" fileName="/react/" />
4 changes: 2 additions & 2 deletions modules/react/expandable/stories/examples/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ export const Avatar = () => (
<Expandable>
<Expandable.Target headingLevel="h4">
<Expandable.Icon iconPosition="start" />
<Expandable.Avatar name="Avatar" url={testAvatar} />
<Expandable.Avatar name="Avatar" isDecorative url={testAvatar} />
<Expandable.Title>Title</Expandable.Title>
</Expandable.Target>

<Expandable.Content>Content</Expandable.Content>
</Expandable>
<Expandable>
<Expandable.Target headingLevel="h4">
<Expandable.Avatar name="Avatar" url={testAvatar} />
<Expandable.Avatar name="Avatar" isDecorative url={testAvatar} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to always define isDecorative on the expandable.avatar?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK! I did it!

<Expandable.Title>Title</Expandable.Title>
<Expandable.Icon iconPosition="end" />
</Expandable.Target>
Expand Down
41 changes: 24 additions & 17 deletions modules/react/expandable/stories/examples/HoistedModel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,27 @@ import React from 'react';
import {Expandable, useExpandableModel} from '@workday/canvas-kit-react/expandable';
import {Flex} from '@workday/canvas-kit-react/layout';
import {SecondaryButton} from '@workday/canvas-kit-react/button';
import {useUniqueId} from '@workday/canvas-kit-react/common';
import {createStyles} from '@workday/canvas-kit-styling';
import {system} from '@workday/canvas-tokens-web';

const listStyles = createStyles({
flexDirection: 'column',
gap: system.space.x2,
padding: system.space.zero,
marginX: system.space.x4,
marginY: system.space.zero,
});

export const HoistedModel = () => {
const modelOne = useExpandableModel();
const modelTwo = useExpandableModel();
const modelThree = useExpandableModel();

const idOne = useUniqueId();
const idTwo = useUniqueId();
const idThree = useUniqueId();

const handleExpandAll = () => {
modelOne.events.show();
modelTwo.events.show();
Expand All @@ -22,19 +37,19 @@ export const HoistedModel = () => {
};

return (
<Flex gap="m" flexDirection="column">
<Flex gap="s">
<Flex gap={system.space.x6} flexDirection="column">
<Flex gap={system.space.x4}>
<SecondaryButton onClick={handleExpandAll}>Expand All</SecondaryButton>
<SecondaryButton onClick={handleCollapseAll}>Collapse All</SecondaryButton>
</Flex>
<Flex flexDirection="column">
<Expandable model={modelOne}>
<Expandable.Target headingLevel="h4">
<Expandable.Title>Usage Guidance</Expandable.Title>
<Expandable.Title id={idOne}>Usage Guidance</Expandable.Title>
<Expandable.Icon iconPosition="end" />
</Expandable.Target>

<Expandable.Content>
<Expandable.Content as="section" aria-labelledby={idOne}>
This component highlights the most important details of a section and reveals more when
a user taps or clicks on the header part of the container. Enabling users to hide and
show information ensures the design remains focused and relevant to their expectations.
Expand All @@ -44,20 +59,12 @@ export const HoistedModel = () => {
</Expandable>
<Expandable model={modelTwo}>
<Expandable.Target headingLevel="h4">
<Expandable.Title>Accessibility Guidelines</Expandable.Title>
<Expandable.Title id={idTwo}>Accessibility Guidelines</Expandable.Title>
<Expandable.Icon iconPosition="end" />
</Expandable.Target>

<Expandable.Content>
<Flex
flexDirection="column"
as="ul"
gap="xxs"
maxWidth="60ch"
padding="zero"
marginX="s"
marginY="zero"
>
<Expandable.Content as="section" aria-labelledby={idTwo}>
<Flex as="ul" cs={listStyles}>
<li>
The state of a component being open or closed must be conveyed to assistive
technologies.
Expand Down Expand Up @@ -92,10 +99,10 @@ export const HoistedModel = () => {
</Expandable>
<Expandable model={modelThree}>
<Expandable.Target headingLevel="h4">
<Expandable.Title>Content Guidelines</Expandable.Title>
<Expandable.Title id={idThree}>Content Guidelines</Expandable.Title>
<Expandable.Icon iconPosition="end" />
</Expandable.Target>
<Expandable.Content>
<Expandable.Content as="section" aria-labelledby={idThree}>
Titles should be short and concise, yet long enough to explain what the user would
expect to see when the content is expanded. If titles must be long, make sure it doesn't
wrap more than two lines.
Expand Down
2 changes: 1 addition & 1 deletion modules/react/expandable/stories/examples/LongTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const LongTitle = () => (
<Expandable>
<Expandable.Target headingLevel="h4">
<Expandable.Icon iconPosition="start" />
<Expandable.Avatar name="Avatar" url={testAvatar} />
<Expandable.Avatar name="Avatar" isDecorative url={testAvatar} />
<Expandable.Title>
Our house special supreme pizza includes pepperoni, sausage, bell peppers, mushrooms,
onions, and oregano.
Expand Down
4 changes: 2 additions & 2 deletions modules/react/expandable/stories/examples/RTL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ export const RTL = () => {
<Expandable>
<Expandable.Target headingLevel="h4">
<Expandable.Icon iconPosition="start" />
<Expandable.Avatar name="Avatar" />
<Expandable.Avatar name="Avatar" isDecorative />
<Expandable.Title>Title</Expandable.Title>
</Expandable.Target>

<Expandable.Content>Content</Expandable.Content>
</Expandable>
<Expandable>
<Expandable.Target headingLevel="h4">
<Expandable.Avatar name="Avatar" />
<Expandable.Avatar name="Avatar" isDecorative />
<Expandable.Title>Title</Expandable.Title>
<Expandable.Icon iconPosition="end" />
</Expandable.Target>
Expand Down
89 changes: 63 additions & 26 deletions modules/react/menu/stories/Menu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,18 @@ yarn add @workday/canvas-kit-react
`Menu` will automatically focus on the cursor item (first item by default). The `Menu` uses a menu
model which composes a list model and a popup model and sets up accessibility features for you.

`Menu` follows the
[Actions Menu pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-actions/)
using roving tabindex. Below is table of supported keyboard shortcuts and associated actions.

| Key | Action |
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
| `Enter` or `Space` | Activates the menu item and then closes the menu |
| `Escape` | Closes the menu |
| `Up Arrow` | Moves focus to the previous menu item – if focus is on first menu item, it moves focus to the last menu item |
| `Down Arrow` | Moves focus to the next menu item – if focus is on last menu item, it moves focus to the first menu item |
| `Home` | Moves focus to the first menu item |
| `End` | Moves focus to the last menu item |

### Context Menu

<ExampleCodeBlock code={ContextMenu} />

> **Accessibility Note**: This variation relies on the `contextmenu` browser event, which has
> varying levels of support across different operating systems. On Windows, this feature is better
> supported and users can typically trigger context menus using the **Shift + F10** keyboard
> shortcut or the dedicated **Context Menu** key (if available on their keyboard). However, on
> macOS, context menu support is limited and may require users to enable specific accessibility
> settings in their system preferences to function properly. Consider providing alternative access
> methods for critical functionality.

### Icons

Menu supports more complex children, including icons, but the text of the item will no longer be
Expand All @@ -63,18 +58,10 @@ about the text of the item.

<ExampleCodeBlock code={Icons} />

### Selectable Menu

The `Menu.Item` renders a `role=menuitem` element which does not have `aria-selected` and thus no
selected state. If you wish your menu to have selectable menu items, use `Menu.Option` instead of
`Menu.Item`. The `Menu.Option` is meant to be used when the `Menu.Card` has a role of `listbox` and
is controlled via `aria-activedescendant`. This example uses `Menu.Option` to should what the
checkmark looks like, but the example is not keyboard or screen reader accessible. There are many
other behaviors that need to be composed into the `Menu.Target` and `Menu.List` components and the
behaviors depend on many other things. To see a full example of all these behaviors, look at the
`<Combobox>` and `<Select>` component source code as examples.

<ExampleCodeBlock code={SelectableMenu} />
> **Accessibility Note**: Icons in menu items do not inherently provide text alternatives to
> assistive technologies. However, in most cases, icons are used decoratively alongside text labels,
> and additional text alternatives are not necessary since the menu item text itself provides the
> accessible name.

### Grouping

Expand All @@ -87,6 +74,14 @@ and are not selectable with the keyboard or mouse.

<ExampleCodeBlock code={Grouping} />

> **Accessibility Note**: Menu groups use `role="group"` with appropriate labeling to provide
> semantic structure for assistive technologies. When navigating through grouped menu items, screen
> readers will announce the group label when users enter a new group, providing important context
> about the organization of the menu. Group headers are not part of the keyboard navigation
> sequence, allowing users to efficiently move between actionable menu items. This semantic grouping
> helps all users, including those using assistive technologies, understand the hierarchy and
> categorization of menu options.

### Nested

Menus support nesting. If you only have a few items and not very many nesting levels, the menu can
Expand All @@ -97,6 +92,10 @@ indicate this dual role.

<ExampleCodeBlock code={Nested} />

> **Accessibility Note**: When a menu item has an attached submenu, the `<Menu.Submenu.TargetItem>`
> includes `aria-haspopup="true"` and `aria-expanded={true | false}` properties. These properties
> will alert screen reader users to the available submenu systems.

### Nested Dynamic Items

Menu nesting is simpler with the dynamic API. In this example, a `renderItem` function is defined to
Expand All @@ -109,7 +108,45 @@ submenus.

## Accessibility

This content is coming soon!
Our Menu component is based on the Menu Button pattern on the ARIA Authoring Practices Guide from
the W3C and relies on the roving tabindex technique for managing focus within the opened menu. This
means that the minimum requirements for screen reader support and keyboard navigation are included
in the component.

[Menu Button Pattern | APG | WAI | W3C](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/)

- The `<Menu.Target>` sub-component uses `aria-haspopup="true"` and `aria-expanded={true | false}`
properties. This benefits screen reader users by indicating when a button element has an attached
menu.
- The `<Menu.List>` sub-component uses `role="menu"` and `<Menu.Item>` uses `role="menuitem"` ARIA
roles. These roles allow screen readers to pass through arrow key events to the web application.
- The `<Menu.List>` sub-component includes an `aria-labelledby` ID reference to the `<Menu.Target>`
sub-component. This assigns a label to the menu for context.

### Navigation

- **Enter or Space**: When focused on the menu button, opens the menu and moves focus to the first
menu item. When focused on a menu item, activates the item and closes the menu
- **Escape**: Closes the menu and returns focus to the menu button
- **Up & Down Arrow**: Moves focus up and down the menu items
- **Home & End**: Moves focus to the first or last menu item
- **Right & Left Arrow**: When focused on a menu item with a submenu, opens the submenu and moves
focus to the first item in the submenu or closes the submenu and returns focus to the parent menu
item

### Screen Reader Experience

- The menu button will be announced with its label text followed by the button role, a notification
that it has a popup menu, and the current state of the menu (For example: "Actions, button, menu
popup, collapsed")
- **Opening the Menu:** When the menu button is activated, screen readers will announce the menu
opening, the number of menu items available, and the currently focused item (For example:
"Actions, menu, First Action, menu item, 1 of 4.")
- **Navigating Menu Items:** As focus moves between menu items, screen readers will announce the
item name and its position in the list (For example: "Second Action, menu item, 2 of 4.")
- **Menu Items with Submenus:** When focused on a menu item that has a submenu, screen readers will
announce that it has a submenu and provide the expanded/collapsed state (For example: "More
Actions, menu item, has submenu, collapsed, 3 of 4.")

## Component API

Expand Down
15 changes: 13 additions & 2 deletions modules/react/menu/stories/examples/SelectableMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@ import React from 'react';
import {Menu} from '@workday/canvas-kit-react/menu';
import {BodyText} from '@workday/canvas-kit-react/text';

/**
* The `Menu.Item` renders a `role=menuitem` element, and `aria-selected` is not a valid attribute for
* the `menuitem` role, so it cannot have a selected state. If you wish your menu to have selectable
* menu items, use `Menu.Option` instead of `Menu.Item`. This is meant to be used when overriding the
* default role of the `Menu.List` to `listbox`. This example uses `Menu.Option` to show what the
* checkmark looks like, but the example is not keyboard or screen reader accessible. There are many
* other behaviors that need to be composed into the `Menu.Target` and `Menu.List` components and the
* behaviors depend on many other things. To see a full example of all these behaviors, look at the
* `<Combobox>` and `<Select>` component source code as examples.
*/

export const SelectableMenu = () => {
const [selected, setSelected] = React.useState('');
return (
<Menu onSelect={data => setSelected(data.id)}>
<Menu.Target>Open Menu</Menu.Target>
<Menu.Popper>
<Menu.Card role="listbox">
<Menu.List as="ul">
<Menu.Card>
<Menu.List role="listbox">
<Menu.Option>First Item</Menu.Option>
<Menu.Option>Second Item</Menu.Option>
<Menu.Divider />
Expand Down
Loading