Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

feat(menu): Added new API to manually set focus when menu is opened #4468

Merged
merged 11 commits into from
Mar 18, 2019
12 changes: 12 additions & 0 deletions packages/mdc-menu/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,16 @@ export interface MDCMenuAdapter {
* Emit an event when a menu item is selected.
*/
notifySelected(evtData: MDCMenuItemEventDetail): void;

/** Returns the menu item count. */
getMenuItemCount(): number;

/** Focuses the menu item at given index. */
focusItemAtIndex(index: number): void;

/** Returns true if menu root element is on focus. */
isFocused(): boolean;

/** Focuses the menu root element. */
focus(): void;
}
27 changes: 16 additions & 11 deletions packages/mdc-menu/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {

private handleKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM()
private handleItemAction_!: CustomEventListener<MDCListActionEvent>; // assigned in initialSyncWithDOM()
private afterOpenedCallback_!: EventListener; // assigned in initialSyncWithDOM()
private handleMenuSurfaceOpened_!: EventListener; // assigned in initialSyncWithDOM()

initialize(
menuSurfaceFactory: MDCMenuSurfaceFactory = (el) => new MDCMenuSurface(el),
Expand All @@ -69,9 +69,9 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {

this.handleKeydown_ = (evt) => this.foundation_.handleKeydown(evt);
this.handleItemAction_ = (evt) => this.foundation_.handleItemAction(this.items[evt.detail.index]);
this.afterOpenedCallback_ = () => this.handleAfterOpened_();
this.handleMenuSurfaceOpened_ = () => this.foundation_.handleMenuSurfaceOpened();

this.menuSurface_.listen(MDCMenuSurfaceFoundation.strings.OPENED_EVENT, this.afterOpenedCallback_);
this.menuSurface_.listen(MDCMenuSurfaceFoundation.strings.OPENED_EVENT, this.handleMenuSurfaceOpened_);
this.listen('keydown', this.handleKeydown_);
this.listen(MDCListFoundation.strings.ACTION_EVENT, this.handleItemAction_);
}
Expand All @@ -82,7 +82,7 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {
}

this.menuSurface_.destroy();
this.menuSurface_.unlisten(MDCMenuSurfaceFoundation.strings.OPENED_EVENT, this.afterOpenedCallback_);
this.menuSurface_.unlisten(MDCMenuSurfaceFoundation.strings.OPENED_EVENT, this.handleMenuSurfaceOpened_);
this.unlisten('keydown', this.handleKeydown_);
this.unlisten(MDCListFoundation.strings.ACTION_EVENT, this.handleItemAction_);
super.destroy();
Expand Down Expand Up @@ -119,6 +119,14 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {
this.menuSurface_.quickOpen = quickOpen;
}

/**
* Sets focus item index where the menu should focus on open. Focuses the
* menu root element by default.
*/
setFocusItemIndex(index: number) {
this.foundation_.setFocusItemIndex(index);
}

/**
* @param corner Default anchor corner alignment of top-left menu corner.
*/
Expand Down Expand Up @@ -166,13 +174,6 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {
this.menuSurface_.anchorElement = element;
}

handleAfterOpened_() {
const list = this.items;
if (list.length > 0) {
(list[0] as HTMLElement).focus();
}
}

getDefaultFoundation() {
// DO NOT INLINE this variable. For backward compatibility, foundations take a Partial<MDCFooAdapter>.
// To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable.
Expand Down Expand Up @@ -206,6 +207,10 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {
index: evtData.index,
item: this.items[evtData.index],
}),
getMenuItemCount: () => this.items.length,
focusItemAtIndex: (index) => (this.items[index] as HTMLElement).focus(),
isFocused: () => document.activeElement === this.root_,
focus: () => (this.root_ as HTMLElement).focus(),
};
// tslint:enable:object-literal-sort-keys
return new MDCMenuFoundation(adapter);
Expand Down
37 changes: 37 additions & 0 deletions packages/mdc-menu/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class MDCMenuFoundation extends MDCFoundation<MDCMenuAdapter> {
}

private closeAnimationEndTimerId_ = 0;
private focusItemIndex_ = -1;

/**
* @see {@link MDCMenuAdapter} for typing information on parameters and return types.
Expand All @@ -54,6 +55,10 @@ export class MDCMenuFoundation extends MDCFoundation<MDCMenuAdapter> {
getParentElement: () => null,
getSelectedElementIndex: () => -1,
notifySelected: () => undefined,
getMenuItemCount: () => 0,
focusItemAtIndex: () => undefined,
isFocused: () => false,
focus: () => undefined,
};
// tslint:enable:object-literal-sort-keys
}
Expand All @@ -77,6 +82,17 @@ export class MDCMenuFoundation extends MDCFoundation<MDCMenuAdapter> {
if (isTab) {
this.adapter_.closeSurface();
}

const arrowUp = evt.key === 'ArrowUp' || evt.keyCode === 38;
const arrowDown = evt.key === 'ArrowDown' || evt.keyCode === 40;

if (!this.adapter_.isFocused()) return;

if (arrowUp || arrowDown) {
evt.preventDefault();
const focusItemIndex = arrowUp ? this.adapter_.getMenuItemCount() - 1 : 0;
this.focusItemAtIndex_(focusItemIndex);
}
}

handleItemAction(listItem: Element) {
Expand All @@ -97,6 +113,27 @@ export class MDCMenuFoundation extends MDCFoundation<MDCMenuAdapter> {
}, MDCMenuSurfaceFoundation.numbers.TRANSITION_CLOSE_DURATION);
}

handleMenuSurfaceOpened() {
this.focusItemAtIndex_(this.focusItemIndex_);
}

/**
* Sets the focus item index where the menu should focus on open. Focuses
* the menu root element by default.
*/
setFocusItemIndex(index: number) {
const lastItemIndex = this.adapter_.getMenuItemCount() - 1;
this.focusItemIndex_ = Math.min(lastItemIndex, index);
}

private focusItemAtIndex_(index: number) {
if (index < 0) {
this.adapter_.focus();
} else {
this.adapter_.focusItemAtIndex(index);
}
}

/**
* Handles toggling the selected classes in a selection group when a selection is made.
*/
Expand Down