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

Commit

Permalink
feat(menu): Support default focus state (#2004)
Browse files Browse the repository at this point in the history
- Adds `defaultFocusState: 'none' | 'list' | 'firstItem' | 'lastItem'` for `MdcMenu`
- Minor code refactoring of `MdcMenuSurfaceBase`
- Update documentation
- Include unit test

closes #1998
  • Loading branch information
trimox authored Sep 13, 2019
1 parent 5408da3 commit 926c076
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 93 deletions.
10 changes: 2 additions & 8 deletions demos/src/app/components/menu-demo/api.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,9 @@ <h4 mdcSubtitle2>Properties</h4>
<td>closeSurfaceOnSelection: boolean</td>
<td>Sets whether the menu surface should close after item selection. Default is true</td>
</tr>
</tbody>
</table>

<h4 mdcSubtitle2>Methods</h4>
<table>
<tbody>
<tr>
<td>focus()</td>
<td>Set focus to the menu.</td>
<td>defaultFocusState: 'none' | 'list' | 'firstItem' | 'lastItem'</td>
<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>
</tr>
</tbody>
</table>
Expand Down
7 changes: 7 additions & 0 deletions demos/src/app/components/menu-demo/examples.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ <h3 class="demo-content__headline">Anchor Margin</h3>
<mdc-checkbox #closeSurfaceOnSelection [checked]="closeSurfaceOnSelection"></mdc-checkbox>
<label>Close Surface on Selection</label>
</mdc-form-field>
<mdc-select #defaultFocusState placeholder="Default Focus State">
<option value="none">None</option>
<option value="list">List Root</option>
<option value="firstItem">First Item</option>
<option value="lastItem">Last Item</option>
</mdc-select>
</div>
</div>
</div>
Expand All @@ -57,6 +63,7 @@ <h3 class="demo-content__headline">Anchor Margin</h3>
[anchorElement]="demoAnchor"
[anchorCorner]="menuSurfaceAnchorCorner.value"
[quickOpen]="quickOpen.checked"
[defaultFocusState]="defaultFocusState.value"
[fixed]="fixed.checked"
[wrapFocus]="wrapFocus.checked"
[closeSurfaceOnSelection]="closeSurfaceOnSelection.checked"
Expand Down
40 changes: 20 additions & 20 deletions demos/src/app/components/menu-demo/menu-demo.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import {Component, OnInit, ViewChild} from '@angular/core';

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

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

Expand All @@ -23,36 +23,36 @@ export class MenuDemo implements OnInit {
}
}

@Component({ templateUrl: './api.html' })
export class Api { }
@Component({templateUrl: './api.html'})
export class Api {}

@Component({ templateUrl: './sass.html' })
export class Sass { }
@Component({templateUrl: './sass.html'})
export class Sass {}

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

fruits = [
{ label: 'Passionfruit' },
{ label: 'Orange' },
{ label: 'Guava' },
{ label: 'Pitaya' },
{ label: null }, // null label sets a mdc-list-divider
{ label: 'Pinaeapple' },
{ label: 'Mango' },
{ label: 'Papaya' },
{ label: 'Lychee' }
{label: 'Passionfruit'},
{label: 'Orange'},
{label: 'Guava'},
{label: 'Pitaya'},
{label: null}, // null label sets a mdc-list-divider
{label: 'Pinaeapple'},
{label: 'Mango'},
{label: 'Papaya'},
{label: 'Lychee'}
];

lastSelection: number;

