Skip to content
This repository has been archived by the owner on Feb 2, 2023. It is now read-only.

Commit

Permalink
fix(material): recover keyboard navigation trough menus
Browse files Browse the repository at this point in the history
  • Loading branch information
klemenoslaj committed Jan 24, 2021
1 parent fc3ae86 commit fe033f4
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 109 deletions.
32 changes: 6 additions & 26 deletions projects/material/src/lib/action-mat-anchor.component.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,24 @@
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation, ElementRef, HostBinding, Inject } from '@angular/core';
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation, ElementRef, Inject, HostBinding } from '@angular/core';
import { ActionAnchor, ActionAnchorComponentImpl } from '@ng-action-outlet/core';
import { FocusableOption, FocusMonitor, FocusOrigin } from '@angular/cdk/a11y';

import { isMenuItem } from './common';
import { actionMatButtonTemplate } from './action-mat-button.template';
import { ICON_TYPE, ACTION_ICON_TYPE_TOKEN } from './action-icon-type-token';

export const AnchorType = <const>{
Link: 0,
MenuLink: 1
};

@Component({
selector: 'action-mat-anchor',
template: `
<ng-container *ngIf="(_action?.visible$ | async) && _action; let action" [ngSwitch]="_getAnchorType(action!)">
<ng-container *ngIf="(_action?.visible$ | async) && _action; let action">
<ng-container *ngIf="(action.href$ | async); let href" [ngTemplateOutlet]="action.isExternalLink() ? external : internal" [ngTemplateOutletContext]="{ $implicit: href }"></ng-container>
<ng-template #external let-href>
<a *ngSwitchCase="AnchorType.Link" mat-button [actionMatButton]="action" [href]="href" [attr.target]="action.target$ | async">
<ng-container *ngTemplateOutlet="content; context: { $implicit: action }"></ng-container>
</a>
<a *ngSwitchCase="AnchorType.MenuLink" mat-menu-item [actionMatButton]="action" [href]="href" [attr.target]="action.target$ | async">
<a mat-button [actionMatButton]="action" [href]="href" [attr.target]="action.target$ | async">
<ng-container *ngTemplateOutlet="content; context: { $implicit: action }"></ng-container>
</a>
</ng-template>
<ng-template #internal let-href>
<a *ngSwitchCase="AnchorType.Link" mat-button [actionMatButton]="action" [routerLink]="href" [target]="action.target$ | async">
<ng-container *ngTemplateOutlet="content; context: { $implicit: action }"></ng-container>
</a>
<a *ngSwitchCase="AnchorType.MenuLink" mat-menu-item [actionMatButton]="action" [routerLink]="href" [target]="action.target$ | async">
<a mat-button [actionMatButton]="action" [routerLink]="href" [target]="action.target$ | async">
<ng-container *ngTemplateOutlet="content; context: { $implicit: action }"></ng-container>
</a>
</ng-template>
Expand All @@ -45,13 +31,11 @@ export const AnchorType = <const>{
encapsulation: ViewEncapsulation.None,
})
export class ActionMatAnchorComponent implements ActionAnchorComponentImpl, FocusableOption {
@HostBinding('class.action-contents') _contentsClass = true;

@Input('action')
readonly _action?: ActionAnchor | null;

@HostBinding('class')
readonly _classname = 'action-mat-button';

AnchorType = AnchorType;

constructor(
@Inject(ACTION_ICON_TYPE_TOKEN)
Expand All @@ -67,8 +51,4 @@ export class ActionMatAnchorComponent implements ActionAnchorComponentImpl, Focu
this._elementRef.nativeElement.focus(options);
}
}

_getAnchorType(action: ActionAnchor) {
return isMenuItem(action.getParent()) ? AnchorType.MenuLink : AnchorType.Link;
}
}
8 changes: 4 additions & 4 deletions projects/material/src/lib/action-mat-button.component.scss
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
.action-mat-button {
.action-contents {
display: contents;
}

.action-mat-button .mat-button,
.action-mat-button .mat-icon-button {
.action-mat-button.mat-button,
.action-mat-button.mat-icon-button {
vertical-align: top;
}

.action-mat-button a:hover {
a.action-mat-button:hover {
text-decoration: underline;
}
41 changes: 6 additions & 35 deletions projects/material/src/lib/action-mat-button.component.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation, ElementRef, HostBinding, Inject } from '@angular/core';
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation, HostBinding, Inject } from '@angular/core';
import { ActionButton, ActionButtonComponentImpl } from '@ng-action-outlet/core';
import { FocusableOption, FocusMonitor, FocusOrigin } from '@angular/cdk/a11y';

import { isMenuItem } from './common';
import { actionMatButtonTemplate } from './action-mat-button.template';
import { ICON_TYPE, ACTION_ICON_TYPE_TOKEN } from './action-icon-type-token';

export const ButtonType = <const>{
Button: 0,
MenuItem: 1
};

@Component({
selector: 'action-mat-button',
template: `
<ng-container *ngIf="_action && _action.visible$ | async" [ngSwitch]="_getButtonType(_action!)">
<button *ngSwitchCase="ButtonType.Button" mat-button [actionMatButton]="_action">
<ng-container *ngTemplateOutlet="content; context: { $implicit: _action }"></ng-container>
</button>
<button *ngSwitchCase="ButtonType.MenuItem" mat-menu-item [actionMatButton]="_action">
<ng-container *ngIf="_action && _action.visible$ | async">
<button mat-button [actionMatButton]="_action">
<ng-container *ngTemplateOutlet="content; context: { $implicit: _action }"></ng-container>
</button>
</ng-container>
Expand All @@ -30,32 +19,14 @@ export const ButtonType = <const>{
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class ActionMatButtonComponent implements ActionButtonComponentImpl, FocusableOption {
export class ActionMatButtonComponent implements ActionButtonComponentImpl {
@HostBinding('class.action-contents') _contentsClass = true;

@Input('action')
readonly _action?: ActionButton | null;

@HostBinding('class')
readonly _classname = 'action-mat-button';

ButtonType = ButtonType;

constructor(
@Inject(ACTION_ICON_TYPE_TOKEN)
readonly _iconType: ICON_TYPE,
private readonly _elementRef: ElementRef,
private readonly _focusMonitor?: FocusMonitor,
) {}

focus(origin: FocusOrigin = 'program', options?: FocusOptions): void {
if (this._focusMonitor) {
this._focusMonitor.focusVia(this._elementRef.nativeElement, origin, options);
} else {
this._elementRef.nativeElement.focus(options);
}
}

_getButtonType(action: ActionButton) {
const isButtonMenuItem = isMenuItem(action.getParent());
return isButtonMenuItem ? ButtonType.MenuItem : ButtonType.Button;
}
}
37 changes: 19 additions & 18 deletions projects/material/src/lib/action-mat-button.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Directive, Input, OnDestroy, ElementRef, Renderer2, Optional, Inject, C
import { MatMenuTrigger, MatMenuItem } from '@angular/material/menu';
import { MatButton } from '@angular/material/button';
import { ActionButton, ActionGroup, ActionAnchor } from '@ng-action-outlet/core';
import { Subject, fromEvent, ReplaySubject, EMPTY } from 'rxjs';
import { Subject, fromEvent, ReplaySubject } from 'rxjs';
import { takeUntil, switchMap, filter } from 'rxjs/operators';

@Directive({
Expand All @@ -28,18 +28,26 @@ export class ActionMatButtonDirective implements OnDestroy {
renderer: Renderer2,
cdRef: ChangeDetectorRef,
) {
this._action$.pipe(
switchMap(action => action instanceof ActionAnchor ? EMPTY : action.disabled$),
takeUntil(this._unsubscribe$),
).subscribe(disabled => {
// Either MatButton, either MatMenuItem should always be present.
// tslint:disable-next-line: no-non-null-assertion
(matButton ?? matMenuItem)!.disabled = disabled;
cdRef.markForCheck();
});

renderer.addClass(nativeElement, 'action-mat-button');
if (nativeElement.tagName === 'BUTTON') {
renderer.setAttribute(nativeElement, 'type', 'button');

if (!matMenuTrigger) {
fromEvent(nativeElement, 'click').pipe(
switchMap(() => this._action$),
takeUntil(this._unsubscribe$),
).subscribe(action => action.trigger());
}

this._action$.pipe(
switchMap(action => action.disabled$),
takeUntil(this._unsubscribe$),
).subscribe(disabled => {
// Either MatButton, either MatMenuItem should always be present.
// tslint:disable-next-line: no-non-null-assertion
(matButton ?? matMenuItem)!.disabled = disabled;
cdRef.markForCheck();
});
}

const ariaLabel$ = this._action$.pipe(switchMap(action => action.ariaLabel$));
Expand Down Expand Up @@ -69,13 +77,6 @@ export class ActionMatButtonDirective implements OnDestroy {
renderer.addClass(nativeElement, 'mat-icon-button');
});
}

if (!matMenuTrigger) {
fromEvent(nativeElement, 'click').pipe(
switchMap(() => this._action$),
takeUntil(this._unsubscribe$),
).subscribe(action => action.trigger());
}
}

ngOnDestroy() {
Expand Down
14 changes: 12 additions & 2 deletions projects/material/src/lib/action-mat-menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,18 @@ import { ACTION_ICON_TYPE_TOKEN, ICON_TYPE } from './action-icon-type-token';
<ng-template #actionOutlet let-action>
<ng-container *ngIf="action.visible$ | async" [ngSwitch]="_isAnchor(action)">
<action-mat-anchor *ngSwitchCase="true" [action]="action"></action-mat-anchor>
<action-mat-button *ngSwitchDefault [action]="action"></action-mat-button>
<ng-container *ngSwitchCase="true">
<a *ngIf="action.isExternalLink()" mat-menu-item [actionMatButton]="action" [href]="action.href$ | async" [attr.target]="action.target$ | async">
<ng-container *ngTemplateOutlet="content; context: { $implicit: action }"></ng-container>
</a>
<a *ngIf="!action.isExternalLink()" mat-menu-item [actionMatButton]="action" [routerLink]="action.href$ | async" [target]="action.target$ | async">
<ng-container *ngTemplateOutlet="content; context: { $implicit: action }"></ng-container>
</a>
</ng-container>
<button *ngSwitchDefault mat-menu-item [actionMatButton]="action">
<ng-container *ngTemplateOutlet="content; context: { $implicit: action }"></ng-container>
</button>
</ng-container>
</ng-template>
Expand Down
52 changes: 28 additions & 24 deletions projects/playground/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export class AppComponent {
button1 = new ActionButton({
title: 'Button 1',
callback: this.callback,
disabled: true
});
link1 = new ActionAnchor({
title: 'Hello route (this tab)',
Expand Down Expand Up @@ -53,30 +52,35 @@ export class AppComponent {
ariaLabel: 'Menu for more actions',
children: [
this.menuItem1,
new ActionAnchor({
title: 'Hello route (new tab)',
href: ['hello'],
target: '_blank',
}),
new ActionAnchor({
title: 'Home route (this tab)',
href: ['home'],
}),
new ActionAnchor({
title: 'Home route (new tab)',
href: ['home'],
target: '_blank',
}),
new ActionAnchor({
title: 'Google (this tab)',
href: 'http://www.google.com',
}),
new ActionAnchor({
title: 'Google (new tab)',
href: 'http://www.google.com',
target: '_blank',
new ActionGroup({
children: [
new ActionAnchor({
title: 'Hello route (new tab)',
href: ['hello'],
target: '_blank',
}),
new ActionAnchor({
title: 'Home route (this tab)',
href: ['home'],
}),
new ActionAnchor({
title: 'Home route (new tab)',
href: ['home'],
target: '_blank',
}),
new ActionAnchor({
title: 'Google (this tab)',
href: 'http://www.google.com',
}),
new ActionAnchor({
title: 'Google (new tab)',
href: 'http://www.google.com',
target: '_blank',
}),]
}),
this.dropdown2,
new ActionGroup({
children: [this.dropdown2]
})
]
});
group1 = new ActionGroup({
Expand Down

0 comments on commit fe033f4

Please sign in to comment.