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 1 commit
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
7 changes: 3 additions & 4 deletions packages/mdc-menu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +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, selectionGroup: number) => void | Sets the selected index of the specified selection group. Defaults to the first selection group.
`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 @@ -252,8 +252,7 @@ Method Signature | Description
`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.
`getSelectionGroupAtIndex() => Element[]` | Returns the selection group at specified index.
`getListItemIndexOfSelectionGroup(index: number, selectionGroup: Element) => number` | Returns the index of list item within menu selection group, relative to the entire .mdc-list element.
`getListItemByIndex() => Element` | Returns the list item at the specified index.
`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.
Expand All @@ -268,7 +267,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, selectionGroupIndex: number) => void` | Selects the list item at `index` witnin the specified selection group.
`setSelectedIndex(index: number) => void` | Selects the list item at `index` witnin the menu.
moog16 marked this conversation as resolved.
Show resolved Hide resolved

### Events

Expand Down
14 changes: 3 additions & 11 deletions packages/mdc-menu/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,10 @@ export interface MDCMenuAdapter {
getElementIndex(element: Element): number;

/**
* @return selection group at specified index.
* @param selectionGroupIndex Index of selection group within the menu.
* @return List item element.
* @param index Index of list item within menu.
*/
getSelectionGroupAtIndex(selectionGroupIndex: number): Element | null;

/**
* @return Index of list item within menu selection group, relative to the entire .mdc-list element.
* @param index Index of of list item within the selection group.
* @param selectionGroup .mdc-menu__selection-group Element.
*/
getListItemIndexOfSelectionGroup(index: number, selectionGroup: Element): number;

getListItemByIndex(index: number): Element | null;
/**
* @return The parentElement of the provided element.
*/
Expand Down
18 changes: 4 additions & 14 deletions packages/mdc-menu/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

import {MDCComponent} from '@material/base/component';
import {CustomEventListener, SpecificEventListener} from '@material/base/types';
import {cssClasses as listCssClasses} from '@material/list/constants';
import {MDCList, MDCListActionEvent, MDCListFactory, MDCListFoundation} from '@material/list/index';
import {MDCMenuSurfaceFoundation} from '@material/menu-surface/foundation';
import {Corner, MDCMenuSurface, MDCMenuSurfaceFactory} from '@material/menu-surface/index';
Expand Down Expand Up @@ -141,8 +140,8 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {
this.menuSurface_.setAnchorMargin(margin);
}

setSelectedIndex(index: number, selectionGroup = 0) {
this.foundation_.setSelectedIndex(index, selectionGroup);
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);
}

/**
Expand Down Expand Up @@ -210,17 +209,8 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {
const selectedListItem = selectionGroup.querySelector(`.${cssClasses.MENU_SELECTED_LIST_ITEM}`);
return selectedListItem ? this.items.indexOf(selectedListItem) : -1;
},
getSelectionGroupAtIndex: (selectionGroupIndex) => {
return [].slice.call(this.root_.querySelectorAll(`.${cssClasses.MENU_SELECTION_GROUP}`))[selectionGroupIndex];
},
getListItemIndexOfSelectionGroup: (index, selectionGroup) => {
const listItemsInSelectionGroup
= [].slice.call(selectionGroup.querySelectorAll(`.${listCssClasses.LIST_ITEM_CLASS}`));
if (listItemsInSelectionGroup.length === 0) {
return -1;
}
const listItem = listItemsInSelectionGroup[index];
return this.items.indexOf(listItem);
getListItemByIndex: (index: number) => {
moog16 marked this conversation as resolved.
Show resolved Hide resolved
return this.items[index];
},
notifySelected: (evtData) => this.emit<MDCMenuItemComponentEventDetail>(strings.SELECTED_EVENT, {
index: evtData.index,
Expand Down
19 changes: 10 additions & 9 deletions packages/mdc-menu/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,8 @@ export class MDCMenuFoundation extends MDCFoundation<MDCMenuAdapter> {
closeSurface: () => undefined,
getElementIndex: () => -1,
getParentElement: () => null,
getListItemByIndex: () => null,
getSelectedElementIndex: () => -1,
getSelectionGroupAtIndex: () => null,
getListItemIndexOfSelectionGroup: () => -1,
notifySelected: () => undefined,
getMenuItemCount: () => 0,
focusItemAtIndex: () => undefined,
Expand Down Expand Up @@ -134,12 +133,15 @@ export class MDCMenuFoundation extends MDCFoundation<MDCMenuAdapter> {
/**
* Selects the list item at `index` witnin the specified selection group.
moog16 marked this conversation as resolved.
Show resolved Hide resolved
* @param index Index of list item within the selection group.
* @param selectionGroupIndex Index of selection group within the menu.
*/
setSelectedIndex(index: number, selectionGroupIndex = 0) {
const selectionGroup = this.adapter_.getSelectionGroupAtIndex(selectionGroupIndex);
setSelectedIndex(index: number) {
const listItem = this.adapter_.getListItemByIndex(index);
moog16 marked this conversation as resolved.
Show resolved Hide resolved
if (!listItem) {
moog16 marked this conversation as resolved.
Show resolved Hide resolved
throw new Error('No list item at specified index.');
moog16 marked this conversation as resolved.
Show resolved Hide resolved
}
const selectionGroup = this.getSelectionGroup_(listItem);
if (!selectionGroup) {
throw new Error('No selection group at specified index');
throw new Error('No selection group at specified index.');
}
this.handleSelectionGroup_(selectionGroup, index);
}
Expand All @@ -158,9 +160,8 @@ export class MDCMenuFoundation extends MDCFoundation<MDCMenuAdapter> {
this.adapter_.removeClassFromElementAtIndex(selectedIndex, cssClasses.MENU_SELECTED_LIST_ITEM);
}
// Select the new list item in this group.
const normalizedIndex = this.adapter_.getListItemIndexOfSelectionGroup(index, selectionGroup);
this.adapter_.addClassToElementAtIndex(normalizedIndex, cssClasses.MENU_SELECTED_LIST_ITEM);
this.adapter_.addAttributeToElementAtIndex(normalizedIndex, strings.ARIA_SELECTED_ATTR, 'true');
this.adapter_.addClassToElementAtIndex(index, cssClasses.MENU_SELECTED_LIST_ITEM);
this.adapter_.addAttributeToElementAtIndex(index, strings.ARIA_SELECTED_ATTR, 'true');
}

