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 6 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
3 changes: 3 additions & 0 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 @@ -251,6 +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.
`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 @@ -265,6 +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) => void` | Selects the list item at `index` witnin the menu.
moog16 marked this conversation as resolved.
Show resolved Hide resolved

### Events

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

/**
* @return List item element.
* @param index Index of list item within menu.
*/
getListItemByIndex(index: number): Element | null;
/**
* @return The parentElement of the provided element.
*/
Expand Down
9 changes: 8 additions & 1 deletion packages/mdc-menu/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {
}

private menuSurfaceFactory_!: MDCMenuSurfaceFactory; // assigned in initialize()
private listFactory_!: MDCListFactory; // assigned in initialize()
private listFactory_!: MDCListFactory; // ` assigned in initialize()
moog16 marked this conversation as resolved.
Show resolved Hide resolved

private menuSurface_!: MDCMenuSurface; // assigned in initialSyncWithDOM()
private list_!: MDCList | null; // assigned in initialSyncWithDOM()
Expand Down Expand Up @@ -140,6 +140,10 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {
this.menuSurface_.setAnchorMargin(margin);
}

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 @@ -205,6 +209,9 @@ export class MDCMenu extends MDCComponent<MDCMenuFoundation> {
const selectedListItem = selectionGroup.querySelector(`.${cssClasses.MENU_SELECTED_LIST_ITEM}`);
return selectedListItem ? this.items.indexOf(selectedListItem) : -1;
},
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,
item: this.items[evtData.index],
Expand Down
26 changes: 22 additions & 4 deletions packages/mdc-menu/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class MDCMenuFoundation extends MDCFoundation<MDCMenuAdapter> {
closeSurface: () => undefined,
getElementIndex: () => -1,
getParentElement: () => null,
getListItemByIndex: () => null,
getSelectedElementIndex: () => -1,
notifySelected: () => undefined,
getMenuItemCount: () => 0,
Expand Down Expand Up @@ -99,9 +100,7 @@ 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);
}
this.handleSelectionGroup_(selectionGroup, index);
moog16 marked this conversation as resolved.
Show resolved Hide resolved
}, MDCMenuSurfaceFoundation.numbers.TRANSITION_CLOSE_DURATION);
}

Expand Down Expand Up @@ -131,10 +130,29 @@ export class MDCMenuFoundation extends MDCFoundation<MDCMenuAdapter> {
this.defaultFocusState_ = focusState;
}

/**
* 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.
*/
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.');
}
this.handleSelectionGroup_(selectionGroup, index);
}

