Skip to content

Commit

Permalink
feat(menu component): improve accessibility (#3149)
Browse files Browse the repository at this point in the history
Co-authored-by: denStrigo <[email protected]>
  • Loading branch information
denStrigo and denStrigo authored Dec 14, 2022
1 parent ead1002 commit 1785488
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 75 deletions.
114 changes: 65 additions & 49 deletions src/framework/theme/components/menu/menu-item.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,75 +2,91 @@
<nb-icon class="menu-icon" [config]="menuItem.icon" *ngIf="menuItem.icon"></nb-icon>
{{ menuItem.title }}
</span>
<a *ngIf="menuItem.link && !menuItem.url && !menuItem.children && !menuItem.group"
[routerLink]="menuItem.link"
[queryParams]="menuItem.queryParams"
[fragment]="menuItem.fragment"
[queryParamsHandling]="menuItem.queryParamsHandling"
[preserveFragment]="menuItem.preserveFragment"
[skipLocationChange]="menuItem.skipLocationChange"
[attr.target]="menuItem.target"
[attr.title]="menuItem.title"
[class.active]="menuItem.selected"
(mouseenter)="onHoverItem(menuItem)"
(click)="onItemClick(menuItem);">
<a
*ngIf="menuItem.link && !menuItem.url && !menuItem.children && !menuItem.group"
[routerLink]="menuItem.link"
[queryParams]="menuItem.queryParams"
[fragment]="menuItem.fragment"
[queryParamsHandling]="menuItem.queryParamsHandling"
[preserveFragment]="menuItem.preserveFragment"
[skipLocationChange]="menuItem.skipLocationChange"
[attr.target]="menuItem.target"
[attr.title]="menuItem.title"
[attr.role]="menuItem.ariaRole"
[class.active]="menuItem.selected"
(mouseenter)="onHoverItem(menuItem)"
(click)="onItemClick(menuItem)"
>
<nb-icon class="menu-icon" [config]="menuItem.icon" *ngIf="menuItem.icon"></nb-icon>
<span class="menu-title">{{ menuItem.title }}</span>
<ng-container *ngIf="badge" [ngTemplateOutlet]="badgeTemplate"></ng-container>
</a>
<a *ngIf="menuItem.url && !menuItem.children && !menuItem.link && !menuItem.group"
[attr.href]="menuItem.url"
[attr.target]="menuItem.target"
[attr.title]="menuItem.title"
[class.active]="menuItem.selected"
(mouseenter)="onHoverItem(menuItem)"
(click)="onSelectItem(menuItem)">
<a
*ngIf="menuItem.url && !menuItem.children && !menuItem.link && !menuItem.group"
[attr.href]="menuItem.url"
[attr.target]="menuItem.target"
[attr.title]="menuItem.title"
[attr.role]="menuItem.ariaRole"
[class.active]="menuItem.selected"
(mouseenter)="onHoverItem(menuItem)"
(click)="onSelectItem(menuItem)"
>
<nb-icon class="menu-icon" [config]="menuItem.icon" *ngIf="menuItem.icon"></nb-icon>
<span class="menu-title">{{ menuItem.title }}</span>
<ng-container *ngIf="badge" [ngTemplateOutlet]="badgeTemplate"></ng-container>
</a>
<a *ngIf="!menuItem.children && !menuItem.link && !menuItem.url && !menuItem.group"
[attr.target]="menuItem.target"
[attr.title]="menuItem.title"
[class.active]="menuItem.selected"
(mouseenter)="onHoverItem(menuItem)"
(click)="$event.preventDefault(); onItemClick(menuItem);">
<a
*ngIf="!menuItem.children && !menuItem.link && !menuItem.url && !menuItem.group"
[attr.target]="menuItem.target"
[attr.title]="menuItem.title"
[attr.role]="menuItem.ariaRole"
[class.active]="menuItem.selected"
(mouseenter)="onHoverItem(menuItem)"
(click)="$event.preventDefault(); onItemClick(menuItem)"
>
<nb-icon class="menu-icon" [config]="menuItem.icon" *ngIf="menuItem.icon"></nb-icon>
<span class="menu-title">{{ menuItem.title }}</span>
<ng-container *ngIf="badge" [ngTemplateOutlet]="badgeTemplate"></ng-container>
</a>
<a *ngIf="menuItem.children"
(click)="$event.preventDefault(); onToggleSubMenu(menuItem);"
[attr.target]="menuItem.target"
[attr.title]="menuItem.title"
[class.active]="menuItem.selected"
(mouseenter)="onHoverItem(menuItem)"
href="#">
<a
*ngIf="menuItem.children"
(click)="$event.preventDefault(); onToggleSubMenu(menuItem)"
[attr.target]="menuItem.target"
[attr.title]="menuItem.title"
[attr.aria-expanded]="menuItem.expanded || false"
[attr.role]="menuItem.ariaRole"
[class.active]="menuItem.selected"
(mouseenter)="onHoverItem(menuItem)"
href="#"
>
<nb-icon class="menu-icon" [config]="menuItem.icon" *ngIf="menuItem.icon"></nb-icon>
<span class="menu-title">{{ menuItem.title }}</span>
<ng-container *ngIf="badge" [ngTemplateOutlet]="badgeTemplate"></ng-container>
<nb-icon class="expand-state" [icon]="getExpandStateIcon()" pack="nebular-essentials"></nb-icon>
</a>
<ul *ngIf="menuItem.children"
[class.collapsed]="!(menuItem.children && menuItem.expanded)"
[class.expanded]="menuItem.expanded"
[@toggle]="toggleState"
class="menu-items">
<ul
*ngIf="menuItem.children"
[class.collapsed]="!(menuItem.children && menuItem.expanded)"
[class.expanded]="menuItem.expanded"
[@toggle]="toggleState"
class="menu-items"
>
<ng-container *ngFor="let item of menuItem.children">
<li nbMenuItem *ngIf="!item.hidden"
[menuItem]="item"
[badge]="item.badge"
[class.menu-group]="item.group"
(hoverItem)="onHoverItem($event)"
(toggleSubMenu)="onToggleSubMenu($event)"
(selectItem)="onSelectItem($event)"
(itemClick)="onItemClick($event)"
class="menu-item">
</li>
<li
nbMenuItem
*ngIf="!item.hidden"
[menuItem]="item"
[badge]="item.badge"
[class.menu-group]="item.group"
(hoverItem)="onHoverItem($event)"
(toggleSubMenu)="onToggleSubMenu($event)"
(selectItem)="onSelectItem($event)"
(itemClick)="onItemClick($event)"
class="menu-item"
></li>
</ng-container>
</ul>

<ng-template #badgeTemplate>
<nb-badge [text]="badge.text" [dotMode]="badge.dotMode" [status]="badge.status">
</nb-badge>
<nb-badge [text]="badge.text" [dotMode]="badge.dotMode" [status]="badge.status"> </nb-badge>
</ng-template>
1 change: 1 addition & 0 deletions src/framework/theme/components/menu/menu.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

.menu-title {
flex: 1 0 auto;
pointer-events: none;
@include nb-rtl(text-align, right);
}
}
Expand Down
50 changes: 27 additions & 23 deletions src/framework/theme/components/menu/menu.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import { isFragmentContain, isFragmentEqual, isUrlPathContain, isUrlPathEqual }
import { NbIconConfig } from '../icon/icon.component';
import { NbBadge } from '../badge/badge.component';

