Skip to content
This repository was archived by the owner on Oct 7, 2020. It is now read-only.

Commit cb4061d

Browse files
committed
feat(menu): Menu Improvements - Complete overhaul
* New mdc-menu-divider component * New mdc-menu-anchor directive * New mdc-menu-item directive * Update MDC Menu for MDC v0.13.0 BREAKING CHANGE: Removed [items] property. You'll need to start using the new mdc-menu-item directive.
1 parent 0d353aa commit cb4061d

File tree

9 files changed

+104
-55
lines changed

9 files changed

+104
-55
lines changed

src/lib/menu/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { NgModule } from '@angular/core';
22
import { CommonModule } from '@angular/common';
33

4-
import { MenuComponent } from './menu';
5-
import { MenuItemDirective } from './menu-item';
4+
import { MenuComponent } from './menu.component';
5+
import { MenuItemDirective } from './menu-item.directive';
6+
import { MenuAnchorDirective } from './menu-anchor.directive';
7+
import { MenuDividerComponent } from './menu-divider.component';
68

79
const MENU_COMPONENTS = [
810
MenuComponent,
9-
MenuItemDirective
11+
MenuItemDirective,
12+
MenuAnchorDirective,
13+
MenuDividerComponent
1014
];
1115

1216
@NgModule({

src/lib/menu/menu-adapter.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export interface MDCMenuAdapter {
33
removeClass: (string) => void
44
hasClass: (string) => void
55
hasNecessaryDom: () => boolean
6+
getAttributeForEventTarget: (target: EventTarget, attributeName: string) => string
67
getInnerDimensions: () => { width: number, height: number }
78
hasAnchor: () => boolean
89
getAnchorDimensions: () => { width: number, height: number, top: number, right: number, bottom: number, left: number }
@@ -12,8 +13,8 @@ export interface MDCMenuAdapter {
1213
getNumberOfItems: () => number
1314
registerInteractionHandler: (type: string, handler: EventListener) => void
1415
deregisterInteractionHandler: (type: string, handler: EventListener) => void
15-
registerDocumentClickHandler: (handler: EventListener) => void
16-
deregisterDocumentClickHandler: (handler: EventListener) => void
16+
registerBodyClickHandler: (handler: EventListener) => void
17+
deregisterBodyClickHandler: (handler: EventListener) => void
1718
getYParamsForItemAtIndex: (index: number) => { top: number, height: number }
1819
setTransitionDelayForItemAtIndex: (index: number, value: string) => void
1920
getIndexForEventTarget: (target: EventTarget) => number

src/lib/menu/menu-anchor.directive.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {
2+
Directive,
3+
HostBinding,
4+
} from '@angular/core';
5+
6+
@Directive({
7+
selector: '[mdc-menu-anchor]'
8+
})
9+
export class MenuAnchorDirective {
10+
@HostBinding('class') className: string = 'mdc-menu-anchor';
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {
2+
Component
3+
} from '@angular/core';
4+
5+
@Component({
6+
selector: 'mdc-menu-divider',
7+
template: '<li class="mdc-list-divider" role="seperator"><ng-content></ng-content></li>'
8+
})
9+
export class MenuDividerComponent { }

src/lib/menu/menu-item.directive.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {
2+
Directive,
3+
ElementRef,
4+
HostBinding,
5+
Input,
6+
Renderer2,
7+
} from '@angular/core';
8+
9+
@Directive({
10+
selector: 'mdc-menu-item'
11+
})
12+
export class MenuItemDirective {
13+
private disabled_: boolean = false;
14+
15+
@Input() id: string;
16+
@Input() label: string;
17+
@Input() icon: string;
18+
@Input()
19+
get disabled() {
20+
return this.disabled_;
21+
}
22+
set disabled(value: boolean) {
23+
this.disabled_ = value;
24+
if (value) {
25+
this._renderer.setAttribute(this._root.nativeElement, 'aria-disabled', 'true');
26+
this.tabindex = -1;
27+
} else {
28+
this._renderer.removeAttribute(this._root.nativeElement, 'aria-disabled');
29+
this.tabindex = 0;
30+
}
31+
}
32+
@HostBinding('class') className: string = 'mdc-list-item';
33+
@HostBinding('attr.role') role: string = 'menuitem';
34+
@HostBinding('tabindex') tabindex: number = 0;
35+
itemEl: ElementRef = this._root;
36+
37+
constructor(
38+
private _renderer: Renderer2,
39+
private _root: ElementRef) { }
40+
}

src/lib/menu/menu-item.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/lib/menu/menu.component.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<ul #menuContainer class="mdc-simple-menu__items mdc-list" role="menu" aria-hidden="true">
2+
<ng-content select="mdc-menu-item, mdc-menu-divider"></ng-content>
3+
</ul>

src/lib/menu/menu.ts renamed to src/lib/menu/menu.component.ts

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import {
22
AfterViewInit,
33
Component,
4+
ContentChildren,
45
ElementRef,
56
EventEmitter,
67
HostBinding,
78
Input,
8-
Output,
99
OnDestroy,
10+
Output,
1011
QueryList,
1112
Renderer2,
1213
ViewChild,
13-
ViewChildren,
1414
ViewEncapsulation
1515
} from '@angular/core';
1616
import { MDCMenuAdapter } from './menu-adapter';
17-
import { MenuItemDirective } from './menu-item';
17+
import { MenuItemDirective } from './menu-item.directive';
1818

19-
const { MDCSimpleMenuFoundation } = require('@material/menu');
19+
const { MDCSimpleMenuFoundation } = require('@material/menu/simple');
2020
const { getTransformPropertyName } = require('@material/menu/util');
2121
const MDC_MENU_STYLES = require('@material/menu/mdc-menu.scss');
2222
const MDC_LIST_STYLES = require('@material/list/mdc-list.scss');
@@ -25,18 +25,19 @@ type UnlistenerMap = WeakMap<EventListener, Function>;
2525

2626
@Component({
2727
selector: 'mdc-menu',
28-
templateUrl: './menu.html',
29-
styles: [String(MDC_MENU_STYLES)],
28+
templateUrl: './menu.component.html',
29+
styles: [String(MDC_MENU_STYLES), String(MDC_LIST_STYLES)],
3030
encapsulation: ViewEncapsulation.None
3131
})
3232
export class MenuComponent implements AfterViewInit, OnDestroy {
33-
@Input() items: MenuItemDirective[];
33+
private previousFocus_: any;
34+
3435
@Output() cancel: EventEmitter<void> = new EventEmitter<void>();
3536
@Output() select: EventEmitter<number> = new EventEmitter<number>();
3637
@HostBinding('class') className: string = 'mdc-simple-menu';
3738
@HostBinding('tabindex') tabindex: number = -1;
38-
@ViewChild('itemsContainer') public itemsContainerEl: ElementRef;
39-
@ViewChildren(MenuItemDirective) menuItems: QueryList<MenuItemDirective>;
39+
@ViewChild('menuContainer') public menuContainerEl: ElementRef;
40+
@ContentChildren(MenuItemDirective) menuItems: QueryList<MenuItemDirective>;
4041

4142
private _unlisteners: Map<string, UnlistenerMap> = new Map<string, UnlistenerMap>();
4243

@@ -49,11 +50,14 @@ export class MenuComponent implements AfterViewInit, OnDestroy {
4950
const { _renderer: renderer, _root: root } = this;
5051
renderer.removeClass(root.nativeElement, className);
5152
},
53+
getAttributeForEventTarget: (target: any, attributeName) => {
54+
return target.getAttribute(attributeName);
55+
},
5256
hasClass: (className: string) => {
5357
const { _root: root } = this;
5458
return root.nativeElement.classList.contains(className);
5559
},
56-
hasNecessaryDom: () => Boolean(this.itemsContainerEl),
60+
hasNecessaryDom: () => Boolean(this.menuContainerEl),
5761
getInnerDimensions: () => {
5862
const { _root: root } = this;
5963
return {
@@ -80,13 +84,11 @@ export class MenuComponent implements AfterViewInit, OnDestroy {
8084
renderer.setStyle(root.nativeElement, getTransformPropertyName(window), `scale(${x}, ${y})`);
8185
},
8286
setInnerScale: (x: number, y: number) => {
83-
if (this.itemsContainerEl) {
84-
const { _renderer: renderer, _root: root } = this;
85-
renderer.setStyle(this.itemsContainerEl.nativeElement, getTransformPropertyName(window), `scale(${x}, ${y})`);
86-
}
87+
const { _renderer: renderer, _root: root } = this;
88+
renderer.setStyle(this.menuContainerEl.nativeElement, getTransformPropertyName(window), `scale(${x}, ${y})`);
8789
},
8890
getNumberOfItems: () => {
89-
return this.items ? this.items.length : 0;
91+
return this.menuItems ? this.menuItems.length : 0;
9092
},
9193
registerInteractionHandler: (type: string, handler: EventListener) => {
9294
if (this._root) {
@@ -96,36 +98,37 @@ export class MenuComponent implements AfterViewInit, OnDestroy {
9698
deregisterInteractionHandler: (type: string, handler: EventListener) => {
9799
this.unlisten_(type, handler);
98100
},
99-
registerDocumentClickHandler: (handler: EventListener) => {
101+
registerBodyClickHandler: (handler: EventListener) => {
100102
if (this._root) {
101103
this.listen_('click', handler, this._root.nativeElement.ownerDocument);
102104
}
103105
},
104-
deregisterDocumentClickHandler: (handler: EventListener) => {
106+
deregisterBodyClickHandler: (handler: EventListener) => {
105107
this.unlisten_('click', handler);
106108
},
107109
getYParamsForItemAtIndex: (index: number) => {
108-
const { offsetTop: top, offsetHeight: height } = this.menuItems.toArray()[index].root.nativeElement;
110+
const { offsetTop: top, offsetHeight: height } = this.menuItems.toArray()[index].itemEl.nativeElement;
109111
return { top, height };
110112
},
111113
setTransitionDelayForItemAtIndex: (index: number, value: string) => {
112114
const { _renderer: renderer, _root: root } = this;
113-
renderer.setStyle(this.menuItems.toArray()[index].root.nativeElement, 'transition-delay', value);
115+
renderer.setStyle(this.menuItems.toArray()[index].itemEl.nativeElement, 'transition-delay', value);
114116
},
115-
getIndexForEventTarget: (target: any) => {
116-
if (!target.attributes.id) {
117-
return -1;
118-
}
119-
return this.items.findIndex(_ => _.id == target.attributes.id.value);
117+
getIndexForEventTarget: (target: EventTarget) => {
118+
return this.menuItems.toArray().findIndex((_) => _.itemEl.nativeElement === target);
120119
},
121120
notifySelected: (evtData) => {
122121
this.select.emit(evtData.index);
123122
},
124123
notifyCancel: () => {
125124
this.cancel.emit();
126125
},
127-
saveFocus: () => { }, /* TODO */
128-
restoreFocus: () => { }, /* TODO */
126+
saveFocus: () => this.previousFocus_ = document.activeElement,
127+
restoreFocus: () => {
128+
if (this.previousFocus_) {
129+
this.previousFocus_.focus()
130+
}
131+
},
129132
isFocused: () => {
130133
const { _root: root } = this;
131134
return root.nativeElement.ownerDocument.activeElement === root.nativeElement;
@@ -136,12 +139,12 @@ export class MenuComponent implements AfterViewInit, OnDestroy {
136139
getFocusedItemIndex: () => {
137140
const { _root: root } = this;
138141
return this.menuItems.length ? this.menuItems.toArray().findIndex(_ =>
139-
_.root.nativeElement === root.nativeElement.ownerDocument.activeElement) : -1;
142+
_.itemEl.nativeElement === root.nativeElement.ownerDocument.activeElement) : -1;
140143
},
141144
focusItemAtIndex: (index: number) => {
142145
const { _root: root } = this;
143146
if (this.menuItems.toArray()[index] !== undefined) {
144-
this.menuItems.toArray()[index].root.nativeElement.focus();
147+
this.menuItems.toArray()[index].itemEl.nativeElement.focus();
145148
} else {
146149
// set focus back to root element when index is undefined
147150
root.nativeElement.focus();

src/lib/menu/menu.html

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)