/**
* Handles toggling the selected classes in a selection group when a selection is made.
*/
private handleSelectionGroup_(selectionGroup: Element, index: number) {
private handleSelectionGroup_(selectionGroup: Element | null, index: number) {
if (!selectionGroup) {
abhiomkar marked this conversation as resolved.
Show resolved Hide resolved
return;
}
// De-select the previous selection in this group.
const selectedIndex = this.adapter_.getSelectedElementIndex(selectionGroup);
if (selectedIndex >= 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<!DOCTYPE html>
<!--
Copyright 2018 Google Inc.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2019*


Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<title>Menu Selection Group - MDC Web Screenshot Test</title>
moog16 marked this conversation as resolved.
Show resolved Hide resolved
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../../out/mdc.list.css">
<link rel="stylesheet" href="../../../out/mdc.menu-surface.css">
<link rel="stylesheet" href="../../../out/mdc.menu.css">
<link rel="stylesheet" href="../../../out/mdc.button.css">
<link rel="stylesheet" href="../../../out/spec/fixture.css">
<link rel="stylesheet" href="../../../out/spec/mdc-menu/fixture.css">

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-118996389-2"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-118996389-2');
</script>
</head>

<body class="test-container test-container--edge-fonts">
<main class="test-viewport test-viewport--mobile">
<div class="test-layout">
<div class="test-cell test-cell--menu">
<div class="mdc-menu-surface--anchor test-menu-button-container">
<button class="mdc-button test-menu-button">Open menu</button>
<div class="mdc-menu mdc-menu-surface mdc-menu-surface--open" id="multiple-selection-group-menu" style="left: 0px; top: 36px;">
<ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1">
<li>
<ul class="mdc-menu__selection-group">
<li class="mdc-list-item" role="menuitem">
<span class="mdc-list-item__graphic mdc-menu__selection-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
</svg>
</span>
<span class="mdc-list-item__text">
Single
</span>
</li>
<li class="mdc-list-item mdc-list-item--disabled" role="menuitem">
<span class="mdc-list-item__graphic mdc-menu__selection-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
</svg>
</span>
<span class="mdc-list-item__text">
1.15
</span>
</li>
<li class="mdc-list-item" role="menuitem">
<span class="mdc-list-item__graphic mdc-menu__selection-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
</svg>
</span>
<span class="mdc-list-item__text">
Double
</span>
</li>
<li class="mdc-list-item mdc-menu-item--selected" role="menuitem">
<span class="mdc-list-item__graphic mdc-menu__selection-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
</svg>
</span>
<span class="mdc-list-item__text">
Custom: 1.2
</span>
</li>
</ul>
</li>
<li class="mdc-list-divider" role="separator"></li>
<li>
<ul class="mdc-menu__selection-group">
<li class="mdc-list-item" role="menuitem">
<span class="mdc-list-item__graphic mdc-menu__selection-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
</svg>
</span>
<span class="mdc-list-item__text">
Bold
</span>
</li>
<li class="mdc-list-item" role="menuitem">
<span class="mdc-list-item__graphic mdc-menu__selection-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
</svg>
</span>
<span class="mdc-list-item__text">
Italic
</span>
</li>
<li class="mdc-list-item" role="menuitem">
<span class="mdc-list-item__graphic mdc-menu__selection-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
</svg>
</span>
<span class="mdc-list-item__text">
StrikeThrough
</span>
</li>
<li class="mdc-list-item mdc-menu-item--selected" role="menuitem">
<span class="mdc-list-item__graphic mdc-menu__selection-group-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
</svg>
</span>
<span class="mdc-list-item__text">
SuperScript
</span>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>

<!-- Automatically provides/replaces `Promise` if missing or broken. -->
<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.js"></script>
<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fontfaceobserver/2.0.13/fontfaceobserver.standalone.js"></script>
<script src="../../../out/material-components-web.js"></script>
<script src="../../../out/spec/fixture.js"></script>
<script src="../../../out/spec/mdc-menu/fixture.js"></script>
</body>
</html>
5 changes: 5 additions & 0 deletions test/screenshot/spec/mdc-menu/fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('multiple-selection-group-menu');
moog16 marked this conversation as resolved.
Show resolved Hide resolved
const menu = mdc.menu.MDCMenu.attachTo(menuEl);
menu.setAnchorCorner(mdc.menu.Corner.BOTTOM_LEFT);
menu.open = true;
Expand All @@ -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();
});
55 changes: 47 additions & 8 deletions test/unit/mdc-menu/mdc-menu.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,37 @@ function getFixture(open) {
<li tabIndex="-1" class="mdc-list-item" role="menuitem">Item</a>
<li role="separator"></li>
<li tabIndex="-1" class="mdc-list-item" role="menuitem">Another Item</a>
<ul class="mdc-menu__selection-group mdc-list" role="menu">
<li tabIndex="-1" class="mdc-list-item" role="menuitem">Item</a>
<li tabIndex="-1" class="mdc-list-item mdc-menu-item--selected" role="menuitem">Another Item</a>
</ul>
<li>
abhiomkar marked this conversation as resolved.
Show resolved Hide resolved
<ul class="mdc-menu__selection-group" role="menu">
<li tabIndex="-1" class="mdc-list-item" role="menuitem">Item</a>
<li tabIndex="-1" class="mdc-list-item mdc-menu-item--selected" role="menuitem">Another Item</a>
</ul>
</li>
</ul>
</div>
`;
}

function getFixtureWithMultipleSelectionGroups(open) {
return bel`
<div class="mdc-menu mdc-menu-surface ${open ? 'mdc-menu-surface--open' : ''}">
<ul class="mdc-list" role="menu" tabIndex="-1">
<li tabIndex="-1" class="mdc-list-item" role="menuitem">Item</a>
<li class="mdc-list-divider" role="separator"></li>
<li tabIndex="-1" class="mdc-list-item" role="menuitem">Another Item</a>
<li>
<ul class="mdc-menu__selection-group" role="menu">
<li tabIndex="-1" class="mdc-list-item" role="menuitem">Item</a>
<li tabIndex="-1" class="mdc-list-item mdc-menu-item--selected" role="menuitem">Another Item</a>
</ul>
</li>
<li class="mdc-list-divider" role="separator"></li>
<li>
<ul class="mdc-menu__selection-group" role="menu">
<li tabIndex="-1" class="mdc-list-item mdc-menu-item--selected" role="menuitem">Item2</a>
<li tabIndex="-1" class="mdc-list-item" role="menuitem">Another Item2</a>
</ul>
</li>
</ul>
</div>
`;
Expand Down Expand Up @@ -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};
Expand All @@ -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();
Expand Down Expand Up @@ -209,6 +236,12 @@ test('setAnchorMargin', () => {
td.verify(menuSurface.setAnchorMargin({top: 0, right: 0, bottom: 0, left: 0}));
});

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

test('setQuickOpen', () => {
const {component, menuSurface} = setupTestWithFakes();
component.quickOpen = true;
Expand Down Expand Up @@ -433,6 +466,12 @@ test('adapter#getMenuItemCount returns the menu item count', () => {
assert.equal(component.getDefaultFoundation().adapter_.getMenuItemCount(), component.items.length);
});

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', () => {
const {root, component} = setupTest();
document.body.appendChild(root);
Expand Down
Loading