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

terra-navigation-side-menu : Keyboard navigation Fixes #2126

Merged
merged 33 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e2b747a
Update: keyboard navigation
MadanKumarGovindaswamy Apr 5, 2024
9602997
Update CHANGELOG.md
MadanKumarGovindaswamy Apr 5, 2024
de3fc96
Update NavigationSideMenu.test.jsx.snap
MadanKumarGovindaswamy Apr 5, 2024
24a7f86
Update: Lint errror and wdio failure
MadanKumarGovindaswamy Apr 5, 2024
bfd0308
Update NavigationSideMenu.jsx
MadanKumarGovindaswamy Apr 5, 2024
08df538
update: lint error
MadanKumarGovindaswamy Apr 5, 2024
4d75963
update: check wdio
MadanKumarGovindaswamy Apr 10, 2024
c0384bb
Update NavigationSideMenu.jsx
MadanKumarGovindaswamy Apr 10, 2024
055d20c
Update application-layout-spec.js
MadanKumarGovindaswamy Apr 10, 2024
060e764
update: wdio
MadanKumarGovindaswamy Apr 10, 2024
4ca2aa0
update: failed wdio
MadanKumarGovindaswamy Apr 10, 2024
8886f30
update: failed wdio
MadanKumarGovindaswamy Apr 10, 2024
7de193b
Update application_layout.png
MadanKumarGovindaswamy Apr 10, 2024
9521ffb
Update: navigation
MadanKumarGovindaswamy Apr 12, 2024
5a70a20
update: jest and wdio
MadanKumarGovindaswamy Apr 12, 2024
8abee3a
Update: navigation wdio
MadanKumarGovindaswamy Apr 12, 2024
1a565ca
Update toggles_menu_when_small.png
MadanKumarGovindaswamy Apr 12, 2024
f44db6a
Delete toggles_menu_when_small.png
MadanKumarGovindaswamy Apr 12, 2024
6cd1fc2
Revert "Delete toggles_menu_when_small.png"
MadanKumarGovindaswamy Apr 12, 2024
38b1f95
Update toggles_menu_when_small.png
MadanKumarGovindaswamy Apr 12, 2024
9aee957
update: add new wdio
MadanKumarGovindaswamy Apr 12, 2024
ea797ca
update: new wdio snapshots
MadanKumarGovindaswamy Apr 12, 2024
940f466
Update NavigationSideMenu.jsx
MadanKumarGovindaswamy Apr 15, 2024
e577b1e
Update NavigationSideMenu.test.jsx.snap
MadanKumarGovindaswamy Apr 15, 2024
09aa76c
Update NavigationSideMenu.jsx
MadanKumarGovindaswamy Apr 16, 2024
8989292
Update: example css update
MadanKumarGovindaswamy Apr 17, 2024
08f7d4f
update: wdio
MadanKumarGovindaswamy Apr 17, 2024
6815c03
Updtae: navigation example styles
MadanKumarGovindaswamy Apr 17, 2024
5700fa8
Update: refactor back button
MadanKumarGovindaswamy Apr 18, 2024
a44e116
fix: lint error
MadanKumarGovindaswamy Apr 18, 2024
927a303
Update: wdio
MadanKumarGovindaswamy Apr 19, 2024
bbb33f6
Update: change focus key on selection
MadanKumarGovindaswamy Apr 22, 2024
7ff7865
Merge branch 'main' into NavigationSideMenuKeyboardNavigation
MadanKumarGovindaswamy Apr 29, 2024
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
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
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we need UX design +1 for these ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No we just replaced action header component. and still we have used same icon and css styles used in action header.

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
Loading