export interface NbMenuBag { tag: string; item: NbMenuItem }
export interface NbMenuBag {
tag: string;
item: NbMenuItem;
}

const itemClick$ = new Subject<NbMenuBag>();
const addItems$ = new ReplaySubject<{ tag: string; items: NbMenuItem[] }>(1);
const navigateHome$ = new ReplaySubject<{ tag: string }>(1);
const getSelectedItem$
= new ReplaySubject<{ tag: string; listener: BehaviorSubject<NbMenuBag> }>(1);
const getSelectedItem$ = new ReplaySubject<{ tag: string; listener: BehaviorSubject<NbMenuBag> }>(1);
const itemSelect$ = new ReplaySubject<NbMenuBag>(1);
const itemHover$ = new ReplaySubject<NbMenuBag>(1);
const submenuToggle$ = new ReplaySubject<NbMenuBag>(1);
Expand Down Expand Up @@ -111,6 +113,10 @@ export class NbMenuItem {
data?: any;
fragment?: string;
preserveFragment?: boolean;
/** The name of a role in the ARIA specification
* @type {string}
*/
ariaRole?: string;

/**
* @returns item parents in top-down order
Expand All @@ -128,9 +134,7 @@ export class NbMenuItem {
}

static isParent(item: NbMenuItem, possibleChild: NbMenuItem): boolean {
return possibleChild.parent
? possibleChild.parent === item || this.isParent(item, possibleChild.parent)
: false;
return possibleChild.parent ? possibleChild.parent === item || this.isParent(item, possibleChild.parent) : false;
}
}

Expand All @@ -146,7 +150,6 @@ export class NbMenuItem {
*/
@Injectable()
export class NbMenuService {

/**
* Add items to the end of the menu items list
* @param {List<NbMenuItem>} items
Expand Down Expand Up @@ -204,12 +207,11 @@ export class NbMenuService {

@Injectable()
export class NbMenuInternalService {

constructor(private location: Location) {}

prepareItems(items: NbMenuItem[]) {
const defaultItem = new NbMenuItem();
items.forEach(i => {
items.forEach((i) => {
this.applyDefaults(i, defaultItem);
this.setParent(i);
});
Expand Down Expand Up @@ -284,19 +286,19 @@ export class NbMenuInternalService {
}

itemHover(item: NbMenuItem, tag?: string) {
itemHover$.next({tag, item});
itemHover$.next({ tag, item });
}

submenuToggle(item: NbMenuItem, tag?: string) {
submenuToggle$.next({tag, item});
submenuToggle$.next({ tag, item });
}

itemSelect(item: NbMenuItem, tag?: string) {
itemSelect$.next({tag, item});
itemSelect$.next({ tag, item });
}

itemClick(item: NbMenuItem, tag?: string) {
itemClick$.next({tag, item});
itemClick$.next({ tag, item });
}

/**
Expand Down Expand Up @@ -336,7 +338,7 @@ export class NbMenuInternalService {
}

if (item.expanded) {
collapsedItems.push(item)
collapsedItems.push(item);
}
item.expanded = false;

Expand All @@ -349,18 +351,20 @@ export class NbMenuInternalService {
}

private applyDefaults(item, defaultItem) {
const menuItem = {...item};
const menuItem = { ...item };
Object.assign(item, defaultItem, menuItem);
item.children && item.children.forEach(child => {
this.applyDefaults(child, defaultItem);
});
item.children &&
item.children.forEach((child) => {
this.applyDefaults(child, defaultItem);
});
}

private setParent(item: NbMenuItem) {
item.children && item.children.forEach(child => {
child.parent = item;
this.setParent(child);
});
item.children &&
item.children.forEach((child) => {
child.parent = item;
this.setParent(child);
});
}

/**
Expand All @@ -371,7 +375,7 @@ export class NbMenuInternalService {
private findItemByUrl(items: NbMenuItem[]): NbMenuItem | undefined {
let selectedItem;

items.some(item => {
items.some((item) => {
if (item.children) {
selectedItem = this.findItemByUrl(item.children);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,16 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NbMenuItem } from '@nebular/theme';

@Component({
selector: 'nb-menu-autocollapse',
selector: 'npg-menu-autocollapse',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './menu-autocollapse.component.html',
})

export class MenuAutoCollapseComponent {

items: NbMenuItem[] = [
{
title: 'Profile',
expanded: true,
ariaRole: 'button',
children: [
{
title: 'Change Password',
Expand All @@ -33,6 +32,7 @@ export class MenuAutoCollapseComponent {
},
{
title: 'Shopping Bag',
ariaRole: 'button',
children: [
{
title: 'First Product',
Expand All @@ -47,6 +47,7 @@ export class MenuAutoCollapseComponent {
},
{
title: 'Orders',
ariaRole: 'button',
children: [
{
title: 'First Order',
Expand Down

0 comments on commit 1785488

Please sign in to comment.