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

Commit 926c076

Browse files
authored
feat(menu): Support default focus state (#2004)
- Adds `defaultFocusState: 'none' | 'list' | 'firstItem' | 'lastItem'` for `MdcMenu` - Minor code refactoring of `MdcMenuSurfaceBase` - Update documentation - Include unit test closes #1998
1 parent 5408da3 commit 926c076

File tree

6 files changed

+135
-93
lines changed

6 files changed

+135
-93
lines changed

demos/src/app/components/menu-demo/api.html

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,9 @@ <h4 mdcSubtitle2>Properties</h4>
5454
<td>closeSurfaceOnSelection: boolean</td>
5555
<td>Sets whether the menu surface should close after item selection. Default is true</td>
5656
</tr>
57-
</tbody>
58-
</table>
59-
60-
<h4 mdcSubtitle2>Methods</h4>
61-
<table>
62-
<tbody>
6357
<tr>
64-
<td>focus()</td>
65-
<td>Set focus to the menu.</td>
58+
<td>defaultFocusState: 'none' | 'list' | 'firstItem' | 'lastItem'</td>
59+
<td>Sets default focus state where the menu should focus every time when menu is opened. Focuses the list root ('list') element by default.</td>
6660
</tr>
6761
</tbody>
6862
</table>

demos/src/app/components/menu-demo/examples.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ <h3 class="demo-content__headline">Anchor Margin</h3>
4444
<mdc-checkbox #closeSurfaceOnSelection [checked]="closeSurfaceOnSelection"></mdc-checkbox>
4545
<label>Close Surface on Selection</label>
4646
</mdc-form-field>
47+
<mdc-select #defaultFocusState placeholder="Default Focus State">
48+
<option value="none">None</option>
49+
<option value="list">List Root</option>
50+
<option value="firstItem">First Item</option>
51+
<option value="lastItem">Last Item</option>
52+
</mdc-select>
4753
</div>
4854
</div>
4955
</div>
@@ -57,6 +63,7 @@ <h3 class="demo-content__headline">Anchor Margin</h3>
5763
[anchorElement]="demoAnchor"
5864
[anchorCorner]="menuSurfaceAnchorCorner.value"
5965
[quickOpen]="quickOpen.checked"
66+
[defaultFocusState]="defaultFocusState.value"
6067
[fixed]="fixed.checked"
6168
[wrapFocus]="wrapFocus.checked"
6269
[closeSurfaceOnSelection]="closeSurfaceOnSelection.checked"

demos/src/app/components/menu-demo/menu-demo.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Component, OnInit, ViewChild } from '@angular/core';
1+
import {Component, OnInit, ViewChild} from '@angular/core';
22

3-
import { MdcListItem } from '@angular-mdc/web';
4-
import { ComponentViewer, ComponentView } from '../../shared/component-viewer';
3+
import {MdcListItem} from '@angular-mdc/web';
4+
import {ComponentViewer, ComponentView} from '../../shared/component-viewer';
55

6-
@Component({ template: '<component-viewer></component-viewer>' })
6+
@Component({template: '<component-viewer></component-viewer>'})
77
export class MenuDemo implements OnInit {
88
@ViewChild(ComponentViewer, {static: true}) _componentViewer: ComponentViewer;
99

@@ -23,36 +23,36 @@ export class MenuDemo implements OnInit {
2323
}
2424
}
2525

26-
@Component({ templateUrl: './api.html' })
27-
export class Api { }
26+
@Component({templateUrl: './api.html'})
27+
export class Api {}
2828

29-
@Component({ templateUrl: './sass.html' })
30-
export class Sass { }
29+
@Component({templateUrl: './sass.html'})
30+
export class Sass {}
3131

