Skip to content
This repository has been archived by the owner on May 24, 2024. It is now read-only.

[terra-navigation-side-menu] Accessibility Changes #2018

Merged
merged 15 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/terra-framework-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

* Changed
* Updated `terra-navigation-side-menu` example to use more meaningful labels.
* Updated the `terra-date-time-picker` example for field label.

## 1.64.0 - (February 7, 2024)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,22 @@ class NavigationSideMenuDefault extends React.Component {
<NavigationSideMenu
id="test-menu"
menuItems={[
{ key: 'menu', text: 'Menu', childKeys: ['submenu1', 'submenu2', 'submenu3', 'submenu4'] },
{ key: 'menu', text: 'Hospital Details', childKeys: ['submenu1', 'submenu2', 'submenu3', 'submenu4'] },
{
key: 'submenu1', text: 'Sub Menu 1', childKeys: ['subsubmenu1', 'subsubmenu2', 'subsubmenu3'], id: 'test-item-1',
key: 'submenu1', text: 'Hospital services', childKeys: ['subsubmenu1', 'subsubmenu2', 'subsubmenu3'], id: 'test-item-1',
},
{ key: 'submenu2', text: 'Sub Menu 2' },
{ key: 'submenu3', text: 'Sub Menu 3' },
{ key: 'submenu4', text: 'Sub Menu 4' },
{ key: 'subsubmenu1', text: 'Sub-Sub Menu 1', id: 'test-item-2' },
{ key: 'subsubmenu2', text: 'Sub-Sub Menu 2' },
{ key: 'subsubmenu3', text: 'Sub-Sub Menu 3' },
{ key: 'submenu2', text: 'Hospital events' },
{ key: 'submenu3', text: 'Hospital Accommodations' },
{ key: 'submenu4', text: 'Hospital Careers' },
{ key: 'subsubmenu1', text: 'Imaging', id: 'test-item-2' },
{ key: 'subsubmenu2', text: 'Laboratory' },
{ key: 'subsubmenu3', text: 'Rehabilitation services' },
]}
onChange={this.handleOnChange}
routingStackBack={this.fakeRoutingBack}
selectedMenuKey={this.state.selectedMenuKey}
selectedChildKey={this.state.selectedChildKey}
ariaLabel="Sub Menu List"
/>
);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/terra-navigation-side-menu/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

* Added
* Added dashed focus border for navigation menu items.
* Keyboard navigation with arrow keys

## 2.49.0 - (December 18, 2023)

* Changed
Expand Down
19 changes: 10 additions & 9 deletions packages/terra-navigation-side-menu/src/MenuItem.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@
color: var(--terra-navigation-side-menu-item-hover-chevron-color, #bcbfc0);
}
}

