diff --git a/.storybook/preview.js b/.storybook/preview.js index 3ebfdddd45..7c708e772a 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -62,4 +62,8 @@ export const parameters = { defaultValue: defaultCanvasTheme, }, }, + // Make stories available to our internal code sandbox tool + tesseract: { + enable: true + } }; diff --git a/modules/preview-react/menu/lib/Menu.tsx b/modules/preview-react/menu/lib/Menu.tsx index 105d85de75..472e20263f 100644 --- a/modules/preview-react/menu/lib/Menu.tsx +++ b/modules/preview-react/menu/lib/Menu.tsx @@ -119,6 +119,7 @@ export default class Menu extends React.Component { } = this.props; const {selectedItemIndex} = this.state; const cardWidth = grow ? '100%' : width; + let interactiveItemIndex: number | null = null; return ( @@ -133,17 +134,22 @@ export default class Menu extends React.Component { ref={this.menuRef} {...elemProps} > - {React.Children.map(children, (menuItem, index) => { + {React.Children.map(children, menuItem => { if (!React.isValidElement(menuItem)) { return; } - const itemId = `${id}-${index}`; + let itemId = null; + if (!menuItem.props.isHeader) { + interactiveItemIndex = (interactiveItemIndex ?? -1) + 1; + itemId = `${id}-${interactiveItemIndex}`; + } return ( {React.cloneElement(menuItem, { onClick: (event: React.MouseEvent) => this.handleClick(event, menuItem.props), id: itemId, - isFocused: selectedItemIndex === index, + isFocused: + selectedItemIndex === interactiveItemIndex && !menuItem.props.isHeader, })} ); @@ -175,7 +181,9 @@ export default class Menu extends React.Component { const children = React.Children.toArray(this.props.children); let nextSelectedIndex = 0; let isShortcut = false; - const itemCount = children.length; + const itemCount = children.filter(child => { + return !(child as React.ReactElement)?.props?.isHeader; + }).length; const firstItem = 0; const lastItem = itemCount - 1; @@ -308,6 +316,9 @@ export default class Menu extends React.Component { }; const firstCharacters = React.Children.map(this.props.children, child => { + if ((child as React.ReactElement)?.props?.isHeader) { + return; + } return getFirstCharacter(child); }); diff --git a/modules/preview-react/menu/lib/MenuItem.tsx b/modules/preview-react/menu/lib/MenuItem.tsx index 621e499909..0adec1a502 100644 --- a/modules/preview-react/menu/lib/MenuItem.tsx +++ b/modules/preview-react/menu/lib/MenuItem.tsx @@ -33,6 +33,11 @@ export interface MenuItemProps extends React.LiHTMLAttributes { * @default false */ hasDivider?: boolean; + /** + * If true, render a header to group data, this menu item will not be intractable. + * @default false + */ + isHeader?: boolean; /** * If true, set the MenuItem to the disabled state so it is not clickable. * @default false @@ -55,7 +60,7 @@ export interface MenuItemProps extends React.LiHTMLAttributes { shouldClose?: boolean; } -const Item = styled('li')>( +const Item = styled('li')>( { ...type.levels.subtext.large, padding: `${space.xxs} ${space.s}`, @@ -71,6 +76,9 @@ const Item = styled('li')>( outline: 'none', }, }, + ({isHeader}) => { + return {pointerEvents: isHeader ? 'none' : 'all'}; + }, ({isFocused, isDisabled}) => { if (!isFocused && !isDisabled) { return { @@ -258,6 +266,7 @@ class MenuItem extends React.Component { hasDivider, isDisabled, isFocused, + isHeader, role, ...elemProps } = this.props; @@ -272,11 +281,12 @@ class MenuItem extends React.Component { ref={this.ref} tabIndex={-1} id={id} - role={role} + role={isHeader ? 'presentation' : role} onClick={this.handleClick} aria-disabled={isDisabled ? true : undefined} isDisabled={!!isDisabled} isFocused={!!isFocused} + isHeader={!!isHeader} {...elemProps} > {icon && iconProps && } diff --git a/modules/preview-react/menu/spec/Menu.spec.tsx b/modules/preview-react/menu/spec/Menu.spec.tsx index 2457783a2e..7c2c30d0ce 100644 --- a/modules/preview-react/menu/spec/Menu.spec.tsx +++ b/modules/preview-react/menu/spec/Menu.spec.tsx @@ -183,6 +183,7 @@ describe('Menu Keyboard Shortcuts', () => { render( Alpha + Bravo Header Bravo Item (with markup) @@ -211,8 +212,10 @@ describe('Menu Keyboard Shortcuts', () => { it('should loop around selected items using the down arrow', () => { render( + Beginning Alpha Bravo + End ); @@ -228,8 +231,10 @@ describe('Menu Keyboard Shortcuts', () => { it('should loop around selected items using the up arrow', () => { render( + Beginning Alpha Bravo + End ); @@ -245,8 +250,10 @@ describe('Menu Keyboard Shortcuts', () => { it('should select the first items when Home key is pressed', () => { render( + Beginning Alpha Bravo + End ); @@ -260,8 +267,10 @@ describe('Menu Keyboard Shortcuts', () => { it('should select the last items when End key is pressed', () => { render( + Beginning Alpha Bravo + End ); @@ -280,7 +289,6 @@ describe('Menu Keyboard Shortcuts', () => { ); - const firstId = screen.getByRole('menuitem', {name: 'Alpha'}).getAttribute('id'); const secondId = screen.getByRole('menuitem', {name: 'Bravo'}).getAttribute('id'); fireEvent.keyDown(screen.getByRole('menu'), {key: 'Meta'}); @@ -391,4 +399,19 @@ describe('Menu Initial Selected Item', () => { expect(screen.getByRole('menu')).toHaveAttribute('aria-activedescendant', firstId); }); + + it('should select correct when headers are used', () => { + render( + + Alpha Header + Alpha + Bravo Header + Bravo + + ); + + const secondId = screen.getByRole('menuitem', {name: 'Bravo'}).getAttribute('id'); + + expect(screen.getByRole('menu')).toHaveAttribute('aria-activedescendant', secondId); + }); }); diff --git a/modules/preview-react/menu/stories/Menu.stories.mdx b/modules/preview-react/menu/stories/Menu.stories.mdx index 983a67814d..98100e35d1 100644 --- a/modules/preview-react/menu/stories/Menu.stories.mdx +++ b/modules/preview-react/menu/stories/Menu.stories.mdx @@ -6,6 +6,7 @@ import {Basic} from './examples/Basic'; import {ContextMenu} from './examples/ContextMenu'; import {CustomMenuItem} from './examples/CustomMenuItem'; import {Icons} from './examples/Icons'; +import {Headers} from './examples/Headers'; import {ManyItems} from './examples/ManyItems'; @@ -74,6 +75,13 @@ Below is an example: +### Headers + +You can group menu items logically by adding a `isHeader` attribute on your ``. To make +your new items screen reader friendly add an `aria-label` around each grouped item. + + + ### Many Items diff --git a/modules/preview-react/menu/stories/examples/Headers.tsx b/modules/preview-react/menu/stories/examples/Headers.tsx new file mode 100644 index 0000000000..001650b0fe --- /dev/null +++ b/modules/preview-react/menu/stories/examples/Headers.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import {checkIcon} from '@workday/canvas-system-icons-web'; +import {Menu, MenuItem} from '@workday/canvas-kit-preview-react/menu'; +import {styled} from '@workday/canvas-kit-react/common'; +import {type} from '@workday/canvas-kit-react/tokens'; + +const Header = styled(MenuItem)({ + fontWeight: type.properties.fontWeights.bold, +}); + +export const Headers = () => { + return ( + +
Sort By
+ + Newest + + + Oldest + +
+ Display Density +
+ + Simple + + + Detailed + +
+ ); +};