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 : Keyboard navigation Fixes (#2126)
Browse files Browse the repository at this point in the history
  • Loading branch information
MadanKumarGovindaswamy authored Apr 29, 2024
1 parent 05f7d4f commit fbcbb5e
Show file tree
Hide file tree
Showing 57 changed files with 307 additions and 640 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ Terra.describeViewports('ApplicationLayout', ['small'], () => {

describe('Renders primary nav menu when small', () => {
it('Renders primary nav menu when small', () => {
$('[data-routing-menu] [data-navigation-side-menu-action-header] button').waitForDisplayed();
$('[data-routing-menu] [data-navigation-side-menu-action-header] button').click();
$('[data-routing-menu] [data-navigation-side-menu]').waitForDisplayed();
$('[data-routing-menu] [data-navigation-side-menu]').click();
$('[data-routing-menu]').waitForDisplayed();

Terra.validates.element('renders primary nav menu when small', { selector: '#application-layout-test' });
Expand Down
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 styles.
* Updated `terra-compact-interactive-list` keyboard interactions descriptions for the left and right arrow keys.

* Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
border: 1px solid #d3d3d3;
height: 450px;
position: relative;
width: 300px;
max-width: 300px;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
.content-wrapper {
height: 768px;
position: relative;
width: 300px;
max-width: 300px;
}

.toolbar {
Expand Down
3 changes: 3 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,9 @@

## Unreleased

* Updated
* Keyboard navigation with arrow keys.

## 2.54.0 - (April 4, 2024)

* Changed
Expand Down
176 changes: 146 additions & 30 deletions packages/terra-navigation-side-menu/src/NavigationSideMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl';
import classNames from 'classnames/bind';
import ActionHeader from 'terra-action-header';
import ContentContainer from 'terra-content-container';
import VisuallyHiddenText from 'terra-visually-hidden-text';
import * as KeyCode from 'keycode-js';
Expand Down Expand Up @@ -110,6 +109,9 @@ class NavigationSideMenu extends Component {
super(props);

this.handleBackClick = this.handleBackClick.bind(this);
this.handleBackKeydown = this.handleBackKeydown.bind(this);
this.handleEvents = this.handleEvents.bind(this);
this.setTabIndex = this.setTabIndex.bind(this);
this.handleItemClick = this.handleItemClick.bind(this);
this.updateAriaLiveContent = this.updateAriaLiveContent.bind(this);
this.setVisuallyHiddenComponent = this.setVisuallyHiddenComponent.bind(this);
Expand All @@ -131,6 +133,7 @@ class NavigationSideMenu extends Component {

handleBackClick(event) {
const parentKey = this.state.parents[this.props.selectedMenuKey];
this.focusKey = this.props.selectedMenuKey;
if (parentKey) {
this.props.onChange(
event,
Expand All @@ -141,6 +144,40 @@ class NavigationSideMenu extends Component {
},
);
}
this.setHeaderFocus = false;
event.preventDefault();
}

handleBackKeydown(event) {
const key = event.nativeEvent.keyCode;
switch (key) {
case KeyCode.KEY_SPACE:
case KeyCode.KEY_RETURN:
case KeyCode.KEY_LEFT:
case KeyCode.KEY_ESCAPE: {
const parentKey = this.state.parents[this.props.selectedMenuKey];
if (parentKey) {
this.handleBackClick(event);
} else if (this.props.routingStackBack) {
this.props.routingStackBack();
}
break;
}
case KeyCode.KEY_DOWN:
case KeyCode.KEY_UP: {
const listMenuItems = this.menuContainer && this.menuContainer.querySelectorAll('[data-menu-item]');
if (listMenuItems && listMenuItems.length) {
if (event.nativeEvent.keyCode === KeyCode.KEY_DOWN) {
listMenuItems[0].focus();
} else {
listMenuItems[listMenuItems.length - 1].focus();
}
}
event.preventDefault();
break;
}
default:
}
}

handleItemClick(event, key) {
Expand All @@ -151,8 +188,6 @@ 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 @@ -162,7 +197,6 @@ class NavigationSideMenu extends Component {
},
);
} else {
this.needsFocus = false;
this.props.onChange(
event,
{
Expand All @@ -172,32 +206,104 @@ class NavigationSideMenu extends Component {
},
);
}
this.focusKey = key;
if (selectedItem && selectedItem.childKeys && selectedItem.childKeys.length) {
this.setHeaderFocus = true;
} else {
this.setHeaderFocus = false;
}
}

handleRightMove(event, key) {
this.handleItemClick(event, key);
}

handleLeftMove(event) {
this.handleBackClick(event);
}

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 (node && this.focusKey) {
const subMenuNodes = node.querySelectorAll(`[data-menu-item="${this.focusKey}"]`);
if (subMenuNodes && subMenuNodes.length) {
subMenuNodes[0].focus();
}
}
};

getMenuContainerRef = () => this.menuContainer;
handleEvents = (event, item, key) => {
const listMenuItems = this.menuContainer && this.menuContainer.querySelectorAll('[data-menu-item]');
const currentIndex = Array.from(listMenuItems).indexOf(event.target);
const lastIndex = listMenuItems.length - 1;
if (event.nativeEvent.keyCode === KeyCode.KEY_SPACE || event.nativeEvent.keyCode === KeyCode.KEY_RETURN) {
event.preventDefault();
this.handleItemClick(event, key);
}

if (event.nativeEvent.keyCode === KeyCode.KEY_DOWN) {
const nextIndex = currentIndex < lastIndex ? currentIndex + 1 : 0;
if (currentIndex === lastIndex && this.onBack) {
if (this.backButtonContainer) {
this.backButtonContainer.focus();
}
} else if (listMenuItems && listMenuItems[nextIndex]) {
this.setTabIndex(listMenuItems[currentIndex], '-1');
this.setTabIndex(listMenuItems[nextIndex], '0');
listMenuItems[nextIndex].focus();
}
event.preventDefault();
}

if (event.nativeEvent.keyCode === KeyCode.KEY_UP) {
const previousIndex = currentIndex > 0 ? currentIndex - 1 : lastIndex;
if (currentIndex === 0 && this.onBack) {
if (this.backButtonContainer) {
this.backButtonContainer.focus();
}
} else if (listMenuItems && listMenuItems[previousIndex]) {
this.setTabIndex(listMenuItems[currentIndex], '-1');
this.setTabIndex(listMenuItems[previousIndex], '0');
listMenuItems[previousIndex].focus();
}
event.preventDefault();
}

if (event.nativeEvent.keyCode === KeyCode.KEY_RIGHT && (item.hasSubMenu || (item.childKeys && item.childKeys.length > 0))) {
this.handleRightMove(event, key);
event.preventDefault();
}

if (event.nativeEvent.keyCode === KeyCode.KEY_LEFT) {
this.handleLeftMove(event, key);
event.preventDefault();
}
};

setVisuallyHiddenComponent(node) {
this.visuallyHiddenComponent = node;
}

buildListItem(key) {
setTabIndex = (node, value) => {
if (node) {
node.setAttribute('tabIndex', value);
}
};

backButtonRef = (node) => {
this.backButtonContainer = node;
if (node && this.setHeaderFocus) {
if (this.backButtonContainer) {
this.backButtonContainer.focus();
}
}
};

buildListItem(key, keys) {
const item = this.state.items[key];
const tabIndex = Array.from(keys).indexOf(key);
const onKeyDown = (event) => {
if (event.nativeEvent.keyCode === KeyCode.KEY_SPACE || event.nativeEvent.keyCode === KeyCode.KEY_RETURN) {
event.preventDefault();
this.handleItemClick(event, key);
}
this.handleEvents(event, item, key);
};

return (
Expand All @@ -209,15 +315,15 @@ class NavigationSideMenu extends Component {
key={key}
onClick={(event) => { this.handleItemClick(event, key); }}
onKeyDown={onKeyDown}
getMenuContainerRef={this.getMenuContainerRef}
data-menu-item={key}
tabIndex={(tabIndex === 0 && !(this.onBack)) ? '0' : '-1'}
/>
);
}

buildListContent(currentItem) {
if (currentItem && currentItem.childKeys && currentItem.childKeys.length) {
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 currentItem.childKeys.map(key => this.buildListItem(key, currentItem.childKeys));
}
return null;
}
Expand Down Expand Up @@ -251,27 +357,32 @@ class NavigationSideMenu extends Component {
theme.className,
]);

let onBack;
const parentKey = this.state.parents[selectedMenuKey];
if (parentKey) {
onBack = this.handleBackClick;
this.onBack = this.handleBackClick;
} else {
onBack = routingStackBack;
this.onBack = routingStackBack;
}

let header;
if (onBack || !currentItem.isRootMenu) {
if (this.onBack || !currentItem.isRootMenu) {
header = (
<Fragment>
<ActionHeader
className={cx('side-menu-action-header')}
onBack={onBack}
text={currentItem ? currentItem.text : null}
data-navigation-side-menu-action-header
backButtonA11yLabel={currentItem ? currentItem.text : null}
/>
<li role="none">
<div
className={cx('side-navigation-menu')}
role="menuitem"
ref={(obj) => this.backButtonRef(obj)}
type="button"
tabIndex={(this.onBack) ? '0' : '-1'}
onKeyDown={this.handleBackKeydown}
onClick={this.onBack}
data-navigation-side-menu
>
{(this.onBack) ? <span className={cx(['header-icon', 'back'])} /> : null}
<h1 className={cx('title')}>{currentItem ? currentItem.text : null}</h1>
</div>
{toolbar}
</Fragment>
</li>
);
} else {
sideMenuContentContainerClassNames = cx(['side-menu-content-container', 'is-root']);
Expand All @@ -285,8 +396,13 @@ class NavigationSideMenu extends Component {
aria-relevant="additions text"
refCallback={this.setVisuallyHiddenComponent}
/>
<ContentContainer {...customProps} header={header} fill className={sideMenuContentContainerClassNames}>
{this.buildListContent(currentItem)}
<ContentContainer {...customProps} fill className={sideMenuContentContainerClassNames}>
<nav role="navigation" aria-label={this.props.ariaLabel}>
<ul role="menu" ref={(refobj) => this.handleMenuListRef(refobj)} className={cx(['side-menu-list'])}>
{header}
{this.buildListContent(currentItem, header)}
</ul>
</nav>
</ContentContainer>
</Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,56 @@
padding-top: var(--terra-navigation-side-menu-list-padding-top, 0.125rem);
}

.side-menu-action-header {
.side-navigation-menu {
align-items: center;
background-color: var(--terra-navigation-side-menu-action-header-background-color, #f4f4f4);
border-bottom-color: var(--terra-navigation-side-menu-action-header-border-bottom-color, #dedfe0);
border-bottom-color: var(--terra-navigation-side-menu-action-header-border-bottom-color, 1px solid #dedfe0);
box-shadow: var(--terra-navigation-side-menu-action-header-box-shadow);
color: var(--terra-navigation-side-menu-action-header-color);
display: flex;
z-index: 1;

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

.header-icon {
background-repeat: no-repeat;
background-size: auto;
display: inline-block;
height: 1rem;
margin-left: 10px;
position: relative;
top: 0;
vertical-align: -0.14285rem;
width: 1rem; // needed for different icon size in alternate themes

@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
height: 1rem;
vertical-align: var(--terra-navigation-side-menu-icon-ms-vertical-align, -0.17285rem);
}

&.back {
background-image: var(--terra-navigation-side-menu-back-background-image, inline-svg('<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path d="M48 21H10.6L27.3 3.9 23.5.1 0 24l23.5 23.9 3.8-3.8L10.6 27H48z"></path></svg>'));

&:hover {
background-image: var(--terra-navigation-side-menu-back-hover-background-image, inline-svg('<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path d="M48 21H10.6L27.3 3.9 23.5.1 0 24l23.5 23.9 3.8-3.8L10.6 27H48z"></path></svg>'));
}
}
}

.title {
color: var(--terra-navigation-side-menu-header-color);
font-size: var(--terra-navigation-side-menu-font-size, 1.10714rem);
font-weight: var(--terra-navigation-side-menu-font-weight, 500);
hyphens: auto;
margin: 5px;
overflow-wrap: break-word; /* Modern browsers */
padding: 0.28571rem;
width: 100%;
word-wrap: break-word; /* For IE 10 and IE 11 */
}
}
Loading

0 comments on commit fbcbb5e

Please sign in to comment.