&.is-focused {
background-color: var(--terra-navigation-side-menu-item-focus-background-color);
box-shadow: var(--terra-navigation-side-menu-item-focus-box-shadow, inset 0 0 0 1px #26a2e5, inset 0 0 0 4px rgba(76, 178, 233, 0.2));
z-index: 5;
&:focus {
box-shadow: var(--terra-navigation-side-menu-item-focus-box-shadow, none);
outline: var(--terra-navigation-side-menu-focus-outline, 2px dashed #000);
outline-offset: var(--terra-navigation-side-menu-focus-outline-offset, -2px);

.chevron {
color: var(--terra-navigation-side-menu-item-focus-chevron-color, #bcbfc0);
Expand Down Expand Up @@ -121,10 +121,11 @@
}
}

&.is-focused {
background-color: var(--terra-navigation-side-menu-item-selected-focus-background-color, #f4f4f4);
color: var(--terra-navigation-side-menu-item-selected-focus-color);

&:focus {
box-shadow: var(--terra-navigation-side-menu-item-focus-box-shadow, none);
outline: var(--terra-navigation-side-menu-focus-outline, 2px dashed #000);
outline-offset: var(--terra-navigation-side-menu-focus-outline-offset, -2px);

/* stylelint-disable-next-line max-nesting-depth */
.chevron {
color: var(--terra-navigation-side-menu-item-selected-focus-chevron-color, #909496);
Expand Down
25 changes: 23 additions & 2 deletions packages/terra-navigation-side-menu/src/NavigationSideMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const propTypes = {
* Internationalization object with translation APIs. Provided by `injectIntl`.
*/
intl: PropTypes.shape({ formatMessage: PropTypes.func }).isRequired,
/**
* String that labels the navigation menu for screen readers.
*/
ariaLabel: PropTypes.string,
/**
* An array of configuration for each menu item.
*/
Expand Down Expand Up @@ -147,6 +151,8 @@ class NavigationSideMenu extends Component {
}

if (selectedItem.childKeys && selectedItem.childKeys.length) {
// Add focus on the first item in sub menu
this.needsFocus = true;
this.props.onChange(
event,
{
Expand All @@ -156,6 +162,7 @@ class NavigationSideMenu extends Component {
},
);
} else {
this.needsFocus = false;
this.props.onChange(
event,
{
Expand All @@ -167,6 +174,19 @@ class NavigationSideMenu extends Component {
}
}

handleMenuListRef = (node) => {
this.menuContainer = node;
// To add focus to the first sub menu item
if (node && this.needsFocus) {
const subMenuNodes = node.querySelectorAll('[data-menu-item]');
if (subMenuNodes) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (subMenuNodes) {
if (subMenuNodes && subMenuNodes.length) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Thanks
5382cb3

subMenuNodes[0].focus();
}
}
};

getMenuContainerRef = () => this.menuContainer;

setVisuallyHiddenComponent(node) {
this.visuallyHiddenComponent = node;
}
Expand All @@ -189,14 +209,15 @@ class NavigationSideMenu extends Component {
key={key}
onClick={(event) => { this.handleItemClick(event, key); }}
onKeyDown={onKeyDown}
getMenuContainerRef={this.getMenuContainerRef}
data-menu-item={key}
/>
);
}

buildListContent(currentItem) {
if (currentItem && currentItem.childKeys && currentItem.childKeys.length) {
return <nav role="navigation"><ul role="menu" className={cx(['side-menu-list'])}>{currentItem.childKeys.map(key => this.buildListItem(key))}</ul></nav>;
return <nav role="navigation" aria-label={this.props.ariaLabel}><ul role="menu" ref={(refobj) => this.handleMenuListRef(refobj)} className={cx(['side-menu-list'])}>{currentItem.childKeys.map(key => this.buildListItem(key))}</ul></nav>;
}
return null;
}
Expand Down Expand Up @@ -245,7 +266,7 @@ class NavigationSideMenu extends Component {
<ActionHeader
className={cx('side-menu-action-header')}
onBack={onBack}
title={currentItem ? currentItem.text : null}
text={currentItem ? currentItem.text : null}
data-navigation-side-menu-action-header
/>
{toolbar}
Expand Down
71 changes: 40 additions & 31 deletions packages/terra-navigation-side-menu/src/_MenuItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,52 +38,63 @@ const propTypes = {
* Text display for the menu item.
* */
text: PropTypes.string,
/**
* @private
* Menu container Ref for menu items
* */
getMenuContainerRef: PropTypes.func,
};

class MenuItem extends React.Component {
constructor(props) {
super(props);
this.state = { active: false, focused: false };
this.state = { active: false };
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
this.handleOnBlur = this.handleOnBlur.bind(this);
this.textRender = this.textRender.bind(this);
}

handleOnBlur() {
this.setState({ focused: false });
this.handleMenuItemRef = this.handleMenuItemRef.bind(this);
}

handleKeyDown(event) {
// Add active state to FF browsers
if (event.nativeEvent.keyCode === KeyCode.KEY_SPACE) {
this.setState({ active: true });
const MenuContainerRef = this.props.getMenuContainerRef();
const listMenuItems = MenuContainerRef && MenuContainerRef.querySelectorAll('[data-menu-item]');
const currentIndex = Array.from(listMenuItems).indexOf(event.target);
const lastIndex = listMenuItems.length - 1;

if (event.nativeEvent.keyCode === KeyCode.KEY_DOWN) {
const nextIndex = currentIndex < lastIndex ? currentIndex + 1 : 0;
if (listMenuItems && listMenuItems[nextIndex]) {
listMenuItems[nextIndex].focus();
}
if (this.props.onKeyDown) {
this.props.onKeyDown(event);
}
event.preventDefault();
}

// Add focus styles for keyboard navigation
if (event.nativeEvent.keyCode === KeyCode.KEY_SPACE || event.nativeEvent.keyCode === KeyCode.KEY_RETURN) {
this.setState({ focused: true });
}

if (this.props.onKeyDown) {
// Add active state to FF browsers
this.setState({ active: true });
this.props.onKeyDown(event);
}
}

handleKeyUp(event) {
// Remove active state from FF broswers
if (event.nativeEvent.keyCode === KeyCode.KEY_SPACE) {
this.setState({ active: false });
}

// Apply focus styles for keyboard navigation
if (event.nativeEvent.keyCode === KeyCode.KEY_TAB) {
this.setState({ focused: true });
if (event.nativeEvent.keyCode === KeyCode.KEY_UP) {
// Remove active state from FF broswers
if (event.nativeEvent.keyCode === KeyCode.KEY_SPACE) {
this.setState({ active: false });
}
const previousIndex = currentIndex > 0 ? currentIndex - 1 : lastIndex;
if (listMenuItems && listMenuItems[previousIndex]) {
listMenuItems[previousIndex].focus();
}
if (this.props.onKeyUp) {
this.props.onKeyUp(event);
}
event.preventDefault();
}
}

if (this.props.onKeyUp) {
this.props.onKeyUp(event);
}
handleMenuItemRef(node) {
this.contentNode = node;
}

textRender() {
Expand Down Expand Up @@ -116,7 +127,6 @@ class MenuItem extends React.Component {
'menu-item',
{ 'is-selected': isSelected },
{ 'is-active': this.state.active },
{ 'is-focused': this.state.focused },
theme.className,
),
customProps.className);
Expand All @@ -128,12 +138,11 @@ class MenuItem extends React.Component {
>
<div
role="menuitem"
ref={this.handleMenuItemRef}
{...customProps}
tabIndex="0"
className={itemClassNames}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
onBlur={this.handleOnBlur}
aria-haspopup={hasChevron}
>
<div className={cx('title')}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,8 @@
--terra-navigation-side-menu-item-selected-hover-chevron-color: #c5c5c6;
--terra-navigation-side-menu-item-selected-hover-color: #b2b5b6;
--terra-navigation-side-menu-item-text-transform: none;
--terra-navigation-side-menu-focus-outline: 2px dashed #b2b5b6;
--terra-navigation-side-menu-focus-outline-offset: -2px;
--terra-navigation-side-menu-item-focus-box-shadow: none;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,8 @@
--terra-navigation-side-menu-item-selected-hover-chevron-color: #909496;
--terra-navigation-side-menu-item-selected-hover-color: #1c1f21;
--terra-navigation-side-menu-item-text-transform: none;
--terra-navigation-side-menu-focus-outline: none;
--terra-navigation-side-menu-focus-outline-offset: 0;
--terra-navigation-side-menu-item-focus-box-shadow: 0 0 0 1px rgba(76, 178, 233, 0.5), 0 0 4px 3px rgba(76, 178, 233, 0.35);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@ describe('Layout', () => {
expect(result).toMatchSnapshot();
});

it('should render a NavigationSideMenu with ariaLabel', () => {
const result = enzymeIntl.mountWithIntl((
<NavigationSideMenu
menuItems={[
{ key: 'menu', text: 'Test Menu', childKeys: ['test1', 'test2', 'test3', 'test4'] },
{ key: 'test1', text: 'Test Menu 1' },
{ key: 'test2', text: 'Test Menu 2' },
{ key: 'test3', text: 'Test Menu 3' },
{ key: 'test4', text: 'Test Menu 4' },
]}
onChange={() => {}}
routingStackBack={() => {}}
selectedMenuKey="menu"
ariaLabel="Sub Menu List"
/>
));
const navElement = result.find('nav');
expect(navElement.prop('aria-label')).toEqual('Sub Menu List');
expect(result).toMatchSnapshot();
});

it('correctly applies the theme context className', () => {
const result = enzymeIntl.mountWithIntl(
<ThemeContextProvider theme={{ className: 'orion-fusion-theme' }}>
Expand Down
Loading
Loading