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

feat(menu): Support default focus state #2004

Merged
merged 3 commits into from
Sep 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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