onMenuSelect(event: { index: number, item: MdcListItem }) {
onMenuSelect(event: {index: number, item: MdcListItem}) {
this.lastSelection = event.index;
}

addFruit(): void {
this.fruits.push({ label: 'New fruit item' });
this.fruits.push({label: 'New fruit item'});
}

exampleMenu = {
Expand Down
113 changes: 57 additions & 56 deletions packages/menu-surface/menu-surface-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export abstract class MdcMenuSurfaceBase extends MDCComponent<MDCMenuSurfaceFoun
private _previousFocus: Element | null = null;

@Input()
get open(): boolean { return this._open; }
get open(): boolean {
return this._open;
}
set open(value: boolean) {
const newValue = coerceBooleanProperty(value);
if (newValue !== this._open) {
Expand All @@ -59,30 +61,38 @@ export abstract class MdcMenuSurfaceBase extends MDCComponent<MDCMenuSurfaceFoun
private _open: boolean = false;

@Input()
get anchorElement(): HTMLElement | null { return this._anchorElement; }
get anchorElement(): HTMLElement | null {
return this._anchorElement;
}
set anchorElement(element: HTMLElement | null) {
this._anchorElement = element;
}
private _anchorElement: HTMLElement | null = null;

@Input()
get anchorCorner(): AnchorCorner { return this._anchorCorner; }
get anchorCorner(): AnchorCorner {
return this._anchorCorner;
}
set anchorCorner(value: AnchorCorner) {
this._anchorCorner = value || 'topStart';
this._foundation.setAnchorCorner(ANCHOR_CORNER_MAP[this._anchorCorner]);
}
private _anchorCorner: AnchorCorner = 'topStart';

@Input()
get quickOpen(): boolean { return this._quickOpen; }
get quickOpen(): boolean {
return this._quickOpen;
}
set quickOpen(value: boolean) {
this._quickOpen = coerceBooleanProperty(value);
this._foundation.setQuickOpen(this._quickOpen);
}
private _quickOpen: boolean = false;

@Input()
get fixed(): boolean { return this._fixed; }
get fixed(): boolean {
return this._fixed;
}
set fixed(value: boolean) {
this._fixed = coerceBooleanProperty(value);
this._fixed ? this._getHostElement().classList.add('mdc-menu-surface--fixed') :
Expand All @@ -92,23 +102,29 @@ export abstract class MdcMenuSurfaceBase extends MDCComponent<MDCMenuSurfaceFoun
private _fixed: boolean = false;

@Input()
get coordinates(): Coordinates { return this._coordinates; }
get coordinates(): Coordinates {
return this._coordinates;
}
set coordinates(value: Coordinates) {
this._coordinates = value;
this._foundation.setAbsolutePosition(value.x, value.y);
}
private _coordinates: Coordinates = { x: 0, y: 0 };
private _coordinates: Coordinates = {x: 0, y: 0};

@Input()
get anchorMargin(): AnchorMargin { return this._anchorMargin; }
get anchorMargin(): AnchorMargin {
return this._anchorMargin;
}
set anchorMargin(value: AnchorMargin) {
this._anchorMargin = value;
this._foundation.setAnchorMargin(this._anchorMargin);
}
private _anchorMargin: AnchorMargin = {};

@Input()
get hoistToBody(): boolean { return this._hoistToBody; }
get hoistToBody(): boolean {
return this._hoistToBody;
}
set hoistToBody(value: boolean) {
this._hoistToBody = coerceBooleanProperty(value);
if (this._hoistToBody) {
Expand Down Expand Up @@ -141,56 +157,38 @@ export abstract class MdcMenuSurfaceBase extends MDCComponent<MDCMenuSurfaceFoun
this._registerWindowClickListener();
},
isElementInContainer: (el: Element) => this._getHostElement() === el || this._getHostElement().contains(el),
isRtl: () => {
if (!this.platform.isBrowser) { return false; }

return window.getComputedStyle(this._getHostElement()).getPropertyValue('direction') === 'rtl';
},
setTransformOrigin: (origin: string) => {
if (!this.platform.isBrowser) { return; }

this._getHostElement().style[`${util.getTransformPropertyName(window)}-origin` as any] = origin;
},
isRtl: () => this.platform.isBrowser ?
window.getComputedStyle(this._getHostElement()).getPropertyValue('direction') === 'rtl' : false,
setTransformOrigin: (origin: string) =>
this.platform.isBrowser ?
this._getHostElement().style[`${util.getTransformPropertyName(window)}-origin` as any] = origin : false,
isFocused: () => this.platform.isBrowser ? document.activeElement! === this._getHostElement() : false,
saveFocus: () => {
if (!this.platform.isBrowser) { return; }
this._previousFocus = document.activeElement!;
},
saveFocus: () => this.platform.isBrowser ? this._previousFocus = document.activeElement! : {},
restoreFocus: () => {
if (!this.platform.isBrowser) { return; }

if (this._getHostElement().contains(document.activeElement!)) {
if (!this.platform.isBrowser && this._getHostElement().contains(document.activeElement!)) {
if (this._previousFocus && (<any>this._previousFocus).focus) {
(<any>this._previousFocus).focus();
}
}
},
getInnerDimensions: () => {
return { width: this._getHostElement().offsetWidth, height: this._getHostElement().offsetHeight };
},
getAnchorDimensions: () => {
if (!this.platform.isBrowser || !this.anchorElement) { return null; }
return this._anchorElement!.getBoundingClientRect();
},
getWindowDimensions: () => {
return {
width: this.platform.isBrowser ? window.innerWidth : 0,
height: this.platform.isBrowser ? window.innerHeight : 0
};
},
getBodyDimensions: () => {
return {
width: this.platform.isBrowser ? document.body!.clientWidth : 0,
height: this.platform.isBrowser ? document.body!.clientHeight : 0
};
},
getWindowScroll: () => {
return {
x: this.platform.isBrowser ? window.pageXOffset : 0,
y: this.platform.isBrowser ? window.pageYOffset : 0
};
},
setPosition: (position: { left: number, right: number, top: number, bottom: number }) => {
getInnerDimensions: () =>
({width: this._getHostElement().offsetWidth, height: this._getHostElement().offsetHeight}),
getAnchorDimensions: () =>
this.platform.isBrowser || !this.anchorElement ?
this._anchorElement!.getBoundingClientRect() : {top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0},
getWindowDimensions: () => ({
width: this.platform.isBrowser ? window.innerWidth : 0,
height: this.platform.isBrowser ? window.innerHeight : 0
}),
getBodyDimensions: () => ({
width: this.platform.isBrowser ? document.body!.clientWidth : 0,
height: this.platform.isBrowser ? document.body!.clientHeight : 0
}),
getWindowScroll: () => ({
x: this.platform.isBrowser ? window.pageXOffset : 0,
y: this.platform.isBrowser ? window.pageYOffset : 0
}),
setPosition: (position: {left: number, right: number, top: number, bottom: number}) => {
this._getHostElement().style.left = 'left' in position ? `${position.left}px` : '';
this._getHostElement().style.right = 'right' in position ? `${position.right}px` : '';
this._getHostElement().style.top = 'top' in position ? `${position.top}px` : '';
Expand All @@ -206,7 +204,6 @@ export abstract class MdcMenuSurfaceBase extends MDCComponent<MDCMenuSurfaceFoun
public platform: Platform,
@Optional() private _ngZone: NgZone,
public elementRef: ElementRef<HTMLElement>) {

super(elementRef);
}

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

protected setOpen(): void {
this._open ? this._foundation.open() : this._foundation.close();
}
}

/**
* Removes the menu-surface from it's current location and appends it to the
* body to overcome any overflow:hidden issues.
*/
protected setHoistToBody(): void {
if (!this.platform.isBrowser) { return; }
if (!this.platform.isBrowser) {
return;
}

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

private _registerWindowClickListener(): void {
if (!this.platform.isBrowser) { return; }
if (!this.platform.isBrowser) {
return;
}

this._windowClickSubscription =
this._ngZone.runOutsideAngular(() =>
Expand Down
34 changes: 26 additions & 8 deletions packages/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {MdcList, MdcListItem, MdcListItemAction} from '@angular-mdc/web/list';
import {MdcMenuSurfaceBase} from '@angular-mdc/web/menu-surface';

import {closest} from '@material/dom/ponyfill';
import {cssClasses, MDCMenuFoundation} from '@material/menu';
import {cssClasses, strings, DefaultFocusState, MDCMenuFoundation} from '@material/menu';

export class MdcMenuSelectedEvent {
constructor(
Expand All @@ -31,6 +31,15 @@ export class MdcMenuSelectedEvent {

let nextUniqueId = 0;

export type MdcMenuFocusState = 'none' | 'list' | 'firstItem' | 'lastItem';

const DEFAULT_FOCUS_STATE_MAP = {
none: DefaultFocusState.NONE,
list: DefaultFocusState.LIST_ROOT,
firstItem: DefaultFocusState.FIRST_ITEM,
lastItem: DefaultFocusState.LAST_ITEM
};

@Directive({
selector: '[mdcMenuSelectionGroup], mdc-menu-selection-group',
host: {'class': 'mdc-menu__selection-group'},
Expand Down Expand Up @@ -95,6 +104,18 @@ export class MdcMenu extends MdcMenuSurfaceBase implements AfterContentInit, OnD
}
private _closeSurfaceOnSelection: boolean = true;

@Input()
get defaultFocusState(): MdcMenuFocusState {
return this._defaultFocusState;
}
set defaultFocusState(value: MdcMenuFocusState) {
if (value !== this._defaultFocusState) {
this._defaultFocusState = value;
this._menuFoundation.setDefaultFocusState(DEFAULT_FOCUS_STATE_MAP[this._defaultFocusState]);
}
}
private _defaultFocusState: MdcMenuFocusState = 'list';

@Output() readonly selected: EventEmitter<MdcMenuSelectedEvent> = new EventEmitter<MdcMenuSelectedEvent>();

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

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

focus(): void {
this._getHostElement().focus();
}

_handleKeydown(evt: KeyboardEvent): void {
this._menuFoundation.handleKeydown(evt);
}
Expand Down
Loading

0 comments on commit 926c076

Please sign in to comment.