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

feat(menu): add setSelectedIndex to set selected item in menu selection group #4620

Merged
merged 24 commits into from
May 6, 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
6 changes: 4 additions & 2 deletions packages/mdc-menu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ Method Signature | Description
`setAnchorMargin(Partial<MDCMenuDistance>) => void` | Proxies to the menu surface's `setAnchorMargin(Partial<MDCMenuDistance>)` method.
`setAbsolutePosition(x: number, y: number) => void` | Proxies to the menu surface's `setAbsolutePosition(x: number, y: number)` method.
`setFixedPosition(isFixed: boolean) => void` | Proxies to the menu surface's `setFixedPosition(isFixed: boolean)` method.
`setSelectedIndex(index: number) => void | Sets the list item to the selected state at the specified index.
`hoistMenuToBody() => void` | Proxies to the menu surface's `hoistMenuToBody()` method.
`setIsHoisted(isHoisted: boolean) => void` | Proxies to the menu surface's `setIsHoisted(isHoisted: boolean)` method.
`setAnchorElement(element: Element) => void` | Proxies to the menu surface's `setAnchorElement(element)` method.
Expand All @@ -250,12 +251,12 @@ Method Signature | Description
`elementContainsClass(element: Element, className: string) => boolean` | Returns true if the `element` contains the `className` class.
`closeSurface() => void` | Closes the menu surface.
`getElementIndex(element: Element) => number` | Returns the `index` value of the `element`.
`getParentElement(element: Element) => Element \| null` | Returns the `.parentElement` element of the `element` provided.
`getSelectedElementIndex(element: Element) => number` | Returns the `index` value of the element within the selection group provided, `element` that contains the `mdc-menu-item--selected` class.
`notifySelected(index: number) => void` | Emits a `MDCMenu:selected` event for the element at the `index` specified.
`getMenuItemCount() => number` | Returns the menu item count.
`focusItemAtIndex(index: number)` | Focuses the menu item at given index.
`focusListRoot() => void` | Focuses the list root element.
`getSelectedSiblingOfItemAtIndex(index: number) => number` | Returns selected list item index within the same selection group which is a sibling of item at given `index`.
`isSelectableItemAtIndex(index: number) => boolean` | Returns true if menu item at specified index is contained within an `.mdc-menu__selection-group` element.

### `MDCMenuFoundation`

Expand All @@ -265,6 +266,7 @@ Method Signature | Description
`handleItemAction(listItem: Element) => void` | Event handler for list's action event.
`handleMenuSurfaceOpened() => void` | Event handler for menu surface's opened event.
`setDefaultFocusState(focusState: DefaultFocusState) => void` | Sets default focus state where the menu should focus every time when menu is opened. Focuses the list root (`DefaultFocusState.LIST_ROOT`) element by default.
`setSelectedIndex(index: number) => void` | Selects the list item at given `index`.

### Events

Expand Down
23 changes: 13 additions & 10 deletions packages/mdc-menu/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,6 @@ export interface MDCMenuAdapter {
*/
getElementIndex(element: Element): number;

/**
* @return The parentElement of the provided element.
*/
getParentElement(element: Element): Element | null;

/**
* @return The element within the selectionGroup containing the selected element class.
*/
getSelectedElementIndex(selectionGroup: Element): number;

/**
* Emit an event when a menu item is selected.
*/
Expand All @@ -91,4 +81,17 @@ export interface MDCMenuAdapter {

/** Focuses the list root element. */
focusListRoot(): void;

/**
* @return Returns selected list item index within the same selection group which is
* a sibling of item at given `index`.
* @param index Index of the menu item with possible selected sibling.
*/
getSelectedSiblingOfItemAtIndex(index: number): number;

/**
* @return Returns true if item at specified index is contained within an `.mdc-menu__selection-group` element.
* @param index Index of the selectable menu item.
*/
isSelectableItemAtIndex(index: number): boolean;
}
20 changes: 15 additions & 5 deletions packages/mdc-menu/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import {MDCComponent} from '@material/base/component';
import {CustomEventListener, SpecificEventListener} from '@material/base/types';
import {closest} from '@material/dom/ponyfill';
import {MDCList, MDCListFactory} from '@material/list/component';
import {MDCListFoundation} from '@material/list/foundation';
import {MDCListActionEvent} from '@material/list/types';
Expand Down Expand Up @@ -143,6 +144,14 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {
this.menuSurface_.setAnchorMargin(margin);
}

/**
* Sets the list item as the selected row at the specified index.
* @param index Index of list item within menu.
*/
setSelectedIndex(index: number) {
moog16 marked this conversation as resolved.
Show resolved Hide resolved
abhiomkar marked this conversation as resolved.
Show resolved Hide resolved
this.foundation_.setSelectedIndex(index);
}

/**
* @return The item within the menu at the index specified.
*/
Expand Down Expand Up @@ -203,18 +212,19 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {
elementContainsClass: (element, className) => element.classList.contains(className),
closeSurface: () => this.open = false,
getElementIndex: (element) => this.items.indexOf(element),
getParentElement: (element) => element.parentElement,
getSelectedElementIndex: (selectionGroup) => {
const selectedListItem = selectionGroup.querySelector(`.${cssClasses.MENU_SELECTED_LIST_ITEM}`);
return selectedListItem ? this.items.indexOf(selectedListItem) : -1;
},
notifySelected: (evtData) => this.emit<MDCMenuItemComponentEventDetail>(strings.SELECTED_EVENT, {
index: evtData.index,
item: this.items[evtData.index],
}),
getMenuItemCount: () => this.items.length,
focusItemAtIndex: (index) => (this.items[index] as HTMLElement).focus(),
focusListRoot: () => (this.root_.querySelector(strings.LIST_SELECTOR) as HTMLElement).focus(),
isSelectableItemAtIndex: (index) => !!closest(this.items[index], `.${cssClasses.MENU_SELECTION_GROUP}`),
getSelectedSiblingOfItemAtIndex: (index) => {
const selectionGroupEl = closest(this.items[index], `.${cssClasses.MENU_SELECTION_GROUP}`) as HTMLElement;
const selectedItemEl = selectionGroupEl.querySelector(`.${cssClasses.MENU_SELECTED_LIST_ITEM}`);
return selectedItemEl ? this.items.indexOf(selectedItemEl) : -1;
},
};
// tslint:enable:object-literal-sort-keys
return new MDCMenuFoundation(adapter);
Expand Down
57 changes: 23 additions & 34 deletions packages/mdc-menu/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
*/

import {MDCFoundation} from '@material/base/foundation';
import {MDCListFoundation} from '@material/list/foundation';
import {MDCMenuSurfaceFoundation} from '@material/menu-surface/foundation';
import {MDCMenuAdapter} from './adapter';
import {cssClasses, DefaultFocusState, numbers, strings} from './constants';
Expand Down Expand Up @@ -56,12 +55,12 @@ export class MDCMenuFoundation extends MDCFoundation<MDCMenuAdapter> {
elementContainsClass: () => false,
closeSurface: () => undefined,
getElementIndex: () => -1,
getParentElement: () => null,
getSelectedElementIndex: () => -1,
notifySelected: () => undefined,
getMenuItemCount: () => 0,
focusItemAtIndex: () => undefined,
focusListRoot: () => undefined,
getSelectedSiblingOfItemAtIndex: () => -1,
isSelectableItemAtIndex: () => false,
};
// tslint:enable:object-literal-sort-keys
}
Expand Down Expand Up @@ -98,9 +97,8 @@ export class MDCMenuFoundation extends MDCFoundation<MDCMenuAdapter> {

// Wait for the menu to close before adding/removing classes that affect styles.
this.closeAnimationEndTimerId_ = setTimeout(() => {
const selectionGroup = this.getSelectionGroup_(listItem);
if (selectionGroup) {
this.handleSelectionGroup_(selectionGroup, index);
if (this.adapter_.isSelectableItemAtIndex(index)) {
this.setSelectedIndex(index);
}
}, MDCMenuSurfaceFoundation.numbers.TRANSITION_CLOSE_DURATION);
}
Expand Down Expand Up @@ -132,41 +130,32 @@ export class MDCMenuFoundation extends MDCFoundation<MDCMenuAdapter> {
}

/**
* Handles toggling the selected classes in a selection group when a selection is made.
* Selects the list item at `index` within the menu.
* @param index Index of list item within the menu.
*/
private handleSelectionGroup_(selectionGroup: Element, index: number) {
// De-select the previous selection in this group.
const selectedIndex = this.adapter_.getSelectedElementIndex(selectionGroup);
if (selectedIndex >= 0) {
this.adapter_.removeAttributeFromElementAtIndex(selectedIndex, strings.ARIA_SELECTED_ATTR);
this.adapter_.removeClassFromElementAtIndex(selectedIndex, cssClasses.MENU_SELECTED_LIST_ITEM);
setSelectedIndex(index: number) {
this.validatedIndex_(index);

if (!this.adapter_.isSelectableItemAtIndex(index)) {
throw new Error('MDCMenuFoundation: No selection group at specified index.');
}
// Select the new list item in this group.
this.adapter_.addClassToElementAtIndex(index, cssClasses.MENU_SELECTED_LIST_ITEM);
this.adapter_.addAttributeToElementAtIndex(index, strings.ARIA_SELECTED_ATTR, 'true');
}

/**
* Returns the parent selection group of an element if one exists.
*/
private getSelectionGroup_(listItem: Element): Element | null {
let parent = this.adapter_.getParentElement(listItem);
if (!parent) {
return null;
const prevSelectedIndex = this.adapter_.getSelectedSiblingOfItemAtIndex(index);
if (prevSelectedIndex >= 0) {
this.adapter_.removeAttributeFromElementAtIndex(prevSelectedIndex, strings.ARIA_SELECTED_ATTR);
this.adapter_.removeClassFromElementAtIndex(prevSelectedIndex, cssClasses.MENU_SELECTED_LIST_ITEM);
}

let isGroup = this.adapter_.elementContainsClass(parent, cssClasses.MENU_SELECTION_GROUP);
this.adapter_.addClassToElementAtIndex(index, cssClasses.MENU_SELECTED_LIST_ITEM);
this.adapter_.addAttributeToElementAtIndex(index, strings.ARIA_SELECTED_ATTR, 'true');
}

// Iterate through ancestors until we find the group or get to the list.
while (!isGroup && parent && !this.adapter_.elementContainsClass(parent, MDCListFoundation.cssClasses.ROOT)) {
parent = this.adapter_.getParentElement(parent);
isGroup = parent ? this.adapter_.elementContainsClass(parent, cssClasses.MENU_SELECTION_GROUP) : false;
}
private validatedIndex_(index: number): void {
const menuSize = this.adapter_.getMenuItemCount();
const isIndexInRange = index >= 0 && index < menuSize;

if (isGroup) {
return parent;
} else {
return null;
if (!isIndexInRange) {
throw new Error('MDCMenuFoundation: No list item at specified index.');
}
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/mdc-menu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"dependencies": {
"@material/base": "^1.0.0",
"@material/dom": "^1.1.0",
"@material/feature-targeting": "^0.44.1",
"@material/list": "^2.0.0",
"@material/menu-surface": "^1.1.1",
Expand Down
8 changes: 8 additions & 0 deletions test/screenshot/golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,14 @@
"desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/travis/2019/03/15/18_35_08_782/spec/mdc-menu/classes/menu-selection-group.html.windows_ie_11.png"
}
},
"spec/mdc-menu/classes/multiple-menu-selection-group.html": {
"public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/travis/2019/04/23/22_44_56_133/spec/mdc-menu/classes/multiple-menu-selection-group.html?utm_source=golden_json",
"screenshots": {
"desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/travis/2019/04/23/22_44_56_133/spec/mdc-menu/classes/multiple-menu-selection-group.html.windows_chrome_73.png",
"desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/travis/2019/04/23/22_44_56_133/spec/mdc-menu/classes/multiple-menu-selection-group.html.windows_firefox_65.png",
"desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/travis/2019/04/23/22_44_56_133/spec/mdc-menu/classes/multiple-menu-selection-group.html.windows_ie_11.png"
}
},
"spec/mdc-menu/issues/4025.html": {
"public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/travis/2019/01/18/07_50_32_667/spec/mdc-menu/issues/4025.html?utm_source=golden_json",
"screenshots": {
Expand Down
Loading