/**
Expand Down
4 changes: 2 additions & 2 deletions test/screenshot/spec/mdc-menu/fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ window.mdc.testFixture.fontsLoaded.then(() => {
}
});
if (multipleSelectionGroupMenuEl) {
menu.setSelectedIndex(2, 0);
menu.setSelectedIndex(1, 1);
menu.setSelectedIndex(3);
menu.setSelectedIndex(5);
}

window.mdc.testFixture.notifyDomReady();
Expand Down
32 changes: 6 additions & 26 deletions test/unit/mdc-menu/mdc-menu.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ import {DefaultFocusState} from '../../../packages/mdc-menu/constants';
import {MDCListFoundation} from '../../../packages/mdc-list/index';
import {MDCMenuSurfaceFoundation} from '../../../packages/mdc-menu-surface/foundation';

const {cssClasses} = MDCMenuFoundation;

function getFixture(open) {
return bel`
<div class="mdc-menu mdc-menu-surface ${open ? 'mdc-menu-surface--open' : ''}">
Expand Down Expand Up @@ -240,8 +238,8 @@ test('setAnchorMargin', () => {

test('setSelectedIndex', () => {
moog16 marked this conversation as resolved.
Show resolved Hide resolved
const {component, mockFoundation} = setupTestWithMock({fixture: getFixtureWithMultipleSelectionGroups});
component.setSelectedIndex(1, 1);
td.verify(mockFoundation.setSelectedIndex(1, 1));
component.setSelectedIndex(1);
td.verify(mockFoundation.setSelectedIndex(1));
});

test('setQuickOpen', () => {
Expand Down Expand Up @@ -468,28 +466,10 @@ test('adapter#getMenuItemCount returns the menu item count', () => {
assert.equal(component.getDefaultFoundation().adapter_.getMenuItemCount(), component.items.length);
});

test('adapter#getSelectionGroupAtIndex return first selection group', () => {
const {root, component} = setupTest(false, getFixtureWithMultipleSelectionGroups);
const firstSelectionGroup = root.querySelectorAll('.mdc-menu__selection-group')[0];
assert.equal(component.getDefaultFoundation().adapter_.getSelectionGroupAtIndex(0), firstSelectionGroup);
});

test('adapter#getSelectionGroupAtIndex return last selection group', () => {
const {root, component} = setupTest(false, getFixtureWithMultipleSelectionGroups);
const lastSelectionGroup = root.querySelectorAll('.mdc-menu__selection-group')[1];
assert.equal(component.getDefaultFoundation().adapter_.getSelectionGroupAtIndex(1), lastSelectionGroup);
});

test('adapter#getListItemIndexOfSelectionGroup returns list item index relative to the first selection group', () => {
const {root, component} = setupTest(false, getFixtureWithMultipleSelectionGroups);
const selectionGroup = root.querySelectorAll(`.${cssClasses.MENU_SELECTION_GROUP}`)[0];
assert.equal(component.getDefaultFoundation().adapter_.getListItemIndexOfSelectionGroup(0, selectionGroup), 2);
});

test('adapter#getListItemIndexOfSelectionGroup returns list item index relative to the 2nd selection group', () => {
const {root, component} = setupTest(false, getFixtureWithMultipleSelectionGroups);
const selectionGroup = root.querySelectorAll(`.${cssClasses.MENU_SELECTION_GROUP}`)[1];
assert.equal(component.getDefaultFoundation().adapter_.getListItemIndexOfSelectionGroup(1, selectionGroup), 5);
test('adapter#getListItemByIndex returns the list item by the index', () => {
const {component, root} = setupTest();
const listItem = root.querySelectorAll(`.${MDCListFoundation.cssClasses.LIST_ITEM_CLASS}`)[1];
assert.equal(component.getDefaultFoundation().adapter_.getListItemByIndex(1), listItem);
});

test('adapter#focusItemAtIndex focuses the menu item at given index', () => {
Expand Down
51 changes: 25 additions & 26 deletions test/unit/mdc-menu/menu.foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ test('defaultAdapter returns a complete adapter implementation', () => {
'addClassToElementAtIndex', 'removeClassFromElementAtIndex', 'addAttributeToElementAtIndex',
'removeAttributeFromElementAtIndex', 'elementContainsClass', 'closeSurface', 'getElementIndex', 'getParentElement',
'getSelectedElementIndex', 'notifySelected', 'getMenuItemCount', 'focusItemAtIndex', 'focusListRoot',
'getSelectionGroupAtIndex', 'getListItemIndexOfSelectionGroup',
'getListItemByIndex',
]);
});

Expand Down Expand Up @@ -162,7 +162,6 @@ test('handleItemAction item action event inside of a selection group with anothe
td.when(mockAdapter.getParentElement(itemEl)).thenReturn(itemEl);
td.when(mockAdapter.elementContainsClass(itemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(true);
td.when(mockAdapter.getSelectedElementIndex(itemEl)).thenReturn(0);
td.when(mockAdapter.getListItemIndexOfSelectionGroup(0, itemEl)).thenReturn(0);

foundation.handleItemAction(itemEl);
clock.tick(numbers.TRANSITION_CLOSE_DURATION);
Expand All @@ -179,7 +178,6 @@ test('handleItemAction item action event inside of a selection group with no ele
td.when(mockAdapter.getParentElement(itemEl)).thenReturn(itemEl);
td.when(mockAdapter.elementContainsClass(itemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(true);
td.when(mockAdapter.getSelectedElementIndex(itemEl)).thenReturn(-1);
td.when(mockAdapter.getListItemIndexOfSelectionGroup(0, itemEl)).thenReturn(0);

foundation.handleItemAction(itemEl);
clock.tick(numbers.TRANSITION_CLOSE_DURATION);
Expand All @@ -198,7 +196,6 @@ test('handleItemAction item action event inside of a child element of a list ite
td.when(mockAdapter.getParentElement(itemEl)).thenReturn(itemEl);
td.when(mockAdapter.elementContainsClass(itemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(false, true);
td.when(mockAdapter.getSelectedElementIndex(itemEl)).thenReturn(-1);
td.when(mockAdapter.getListItemIndexOfSelectionGroup(0, itemEl)).thenReturn(0);

foundation.handleItemAction(itemEl);
clock.tick(numbers.TRANSITION_CLOSE_DURATION);
Expand Down Expand Up @@ -270,43 +267,48 @@ test('handleMenuSurfaceOpened does not focus anything when DefaultFocusState is
td.verify(mockAdapter.focusListRoot(), {times: 0});
});

test('setSelectedIndex gets the menu selection groups of the menu', () => {
test('setSelectedIndex gets the list item at the specified index', () => {
const {foundation, mockAdapter} = setupTest();
const selectionGroupEl = document.createElement('div');
td.when(mockAdapter.getSelectionGroupAtIndex(0)).thenReturn(selectionGroupEl);
foundation.setSelectedIndex(0, 0);
td.verify(mockAdapter.getSelectionGroupAtIndex(0), {times: 1});
const listItemEl = document.createElement('div');
td.when(mockAdapter.getListItemByIndex(0)).thenReturn(listItemEl);
td.when(mockAdapter.getParentElement(listItemEl)).thenReturn(listItemEl);
td.when(mockAdapter.elementContainsClass(listItemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(true);

foundation.setSelectedIndex(0);
td.verify(mockAdapter.getListItemByIndex(0), {times: 1});
});

test('setSelectedIndex calls addClass and addAttribute', () => {
const {foundation, mockAdapter} = setupTest();
const selectionGroupEl = document.createElement('div');
td.when(mockAdapter.getSelectionGroupAtIndex(0)).thenReturn(selectionGroupEl);
td.when(mockAdapter.getSelectedElementIndex(selectionGroupEl)).thenReturn(-1);
td.when(mockAdapter.getListItemIndexOfSelectionGroup(0, selectionGroupEl)).thenReturn(2);
const listItemEl = document.createElement('div');
td.when(mockAdapter.getListItemByIndex(0)).thenReturn(listItemEl);
td.when(mockAdapter.getParentElement(listItemEl)).thenReturn(listItemEl);
td.when(mockAdapter.elementContainsClass(listItemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(true);
td.when(mockAdapter.getSelectedElementIndex(listItemEl)).thenReturn(-1);

foundation.setSelectedIndex(0, 0);
foundation.setSelectedIndex(0);
td.verify(mockAdapter.removeClassFromElementAtIndex(
td.matchers.isA(Number), cssClasses.MENU_SELECTED_LIST_ITEM), {times: 0});
td.verify(mockAdapter.removeAttributeFromElementAtIndex(td.matchers.isA(Number),
strings.ARIA_SELECTED_ATTR), {times: 0});
td.verify(mockAdapter.addClassToElementAtIndex(2, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1});
td.verify(mockAdapter.addAttributeToElementAtIndex(2, strings.ARIA_SELECTED_ATTR, 'true'), {times: 1});
td.verify(mockAdapter.addClassToElementAtIndex(0, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1});
td.verify(mockAdapter.addAttributeToElementAtIndex(0, strings.ARIA_SELECTED_ATTR, 'true'), {times: 1});
});

test('setSelectedIndex remove class and attribute, and adds class and attribute to newly selected item', () => {
const {foundation, mockAdapter} = setupTest();
const selectionGroupEl = document.createElement('div');
td.when(mockAdapter.getSelectionGroupAtIndex(0)).thenReturn(selectionGroupEl);
td.when(mockAdapter.getSelectedElementIndex(selectionGroupEl)).thenReturn(1);
td.when(mockAdapter.getListItemIndexOfSelectionGroup(0, selectionGroupEl)).thenReturn(2);
const listItemEl = document.createElement('div');
td.when(mockAdapter.getListItemByIndex(0)).thenReturn(listItemEl);
td.when(mockAdapter.getParentElement(listItemEl)).thenReturn(listItemEl);
td.when(mockAdapter.elementContainsClass(listItemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(true);
td.when(mockAdapter.getSelectedElementIndex(listItemEl)).thenReturn(1);

foundation.setSelectedIndex(0, 0);
foundation.setSelectedIndex(0);
td.verify(mockAdapter.removeClassFromElementAtIndex(1, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1});
td.verify(
mockAdapter.removeAttributeFromElementAtIndex(1, strings.ARIA_SELECTED_ATTR), {times: 1});
td.verify(mockAdapter.addClassToElementAtIndex(2, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1});
td.verify(mockAdapter.addAttributeToElementAtIndex(2, strings.ARIA_SELECTED_ATTR, 'true'), {times: 1});
td.verify(mockAdapter.addClassToElementAtIndex(0, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1});
td.verify(mockAdapter.addAttributeToElementAtIndex(0, strings.ARIA_SELECTED_ATTR, 'true'), {times: 1});
});

// Item Action
Expand Down Expand Up @@ -341,7 +343,6 @@ test('item action event inside of a selection group with another element selecte
td.when(mockAdapter.getParentElement(itemEl)).thenReturn(itemEl);
td.when(mockAdapter.elementContainsClass(itemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(true);
td.when(mockAdapter.getSelectedElementIndex(itemEl)).thenReturn(0);
td.when(mockAdapter.getListItemIndexOfSelectionGroup(0, itemEl)).thenReturn(0);

foundation.handleItemAction(itemEl);
clock.tick(numbers.TRANSITION_CLOSE_DURATION);
Expand All @@ -358,7 +359,6 @@ test('item action event inside of a selection group with no element selected', (
td.when(mockAdapter.getParentElement(itemEl)).thenReturn(itemEl);
td.when(mockAdapter.elementContainsClass(itemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(true);
td.when(mockAdapter.getSelectedElementIndex(itemEl)).thenReturn(-1);
td.when(mockAdapter.getListItemIndexOfSelectionGroup(0, itemEl)).thenReturn(0);

foundation.handleItemAction(itemEl);
clock.tick(numbers.TRANSITION_CLOSE_DURATION);
Expand All @@ -376,7 +376,6 @@ test('item action event inside of a child element of a list item in a selection
td.when(mockAdapter.getParentElement(itemEl)).thenReturn(itemEl);
td.when(mockAdapter.elementContainsClass(itemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(false, true);
td.when(mockAdapter.getSelectedElementIndex(itemEl)).thenReturn(-1);
td.when(mockAdapter.getListItemIndexOfSelectionGroup(0, itemEl)).thenReturn(0);

foundation.handleItemAction(itemEl);
clock.tick(numbers.TRANSITION_CLOSE_DURATION);
Expand Down