32-
@Component({ templateUrl: './examples.html' })
32+
@Component({templateUrl: './examples.html'})
3333
export class Examples {
3434
corners: string[] = ['topStart', 'topEnd', 'bottomStart', 'bottomEnd'];
3535

3636
fruits = [
37-
{ label: 'Passionfruit' },
38-
{ label: 'Orange' },
39-
{ label: 'Guava' },
40-
{ label: 'Pitaya' },
41-
{ label: null }, // null label sets a mdc-list-divider
42-
{ label: 'Pinaeapple' },
43-
{ label: 'Mango' },
44-
{ label: 'Papaya' },
45-
{ label: 'Lychee' }
37+
{label: 'Passionfruit'},
38+
{label: 'Orange'},
39+
{label: 'Guava'},
40+
{label: 'Pitaya'},
41+
{label: null}, // null label sets a mdc-list-divider
42+
{label: 'Pinaeapple'},
43+
{label: 'Mango'},
44+
{label: 'Papaya'},
45+
{label: 'Lychee'}
4646
];
4747

4848
lastSelection: number;
4949

50-
onMenuSelect(event: { index: number, item: MdcListItem }) {
50+
onMenuSelect(event: {index: number, item: MdcListItem}) {
5151
this.lastSelection = event.index;
5252
}
5353

5454
addFruit(): void {
55-
this.fruits.push({ label: 'New fruit item' });
55+
this.fruits.push({label: 'New fruit item'});
5656
}
5757

5858
exampleMenu = {

packages/menu-surface/menu-surface-base.ts

Lines changed: 57 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ export abstract class MdcMenuSurfaceBase extends MDCComponent<MDCMenuSurfaceFoun
4848
private _previousFocus: Element | null = null;
4949

5050
@Input()
51-
get open(): boolean { return this._open; }
51+
get open(): boolean {
52+
return this._open;
53+
}
5254
set open(value: boolean) {
5355
const newValue = coerceBooleanProperty(value);
5456
if (newValue !== this._open) {
@@ -59,30 +61,38 @@ export abstract class MdcMenuSurfaceBase extends MDCComponent<MDCMenuSurfaceFoun
5961
private _open: boolean = false;
6062

6163
@Input()
62-
get anchorElement(): HTMLElement | null { return this._anchorElement; }
64+
get anchorElement(): HTMLElement | null {
65+
return this._anchorElement;
66+
}
6367
set anchorElement(element: HTMLElement | null) {
6468
this._anchorElement = element;
6569
}
6670
private _anchorElement: HTMLElement | null = null;
6771

6872
@Input()
69-
get anchorCorner(): AnchorCorner { return this._anchorCorner; }
73+
get anchorCorner(): AnchorCorner {
74+
return this._anchorCorner;
75+
}
7076
set anchorCorner(value: AnchorCorner) {
7177
this._anchorCorner = value || 'topStart';
7278
this._foundation.setAnchorCorner(ANCHOR_CORNER_MAP[this._anchorCorner]);
7379
}
7480
private _anchorCorner: AnchorCorner = 'topStart';
7581

7682
@Input()
77-
get quickOpen(): boolean { return this._quickOpen; }
83+
get quickOpen(): boolean {
84+
return this._quickOpen;
85+
}
7886
set quickOpen(value: boolean) {
7987
this._quickOpen = coerceBooleanProperty(value);
8088
this._foundation.setQuickOpen(this._quickOpen);
8189
}
8290
private _quickOpen: boolean = false;
8391

8492
@Input()
85-
get fixed(): boolean { return this._fixed; }
93+
get fixed(): boolean {
94+
return this._fixed;
95+
}
8696
set fixed(value: boolean) {
8797
this._fixed = coerceBooleanProperty(value);
8898
this._fixed ? this._getHostElement().classList.add('mdc-menu-surface--fixed') :
@@ -92,23 +102,29 @@ export abstract class MdcMenuSurfaceBase extends MDCComponent<MDCMenuSurfaceFoun
92102
private _fixed: boolean = false;
93103

94104
@Input()
95-
get coordinates(): Coordinates { return this._coordinates; }
105+
get coordinates(): Coordinates {
106+
return this._coordinates;
107+
}
96108
set coordinates(value: Coordinates) {
97109
this._coordinates = value;
98110
this._foundation.setAbsolutePosition(value.x, value.y);
99111
}
100-
private _coordinates: Coordinates = { x: 0, y: 0 };
112+
private _coordinates: Coordinates = {x: 0, y: 0};
101113

102114
@Input()
103-
get anchorMargin(): AnchorMargin { return this._anchorMargin; }
115+
get anchorMargin(): AnchorMargin {
116+
return this._anchorMargin;
117+
}
104118
set anchorMargin(value: AnchorMargin) {
105119
this._anchorMargin = value;
106120
this._foundation.setAnchorMargin(this._anchorMargin);
107121
}
108122
private _anchorMargin: AnchorMargin = {};
109123

110124
@Input()
111-
get hoistToBody(): boolean { return this._hoistToBody; }
125+
get hoistToBody(): boolean {
126+
return this._hoistToBody;
127+
}
112128
set hoistToBody(value: boolean) {
113129
this._hoistToBody = coerceBooleanProperty(value);
114130
if (this._hoistToBody) {
@@ -141,56 +157,38 @@ export abstract class MdcMenuSurfaceBase extends MDCComponent<MDCMenuSurfaceFoun
141157
this._registerWindowClickListener();
142158
},
143159
isElementInContainer: (el: Element) => this._getHostElement() === el || this._getHostElement().contains(el),
144-
isRtl: () => {
145-
if (!this.platform.isBrowser) { return false; }
146-
147-
return window.getComputedStyle(this._getHostElement()).getPropertyValue('direction') === 'rtl';
148-
},
149-
setTransformOrigin: (origin: string) => {
150-
if (!this.platform.isBrowser) { return; }
151-
152-
this._getHostElement().style[`${util.getTransformPropertyName(window)}-origin` as any] = origin;
153-
},
160+
isRtl: () => this.platform.isBrowser ?
161+
window.getComputedStyle(this._getHostElement()).getPropertyValue('direction') === 'rtl' : false,
162+
setTransformOrigin: (origin: string) =>
163+
this.platform.isBrowser ?
164+
this._getHostElement().style[`${util.getTransformPropertyName(window)}-origin` as any] = origin : false,
154165
isFocused: () => this.platform.isBrowser ? document.activeElement! === this._getHostElement() : false,
155-
saveFocus: () => {
156-
if (!this.platform.isBrowser) { return; }
157-
this._previousFocus = document.activeElement!;
158-
},
166+
saveFocus: () => this.platform.isBrowser ? this._previousFocus = document.activeElement! : {},
159167
restoreFocus: () => {
160-
if (!this.platform.isBrowser) { return; }
161-
162-
if (this._getHostElement().contains(document.activeElement!)) {
168+
if (!this.platform.isBrowser && this._getHostElement().contains(document.activeElement!)) {
163169
if (this._previousFocus && (<any>this._previousFocus).focus) {
164170
(<any>this._previousFocus).focus();
165171
}
166172
}
167173
},
168-
getInnerDimensions: () => {
169-
return { width: this._getHostElement().offsetWidth, height: this._getHostElement().offsetHeight };
170-
},
171-
getAnchorDimensions: () => {
172-
if (!this.platform.isBrowser || !this.anchorElement) { return null; }
173-
return this._anchorElement!.getBoundingClientRect();
174-
},
175-
getWindowDimensions: () => {
176-
return {
177-
width: this.platform.isBrowser ? window.innerWidth : 0,
178-
height: this.platform.isBrowser ? window.innerHeight : 0
179-
};
180-
},
181-
getBodyDimensions: () => {
182-
return {
183-
width: this.platform.isBrowser ? document.body!.clientWidth : 0,
184-
height: this.platform.isBrowser ? document.body!.clientHeight : 0
185-
};
186-
},
187-
getWindowScroll: () => {
188-
return {
189-
x: this.platform.isBrowser ? window.pageXOffset : 0,
190-
y: this.platform.isBrowser ? window.pageYOffset : 0
191-
};
192-
},
193-
setPosition: (position: { left: number, right: number, top: number, bottom: number }) => {
174+
getInnerDimensions: () =>
175+
({width: this._getHostElement().offsetWidth, height: this._getHostElement().offsetHeight}),
176+
getAnchorDimensions: () =>
177+
this.platform.isBrowser || !this.anchorElement ?
178+
this._anchorElement!.getBoundingClientRect() : {top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0},
179+
getWindowDimensions: () => ({
180+
width: this.platform.isBrowser ? window.innerWidth : 0,
181+
height: this.platform.isBrowser ? window.innerHeight : 0
182+
}),
183+
getBodyDimensions: () => ({
184+
width: this.platform.isBrowser ? document.body!.clientWidth : 0,
185+
height: this.platform.isBrowser ? document.body!.clientHeight : 0
186+
}),
187+
getWindowScroll: () => ({
188+
x: this.platform.isBrowser ? window.pageXOffset : 0,
189+
y: this.platform.isBrowser ? window.pageYOffset : 0
190+
}),
191+
setPosition: (position: {left: number, right: number, top: number, bottom: number}) => {
194192
this._getHostElement().style.left = 'left' in position ? `${position.left}px` : '';
195193
this._getHostElement().style.right = 'right' in position ? `${position.right}px` : '';
196194
this._getHostElement().style.top = 'top' in position ? `${position.top}px` : '';
@@ -206,7 +204,6 @@ export abstract class MdcMenuSurfaceBase extends MDCComponent<MDCMenuSurfaceFoun
206204
public platform: Platform,
207205
@Optional() private _ngZone: NgZone,
208206
public elementRef: ElementRef<HTMLElement>) {
209-
210207
super(elementRef);
211208
}
212209

@@ -233,14 +230,16 @@ export abstract class MdcMenuSurfaceBase extends MDCComponent<MDCMenuSurfaceFoun
233230

234231
protected setOpen(): void {
235232
this._open ? this._foundation.open() : this._foundation.close();
236-
}
233+
}
237234

238235
/**
239236
* Removes the menu-surface from it's current location and appends it to the
240237
* body to overcome any overflow:hidden issues.
241238
*/
242239
protected setHoistToBody(): void {
243-
if (!this.platform.isBrowser) { return; }
240+
if (!this.platform.isBrowser) {
241+
return;
242+
}
244243

245244
const parentEl = this._getHostElement().parentElement;
246245
if (parentEl) {
@@ -256,7 +255,9 @@ export abstract class MdcMenuSurfaceBase extends MDCComponent<MDCMenuSurfaceFoun
256255
}
257256

258257
private _registerWindowClickListener(): void {
259-
if (!this.platform.isBrowser) { return; }
258+
if (!this.platform.isBrowser) {
259+
return;
260+
}
260261

261262
this._windowClickSubscription =
262263
this._ngZone.runOutsideAngular(() =>

packages/menu/menu.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {MdcList, MdcListItem, MdcListItemAction} from '@angular-mdc/web/list';
2121
import {MdcMenuSurfaceBase} from '@angular-mdc/web/menu-surface';
2222

2323
import {closest} from '@material/dom/ponyfill';
24-
import {cssClasses, MDCMenuFoundation} from '@material/menu';
24+
import {cssClasses, strings, DefaultFocusState, MDCMenuFoundation} from '@material/menu';
2525

2626
export class MdcMenuSelectedEvent {
2727
constructor(
@@ -31,6 +31,15 @@ export class MdcMenuSelectedEvent {
3131

3232
let nextUniqueId = 0;
3333

34+
export type MdcMenuFocusState = 'none' | 'list' | 'firstItem' | 'lastItem';
35+
36+
const DEFAULT_FOCUS_STATE_MAP = {
37+
none: DefaultFocusState.NONE,
38+
list: DefaultFocusState.LIST_ROOT,
39+
firstItem: DefaultFocusState.FIRST_ITEM,
40+
lastItem: DefaultFocusState.LAST_ITEM
41+
};
42+
3443
@Directive({
3544
selector: '[mdcMenuSelectionGroup], mdc-menu-selection-group',
3645
host: {'class': 'mdc-menu__selection-group'},
@@ -95,6 +104,18 @@ export class MdcMenu extends MdcMenuSurfaceBase implements AfterContentInit, OnD
95104
}
96105
private _closeSurfaceOnSelection: boolean = true;
97106

107+
@Input()
108+
get defaultFocusState(): MdcMenuFocusState {
109+
return this._defaultFocusState;
110+
}
111+
set defaultFocusState(value: MdcMenuFocusState) {
112+
if (value !== this._defaultFocusState) {
113+
this._defaultFocusState = value;
114+
this._menuFoundation.setDefaultFocusState(DEFAULT_FOCUS_STATE_MAP[this._defaultFocusState]);
115+
}
116+
}
117+
private _defaultFocusState: MdcMenuFocusState = 'list';
118+
98119
@Output() readonly selected: EventEmitter<MdcMenuSelectedEvent> = new EventEmitter<MdcMenuSelectedEvent>();
99120

100121
@ContentChild(MdcList, {static: false}) _list!: MdcList;
@@ -118,8 +139,8 @@ export class MdcMenu extends MdcMenuSurfaceBase implements AfterContentInit, OnD
118139
notifySelected: (evtData: {index: number}) =>
119140
this.selected.emit(new MdcMenuSelectedEvent(evtData.index, this.listItems.toArray()[evtData.index])),
120141
getMenuItemCount: () => this.listItems.toArray().length,
121-
focusItemAtIndex: (index: number) => this._list.getListItemByIndex(index)!.focus(),
122-
focusListRoot: () => this._list.focus(),
142+
focusItemAtIndex: (index: number) => this.listItems.toArray()[index].focus(),
143+
focusListRoot: () => (this.elementRef.nativeElement.querySelector(strings.LIST_SELECTOR) as HTMLElement).focus(),
123144
isSelectableItemAtIndex: (index: number) =>
124145
!!closest(this.listItems.toArray()[index].getListItemElement(), `.${cssClasses.MENU_SELECTION_GROUP}`),
125146
getSelectedSiblingOfItemAtIndex: (index: number) => {
@@ -136,7 +157,8 @@ export class MdcMenu extends MdcMenuSurfaceBase implements AfterContentInit, OnD
136157
destroy(): void,
137158
handleKeydown(evt: KeyboardEvent): void,
138159
handleItemAction(listItem: HTMLElement): void,
139-
handleMenuSurfaceOpened(): void
160+
handleMenuSurfaceOpened(): void,
161+
setDefaultFocusState(focusState: DefaultFocusState): void
140162
} = new MDCMenuFoundation(this._createAdapter());
141163

142164
ngAfterContentInit(): void {
@@ -156,10 +178,6 @@ export class MdcMenu extends MdcMenuSurfaceBase implements AfterContentInit, OnD
156178
this._menuFoundation.destroy();
157179
}
158180

159-
focus(): void {
160-
this._getHostElement().focus();
161-
}
162-
163181
_handleKeydown(evt: KeyboardEvent): void {
164182
this._menuFoundation.handleKeydown(evt);
165183
}

0 commit comments

Comments
 (0)