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

Commit

Permalink
[terra-navigation-side-menu] Accessibility Changes (#2018)
Browse files Browse the repository at this point in the history
Co-authored-by: Sugan G <sugan.guts@gmail.com>
  • Loading branch information
MadanKumarGovindaswamy and sugan2416 authored Feb 21, 2024
1 parent 4aaab33 commit c44f6ee
Show file tree
Hide file tree
Showing 20 changed files with 911 additions and 70 deletions.
1 change: 1 addition & 0 deletions packages/terra-framework-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* Added
* Added shouldTrapFocus to be set to 'true'.
* 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 && subMenuNodes.length) {
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

0 comments on commit c44f6ee

Please sign in to comment.