Skip to content

Commit

Permalink
fix(ui5-notification): implement keyboard navigation spec (#8975)
Browse files Browse the repository at this point in the history
`ui5-notification-list` component is included to enhance accessibility.

BREAKING CHANGE: Instead of `ui5-list`, `ui5-notification-list` should be used as a container for `ui5-li-notification-group` and `ui5-li-notification` components.

Previously the application developers were defining notifications in this way:

```
<ui5-list>
        <ui5-li-notification-group title-text="Group Title" >
            <ui5-li-notification..
```
To support accessibility, developers should now use the `ui5-notification-list` as seen below:

```
<ui5-notification-list>
        <ui5-li-notification-group title-text="Group Title" >
            <ui5-li-notification..
```
  • Loading branch information
TeodorTaushanov committed May 21, 2024
1 parent 2a8c252 commit d68c883
Show file tree
Hide file tree
Showing 22 changed files with 395 additions and 174 deletions.
3 changes: 2 additions & 1 deletion packages/fiori/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ such as a common header (ShellBar).
| Illustrated Message | `ui5-illustrated-message` | `import "@ui5/webcomponents-fiori/dist/IllustratedMessage.js";` |
| Media Gallery | `ui5-media-gallery` | `import "@ui5/webcomponents-fiori/dist/MediaGallery.js";` |
| Media Gallery Item | `ui5-media-gallery-item` | comes with `ui5-media-gallery` |
| Notification List Item | `ui5-li-notifcation` | `import "@ui5/webcomponents-fiori/dist/NotifcationListItem.js";` |
| Notification List | `ui5-notification-list` | `import "@ui5/webcomponents-fiori/dist/NotifcationList.js";` |
| Notification List Item | `ui5-li-notification` | `import "@ui5/webcomponents-fiori/dist/NotifcationListItem.js";` |
| Notification Group List Item | `ui5-li-notification-group` | `import "@ui5/webcomponents-fiori/dist/NotifcationListGroupItem.js";` |
| Notification Action | `ui5-notification-action` | `import "@ui5/webcomponents-fiori/dist/NotificationAction.js";` |
| Page | `ui5-page` | `import "@ui5/webcomponents-fiori/dist/Page.js";` |
Expand Down
57 changes: 57 additions & 0 deletions packages/fiori/src/NotificationList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import ListItemBase from "@ui5/webcomponents/dist/ListItemBase.js";
import List from "@ui5/webcomponents/dist/List.js";
import NotificationListGroupItem from "./NotificationListGroupItem.js";

// Texts
import {
NOTIFICATION_LIST_ACCESSIBLE_NAME,
} from "./generated/i18n/i18n-defaults.js";

/**
* @class
*
* The `ui5-notification-list` web component represents
* a container for `ui5-li-notification-group` and `ui5-li-notification`.
*
* @constructor
* @extends List
* @since 2.0
* @public
*/
@customElement("ui5-notification-list")

class NotificationList extends List {
constructor() {
super();
this.accessibleName = NotificationList.i18nFioriBundle.getText(NOTIFICATION_LIST_ACCESSIBLE_NAME);
}

static i18nFioriBundle: I18nBundle;

getEnabledItems(): Array<ListItemBase> {
const items = new Array<ListItemBase>();

this.getItems().forEach(item => {
items.push(item);

if (item instanceof NotificationListGroupItem && !item.collapsed) {
item.items.forEach(subItem => {
items.push(subItem);
});
}
});

return items;
}

static async onDefine() {
NotificationList.i18nFioriBundle = await getI18nBundle("@ui5/webcomponents-fiori");
}
}

NotificationList.define();

export default NotificationList;
4 changes: 2 additions & 2 deletions packages/fiori/src/NotificationListGroupItem.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@
<div class="ui5-nli-group-divider"></div>
</div>

<ui5-list
<ui5-notification-group-list
id="{{_id}}-notificationsList"
class="ui5-nli-group-items"
role="list"
aria-labelledby="{{_id}}-title-text">
<slot></slot>
</ui5-list>
</ui5-notification-group-list>

{{#if loading}}
<ui5-busy-indicator
Expand Down
51 changes: 13 additions & 38 deletions packages/fiori/src/NotificationListGroupItem.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {
isSpace, isPlus, isMinus, isLeft, isRight, isDown, isUp,
isSpace, isPlus, isMinus, isLeft, isRight,
} from "@ui5/webcomponents-base/dist/Keys.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import event from "@ui5/webcomponents-base/dist/decorators/event.js";
import List from "@ui5/webcomponents/dist/List.js";
import Button from "@ui5/webcomponents/dist/Button.js";
import BusyIndicator from "@ui5/webcomponents/dist/BusyIndicator.js";
import Icon from "@ui5/webcomponents/dist/Icon.js";
import NotificationListGroupList from "./NotificationListGroupList.js";
import NotificationListItemBase from "./NotificationListItemBase.js";

// Icons
Expand Down Expand Up @@ -85,7 +85,7 @@ type NotificationListGroupItemToggleEventDetail = {
],
template: NotificationListGroupItemTemplate,
dependencies: [
List,
NotificationListGroupList,
Button,
Icon,
BusyIndicator,
Expand Down Expand Up @@ -170,6 +170,7 @@ class NotificationListGroupItem extends NotificationListItemBase {
}

get groupCollapsedTooltip() {
// eslint-disable-next-line
// ToDo: edit and add translation when spec is ready
return this.collapsed ? "expand arrow" : "collapse arrow";
}
Expand All @@ -183,21 +184,22 @@ class NotificationListGroupItem extends NotificationListItemBase {
* Event handlers
*
*/

_onHeaderToggleClick() {
this.toggleCollapsed();
}

_onkeydown(e: KeyboardEvent) {
super._onkeydown(e);
async _onkeydown(e: KeyboardEvent) {
await super._onkeydown(e);

if (!this.focused) {
return;
}

const space = isSpace(e);
const plus = isPlus(e);
const minus = isMinus(e);
const left = isLeft(e);
const right = isRight(e);
const down = isDown(e);
const up = isUp(e);

if (space) {
this.toggleCollapsed();
Expand All @@ -216,37 +218,10 @@ class NotificationListGroupItem extends NotificationListItemBase {
this.toggleCollapsed();
}
}
}

if (down) {
const notificationItems = this.items;
const lastItemIndex = notificationItems.length - 1;
const isLastItem = e.target === notificationItems[lastItemIndex];
const groupsInList = this.parentElement?.children;
const indexOfCurrentGroup = groupsInList ? Array.from(groupsInList).findIndex(element => (element === this)) : -1;

// if the focus is on the header (whole group) move it to the first notification item
if (!this.collapsed && this.hasAttribute("focused") && notificationItems[0]) {
notificationItems[0].focus();
}

// if the focus is on the last item move it to the next group (if available)
if (!this.collapsed && isLastItem) {
// focus the next (sibling) group
if (groupsInList && groupsInList[indexOfCurrentGroup] && groupsInList[indexOfCurrentGroup + 1]) {
// @ts-ignore
groupsInList[indexOfCurrentGroup + 1].focus();
}
}
}

if (up) {
const notificationItems = this.items;

// if the focus is on the first notification item move it to the header (whole group)
if (!this.collapsed && e.target === notificationItems[0]) {
this.focus();
}
}
getHeaderDomRef() {
return this.getDomRef()?.querySelector(".ui5-nli-group-header") as HTMLElement;
}
}

Expand Down
30 changes: 30 additions & 0 deletions packages/fiori/src/NotificationListGroupList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import List from "@ui5/webcomponents/dist/List.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";

/**
* @class
*
* Internal `ui5-li-notification-group-list` component,
* that is used to support keyboard navigation of the notification group internal list.
*
* @private
*/
@customElement("ui5-notification-group-list")
class NotificationListGroupList extends List {
getEnabledItems() {
return [];
}

_handleTabNext() {
}

onForwardBefore() {
}

onForwardAfter() {
}
}

NotificationListGroupList.define();

export default NotificationListGroupList;
7 changes: 4 additions & 3 deletions packages/fiori/src/NotificationListItem.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
aria-describedby="{{ariaDescribedBy}}"
aria-level="2"
>

<div class="{{contentClasses}}">

{{#if hasImportance}}
<ui5-tag class="ui5-nli-importance" design="Set2" color-scheme="2">
<ui5-icon name="high-priority" slot="icon"></ui5-icon>
{{importanceText}}
</ui5-tag>
{{/if}}
{{/if}}
<div class="ui5-nli-title-text-wrapper">
{{#if hasState}}
<ui5-icon
Expand Down Expand Up @@ -76,6 +76,7 @@
{{#if showClose}}
<ui5-button
icon="decline"
class="ui5-nli-close-btn"
design="Transparent"
@click="{{_onBtnCloseClick}}"
tooltip="{{closeBtnAccessibleName}}"
Expand Down
42 changes: 39 additions & 3 deletions packages/fiori/src/NotificationListItem.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
isSpace, isEnter, isDelete, isF10Shift, isEnterShift,
isSpace, isEnter, isDelete, isF10Shift, isEnterShift, isUp, isDown,
} from "@ui5/webcomponents-base/dist/Keys.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
Expand All @@ -18,6 +18,7 @@ import type Menu from "@ui5/webcomponents/dist/Menu.js";
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
import NotificationListItemImportance from "./types/NotificationListItemImportance.js";
import NotificationListItemBase from "./NotificationListItemBase.js";
import type NotificationList from "./NotificationList.js";

// Icons
import "@ui5/webcomponents-icons/dist/overflow.js";
Expand Down Expand Up @@ -480,8 +481,8 @@ class NotificationListItem extends NotificationListItemBase {
this._showMorePressed = !this._showMorePressed;
}

_onkeydown(e: KeyboardEvent) {
super._onkeydown(e);
async _onkeydown(e: KeyboardEvent) {
await super._onkeydown(e);

if (isEnter(e)) {
this.fireItemPress(e);
Expand All @@ -490,6 +491,41 @@ class NotificationListItem extends NotificationListItemBase {
if (isF10Shift(e)) {
e.preventDefault();
}

this.focusSameItemOnNextRow(e);
}

focusSameItemOnNextRow(e: KeyboardEvent) {
if (this.focused || (!isUp(e) && !isDown(e))) {
return;
}

e.preventDefault();
e.stopImmediatePropagation();

const list = this.closest("[ui5-notification-list]") as NotificationList;
if (!list) {
return;
}

const navItems = list.getEnabledItems();
const index = navItems.indexOf(this) + (isUp(e) ? -1 : 1);
const nextItem = navItems[index] as NotificationListItemBase;
if (!nextItem) {
return;
}

const target = e.target as HTMLElement;
if (!target) {
return;
}

const sameItemOnNextRow = nextItem.getHeaderDomRef()!.querySelector(`.${target.className}`) as HTMLElement;
if (sameItemOnNextRow && sameItemOnNextRow.offsetParent) {
sameItemOnNextRow.focus();
} else {
nextItem.focus();
}
}

_onkeyup(e: KeyboardEvent) {
Expand Down
25 changes: 17 additions & 8 deletions packages/fiori/src/NotificationListItemBase.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { isSpace } from "@ui5/webcomponents-base/dist/Keys.js";
import { isSpace, isF2 } from "@ui5/webcomponents-base/dist/Keys.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import { getEventMark } from "@ui5/webcomponents-base/dist/MarkedEvents.js";
import ListItemBase from "@ui5/webcomponents/dist/ListItemBase.js";
import Integer from "@ui5/webcomponents-base/dist/types/Integer.js";
import { getFirstFocusableElement } from "@ui5/webcomponents-base/dist/util/FocusableElements.js";

/**
* @class
Expand Down Expand Up @@ -61,17 +61,26 @@ class NotificationListItemBase extends ListItemBase {
/**
* Event handlers
*/

_onkeydown(e: KeyboardEvent) {
async _onkeydown(e: KeyboardEvent) {
super._onkeydown(e);

if (getEventMark(e) === "button") {
return;
}

if (isSpace(e)) {
e.preventDefault();
}

if (isF2(e)) {
e.stopImmediatePropagation();
const focusDomRef = this.getHeaderDomRef()!;
if (this.focused) {
(await getFirstFocusableElement(focusDomRef))?.focus(); // start content editing
} else {
focusDomRef.focus(); // stop content editing
}
}
}

getHeaderDomRef() {
return this.getFocusDomRef();
}

static async onDefine() {
Expand Down
1 change: 1 addition & 0 deletions packages/fiori/src/bundle.esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import MediaGallery from "./MediaGallery.js";
import MediaGalleryItem from "./MediaGalleryItem.js";
import NotificationListGroupItem from "./NotificationListGroupItem.js";
import NotificationListItem from "./NotificationListItem.js";
import NotificationList from "./NotificationList.js";
import Page from "./Page.js";
import ProductSwitch from "./ProductSwitch.js";
import ProductSwitchItem from "./ProductSwitchItem.js";
Expand Down
3 changes: 3 additions & 0 deletions packages/fiori/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ FCL_END_COLUMN_EXPAND_BUTTON_TOOLTIP=Expand the last column
#XACT: ARIA announcement for the Collapse end column arrow button
FCL_END_COLUMN_COLLAPSE_BUTTON_TOOLTIP=Collapse the last column

#XTXT: Accessible name for the NotificationList
NOTIFICATION_LIST_ACCESSIBLE_NAME=Notifications

#XTXT: Text for the NotificationListGroupItem
NOTIFICATION_LIST_ITEM_TXT=Notification

Expand Down
Loading

0 comments on commit d68c883

Please sign in to comment.