diff --git a/packages/mdc-menu/README.md b/packages/mdc-menu/README.md index ce58b342775..40fc57dcd81 100644 --- a/packages/mdc-menu/README.md +++ b/packages/mdc-menu/README.md @@ -226,6 +226,7 @@ Method Signature | Description `setAnchorMargin(Partial) => void` | Proxies to the menu surface's `setAnchorMargin(Partial)` 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. @@ -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` @@ -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 diff --git a/packages/mdc-menu/adapter.ts b/packages/mdc-menu/adapter.ts index 5690de25a71..324a8154fa7 100644 --- a/packages/mdc-menu/adapter.ts +++ b/packages/mdc-menu/adapter.ts @@ -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. */ @@ -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; } diff --git a/packages/mdc-menu/component.ts b/packages/mdc-menu/component.ts index aa900acf4a2..b1260874de3 100644 --- a/packages/mdc-menu/component.ts +++ b/packages/mdc-menu/component.ts @@ -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'; @@ -143,6 +144,14 @@ export class MDCMenu extends MDCComponent { 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) { + this.foundation_.setSelectedIndex(index); + } + /** * @return The item within the menu at the index specified. */ @@ -203,11 +212,6 @@ export class MDCMenu extends MDCComponent { 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(strings.SELECTED_EVENT, { index: evtData.index, item: this.items[evtData.index], @@ -215,6 +219,12 @@ export class MDCMenu extends MDCComponent { 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); diff --git a/packages/mdc-menu/foundation.ts b/packages/mdc-menu/foundation.ts index 40cc66de800..a4d2cc81b33 100644 --- a/packages/mdc-menu/foundation.ts +++ b/packages/mdc-menu/foundation.ts @@ -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'; @@ -56,12 +55,12 @@ export class MDCMenuFoundation extends MDCFoundation { 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 } @@ -98,9 +97,8 @@ export class MDCMenuFoundation extends MDCFoundation { // 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); } @@ -132,41 +130,32 @@ export class MDCMenuFoundation extends MDCFoundation { } /** - * 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.'); } } } diff --git a/packages/mdc-menu/package.json b/packages/mdc-menu/package.json index ecdd0e67191..292362f4fce 100644 --- a/packages/mdc-menu/package.json +++ b/packages/mdc-menu/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@material/base": "^1.0.0", + "@material/dom": "^1.1.0", "@material/feature-targeting": "^0.44.1", "@material/list": "^2.1.1", "@material/menu-surface": "^1.1.1", diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json index 7e063e55548..532691bc6cb 100644 --- a/test/screenshot/golden.json +++ b/test/screenshot/golden.json @@ -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/05/10/15_16_09_688/spec/mdc-menu/issues/4025.html?utm_source=golden_json", "screenshots": { diff --git a/test/screenshot/spec/mdc-menu/classes/multiple-menu-selection-group.html b/test/screenshot/spec/mdc-menu/classes/multiple-menu-selection-group.html new file mode 100644 index 00000000000..77856b9d715 --- /dev/null +++ b/test/screenshot/spec/mdc-menu/classes/multiple-menu-selection-group.html @@ -0,0 +1,165 @@ + + + + + + Multiple Menu Selection Group - MDC Web Screenshot Test + + + + + + + + + + + + + + +
+
+
+
+ +
+ +
+
+
+
+
+ + + + + + + + + + diff --git a/test/screenshot/spec/mdc-menu/fixture.js b/test/screenshot/spec/mdc-menu/fixture.js index 850b29bfd5a..1c4aac22808 100644 --- a/test/screenshot/spec/mdc-menu/fixture.js +++ b/test/screenshot/spec/mdc-menu/fixture.js @@ -26,6 +26,7 @@ import {DefaultFocusState} from '../../../../packages/mdc-menu/constants'; window.mdc.testFixture.fontsLoaded.then(() => { const buttonEl = document.querySelector('.test-menu-button'); const menuEl = document.querySelector('.mdc-menu'); + const multipleSelectionGroupMenuEl = document.getElementById('test-multiple-selection-group-menu'); const menu = mdc.menu.MDCMenu.attachTo(menuEl); menu.setAnchorCorner(mdc.menu.Corner.BOTTOM_LEFT); menu.open = true; @@ -51,6 +52,10 @@ window.mdc.testFixture.fontsLoaded.then(() => { menu.open = !menu.open; } }); + if (multipleSelectionGroupMenuEl) { + menu.setSelectedIndex(3); + menu.setSelectedIndex(5); + } window.mdc.testFixture.notifyDomReady(); }); diff --git a/test/unit/mdc-menu/mdc-menu.test.js b/test/unit/mdc-menu/mdc-menu.test.js index 8cbba21ffde..31ae9abb2cb 100644 --- a/test/unit/mdc-menu/mdc-menu.test.js +++ b/test/unit/mdc-menu/mdc-menu.test.js @@ -39,10 +39,37 @@ function getFixture(open) {
  • + +
  • + + + `; +} + +function getFixtureWithMultipleSelectionGroups(open) { + return bel` +
    +
    `; @@ -104,8 +131,8 @@ function setupTestWithFakes(open = false) { * @param {boolean=} open * @return {{component: !MDCMenu, root: !HTMLElement}} */ -function setupTest(open = false) { - const root = getFixture(open); +function setupTest(open = false, fixture = getFixture) { + const root = fixture(open); const component = new MDCMenu(root); return {root, component}; @@ -115,8 +142,8 @@ function setupTest(open = false) { * @param {!Object=} options * @return {{component: !MDCMenu, root: !HTMLElement, mockFoundation: !MDCMenuFoundation}} */ -function setupTestWithMock(options = {open: true}) { - const root = getFixture(options.open); +function setupTestWithMock(options = {open: true, fixture: getFixture}) { + const root = options.fixture(options.open); const MockFoundationCtor = td.constructor(MDCMenuFoundation); const mockFoundation = new MockFoundationCtor(); @@ -209,6 +236,12 @@ test('setAnchorMargin', () => { td.verify(menuSurface.setAnchorMargin({top: 0, right: 0, bottom: 0, left: 0})); }); +test('setSelectedIndex calls foundation method setSelectedIndex with given index.', () => { + const {component, mockFoundation} = setupTestWithMock({fixture: getFixtureWithMultipleSelectionGroups}); + component.setSelectedIndex(1); + td.verify(mockFoundation.setSelectedIndex(1)); +}); + test('setQuickOpen', () => { const {component, menuSurface} = setupTestWithFakes(); component.quickOpen = true; @@ -397,29 +430,6 @@ test('adapter#getElementIndex returns -1 if the element does not exist in the li assert.equal(indexValue, -1); }); -test('adapter#getParentElement returns the parent element of an element', () => { - const {root, component} = setupTest(); - const firstItem = root.querySelector('.mdc-list-item'); - const parentElement = component.getDefaultFoundation().adapter_.getParentElement(firstItem); - assert.equal(firstItem.parentElement, parentElement); -}); - -test('adapter#getSelectedElementIndex returns the index of the "selected" element in a group', () => { - const {root, component} = setupTest(); - const selectionGroup = root.querySelector('.mdc-menu__selection-group'); - const index = component.getDefaultFoundation().adapter_.getSelectedElementIndex(selectionGroup); - assert.equal(root.querySelector('.mdc-menu-item--selected'), component.items[index]); -}); - -test('adapter#getSelectedElementIndex returns -1 if the "selected" element is not in a group', () => { - const {root, component} = setupTest(); - const selectionGroup = root.querySelector('.mdc-menu__selection-group'); - const element = root.querySelector('.mdc-menu-item--selected'); - element.classList.remove('mdc-menu-item--selected'); - const index = component.getDefaultFoundation().adapter_.getSelectedElementIndex(selectionGroup); - assert.equal(index, -1); -}); - test('adapter#notifySelected emits an event for a selected element', () => { const {root, component} = setupTest(); const handler = td.func('eventHandler'); @@ -452,3 +462,27 @@ test('adapter#focusListRoot focuses the list root element', () => { document.body.removeChild(root); }); + +test('adapter#isSelectableItemAtIndex returns true if the menu item is within the' + +'.mdc-menu__selection-group element', () => { + const {component} = setupTest(); + + const isSelectableItemAtIndex = component.getDefaultFoundation().adapter_.isSelectableItemAtIndex(3); + assert.isTrue(isSelectableItemAtIndex); +}); + +test('adapter#isSelectableItemAtIndex returns false if the menu item is not within the' + +'.mdc-menu__selection-group element', () => { + const {component} = setupTest(); + + const isSelectableItemAtIndex = component.getDefaultFoundation().adapter_.isSelectableItemAtIndex(1); + assert.isFalse(isSelectableItemAtIndex); +}); + +test('adapter#getSelectedSiblingOfItemAtIndex returns the index of the selected item within the same' + +'selection group', () => { + const {component} = setupTest(); + + const siblingIndex = component.getDefaultFoundation().adapter_.getSelectedSiblingOfItemAtIndex(2); + assert.equal(siblingIndex, 3); +}); diff --git a/test/unit/mdc-menu/menu.foundation.test.js b/test/unit/mdc-menu/menu.foundation.test.js index b62625efdbe..60306fffe03 100644 --- a/test/unit/mdc-menu/menu.foundation.test.js +++ b/test/unit/mdc-menu/menu.foundation.test.js @@ -45,8 +45,9 @@ suite('MDCMenuFoundation'); test('defaultAdapter returns a complete adapter implementation', () => { verifyDefaultAdapter(MDCMenuFoundation, [ 'addClassToElementAtIndex', 'removeClassFromElementAtIndex', 'addAttributeToElementAtIndex', - 'removeAttributeFromElementAtIndex', 'elementContainsClass', 'closeSurface', 'getElementIndex', 'getParentElement', - 'getSelectedElementIndex', 'notifySelected', 'getMenuItemCount', 'focusItemAtIndex', 'focusListRoot', + 'removeAttributeFromElementAtIndex', 'elementContainsClass', 'closeSurface', 'getElementIndex', + 'getSelectedSiblingOfItemAtIndex', 'isSelectableItemAtIndex', 'notifySelected', + 'getMenuItemCount', 'focusItemAtIndex', 'focusListRoot', ]); }); @@ -139,12 +140,10 @@ test('handleItemAction item action event inside of a selection group ' + 'with additional markup does not cause loop', () => { // This test will timeout of there is an endless loop in the selection group logic. const {foundation, mockAdapter, clock} = setupTest(); - const parentElement = {}; const itemEl = document.createElement('li'); td.when(mockAdapter.elementContainsClass(itemEl, listClasses.LIST_ITEM_CLASS)).thenReturn(true); td.when(mockAdapter.elementContainsClass(td.matchers.anything(), listClasses.ROOT)).thenReturn(false, true); td.when(mockAdapter.getElementIndex(itemEl)).thenReturn(0); - td.when(mockAdapter.getParentElement(td.matchers.anything())).thenReturn(parentElement, null); td.when(mockAdapter.elementContainsClass(td.matchers.anything(), cssClasses.MENU_SELECTION_GROUP)).thenReturn(false); foundation.handleItemAction(itemEl); @@ -158,14 +157,15 @@ test('handleItemAction item action event inside of a selection group with anothe const itemEl = document.createElement('li'); td.when(mockAdapter.elementContainsClass(itemEl, listClasses.LIST_ITEM_CLASS)).thenReturn(true); td.when(mockAdapter.getElementIndex(itemEl)).thenReturn(0); - 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.isSelectableItemAtIndex(0)).thenReturn(true); + td.when(mockAdapter.getSelectedSiblingOfItemAtIndex(0)).thenReturn(1); + td.when(mockAdapter.getMenuItemCount()).thenReturn(5); foundation.handleItemAction(itemEl); clock.tick(numbers.TRANSITION_CLOSE_DURATION); - td.verify(mockAdapter.removeClassFromElementAtIndex(0, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1}); + td.verify(mockAdapter.removeClassFromElementAtIndex(1, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1}); td.verify(mockAdapter.addClassToElementAtIndex(0, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1}); }); @@ -174,9 +174,10 @@ test('handleItemAction item action event inside of a selection group with no ele const itemEl = document.createElement('li'); td.when(mockAdapter.elementContainsClass(itemEl, listClasses.LIST_ITEM_CLASS)).thenReturn(true); td.when(mockAdapter.getElementIndex(itemEl)).thenReturn(0); - 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.isSelectableItemAtIndex(0)).thenReturn(true); + td.when(mockAdapter.getSelectedSiblingOfItemAtIndex(0)).thenReturn(-1); + td.when(mockAdapter.getMenuItemCount()).thenReturn(5); foundation.handleItemAction(itemEl); clock.tick(numbers.TRANSITION_CLOSE_DURATION); @@ -192,9 +193,9 @@ test('handleItemAction item action event inside of a child element of a list ite const itemEl = document.createElement('li'); td.when(mockAdapter.elementContainsClass(itemEl, listClasses.LIST_ITEM_CLASS)).thenReturn(true); td.when(mockAdapter.getElementIndex(itemEl)).thenReturn(0); - 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.isSelectableItemAtIndex(0)).thenReturn(true); + td.when(mockAdapter.getMenuItemCount()).thenReturn(5); foundation.handleItemAction(itemEl); clock.tick(numbers.TRANSITION_CLOSE_DURATION); @@ -210,10 +211,10 @@ test('handleItemAction item action event inside of a child element of a selectio const itemEl = document.createElement('li'); td.when(mockAdapter.elementContainsClass(itemEl, listClasses.LIST_ITEM_CLASS)).thenReturn(true); td.when(mockAdapter.getElementIndex(itemEl)).thenReturn(0); - td.when(mockAdapter.getParentElement(itemEl)).thenReturn(itemEl); td.when(mockAdapter.elementContainsClass(itemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(false); td.when(mockAdapter.elementContainsClass(itemEl, listClasses.ROOT)).thenReturn(false, true); - td.when(mockAdapter.getSelectedElementIndex(itemEl)).thenReturn(-1); + td.when(mockAdapter.isSelectableItemAtIndex(0)).thenReturn(false); + td.when(mockAdapter.getMenuItemCount()).thenReturn(5); foundation.handleItemAction(itemEl); clock.tick(numbers.TRANSITION_CLOSE_DURATION); @@ -266,96 +267,57 @@ test('handleMenuSurfaceOpened does not focus anything when DefaultFocusState is td.verify(mockAdapter.focusListRoot(), {times: 0}); }); -// Item Action - -test('Item action event causes the menu to close', () => { +test('setSelectedIndex calls addClass and addAttribute only', () => { const {foundation, mockAdapter} = setupTest(); - const itemEl = document.createElement('li'); - td.when(mockAdapter.elementContainsClass(itemEl, listClasses.LIST_ITEM_CLASS)).thenReturn(true); - td.when(mockAdapter.getElementIndex(itemEl)).thenReturn(0); - - foundation.handleItemAction(itemEl); - - td.verify(mockAdapter.closeSurface(), {times: 1}); + const listItemEl = document.createElement('div'); + td.when(mockAdapter.isSelectableItemAtIndex(0)).thenReturn(true); + td.when(mockAdapter.elementContainsClass(listItemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(true); + td.when(mockAdapter.getSelectedSiblingOfItemAtIndex(0)).thenReturn(-1); + td.when(mockAdapter.getMenuItemCount()).thenReturn(2); + + 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(0, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1}); + td.verify(mockAdapter.addAttributeToElementAtIndex(0, strings.ARIA_SELECTED_ATTR, 'true'), {times: 1}); }); -test('item action event causes the menu to emit the selected item event', () => { +test('setSelectedIndex remove class and attribute, and adds class and attribute to newly selected item', () => { const {foundation, mockAdapter} = setupTest(); - const itemEl = document.createElement('li'); - td.when(mockAdapter.elementContainsClass(itemEl, listClasses.LIST_ITEM_CLASS)).thenReturn(true); - td.when(mockAdapter.getElementIndex(itemEl)).thenReturn(0); - - foundation.handleItemAction(itemEl); - - td.verify(mockAdapter.notifySelected({index: 0}), {times: 1}); -}); - -test('item action event inside of a selection group with another element selected', () => { - const {foundation, mockAdapter, clock} = setupTest(); - const itemEl = document.createElement('li'); - td.when(mockAdapter.elementContainsClass(itemEl, listClasses.LIST_ITEM_CLASS)).thenReturn(true); - td.when(mockAdapter.getElementIndex(itemEl)).thenReturn(0); - 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); - - foundation.handleItemAction(itemEl); - clock.tick(numbers.TRANSITION_CLOSE_DURATION); - - td.verify(mockAdapter.removeClassFromElementAtIndex(0, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1}); + const listItemEl = document.createElement('div'); + td.when(mockAdapter.isSelectableItemAtIndex(0)).thenReturn(true); + td.when(mockAdapter.elementContainsClass(listItemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(true); + td.when(mockAdapter.getMenuItemCount()).thenReturn(2); + td.when(mockAdapter.getSelectedSiblingOfItemAtIndex(0)).thenReturn(1); + + 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(0, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1}); + td.verify(mockAdapter.addAttributeToElementAtIndex(0, strings.ARIA_SELECTED_ATTR, 'true'), {times: 1}); }); -test('item action event inside of a selection group with no element selected', () => { - const {foundation, mockAdapter, clock} = setupTest(); - const itemEl = document.createElement('li'); - td.when(mockAdapter.elementContainsClass(itemEl, listClasses.LIST_ITEM_CLASS)).thenReturn(true); - td.when(mockAdapter.getElementIndex(itemEl)).thenReturn(0); - 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); - - foundation.handleItemAction(itemEl); - clock.tick(numbers.TRANSITION_CLOSE_DURATION); - - td.verify(mockAdapter.removeClassFromElementAtIndex(td.matchers.isA(Number), cssClasses.MENU_SELECTED_LIST_ITEM), - {times: 0}); - td.verify(mockAdapter.addClassToElementAtIndex(0, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1}); +test('setSelectedIndex throws error if index is not in range', () => { + const {foundation} = setupTest(); + try { + foundation.setSelectedIndex(5); + } catch (e) { + assert.equal(e.message, 'MDCMenuFoundation: No list item at specified index.'); + } }); -test('item action event inside of a child element of a list item in a selection group with no element selected', () => { - const {foundation, mockAdapter, clock} = setupTest(); - const itemEl = document.createElement('li'); - td.when(mockAdapter.elementContainsClass(itemEl, listClasses.LIST_ITEM_CLASS)).thenReturn(true); - td.when(mockAdapter.getElementIndex(itemEl)).thenReturn(0); - 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); - - foundation.handleItemAction(itemEl); - clock.tick(numbers.TRANSITION_CLOSE_DURATION); - - td.verify(mockAdapter.removeClassFromElementAtIndex(td.matchers.isA(Number), cssClasses.MENU_SELECTED_LIST_ITEM), - {times: 0}); - td.verify(mockAdapter.addClassToElementAtIndex(0, cssClasses.MENU_SELECTED_LIST_ITEM), {times: 1}); -}); +// Item Action -test('item action event inside of a child element of a selection group (but not a list item) with no element ' + - 'selected', () => { - const {foundation, mockAdapter, clock} = setupTest(); +test('Item action event causes the menu to close', () => { + const {foundation, mockAdapter} = setupTest(); const itemEl = document.createElement('li'); td.when(mockAdapter.elementContainsClass(itemEl, listClasses.LIST_ITEM_CLASS)).thenReturn(true); td.when(mockAdapter.getElementIndex(itemEl)).thenReturn(0); - td.when(mockAdapter.getParentElement(itemEl)).thenReturn(itemEl); - td.when(mockAdapter.elementContainsClass(itemEl, cssClasses.MENU_SELECTION_GROUP)).thenReturn(false); - td.when(mockAdapter.elementContainsClass(itemEl, listClasses.ROOT)).thenReturn(false, true); - td.when(mockAdapter.getSelectedElementIndex(itemEl)).thenReturn(-1); foundation.handleItemAction(itemEl); - clock.tick(numbers.TRANSITION_CLOSE_DURATION); - td.verify(mockAdapter.removeClassFromElementAtIndex(td.matchers.isA(Number), cssClasses.MENU_SELECTED_LIST_ITEM), - {times: 0}); - td.verify(mockAdapter.addClassToElementAtIndex(td.matchers.isA(Number), cssClasses.MENU_SELECTED_LIST_ITEM), - {times: 0}); + td.verify(mockAdapter.closeSurface(), {times: 1}); });