Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(menu): Add --selected class to menu items #2084

Merged
merged 10 commits into from
Jan 30, 2018
12 changes: 10 additions & 2 deletions demos/menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,15 @@
<label>R: <input type="text" id="right-margin" value="0" size="3"></label>
</div>
</p>
<div class="left-column-controls">
<div>
<label><input type="checkbox" name="is-rtl"> RTL</label>
</div>
<div class="right-column-controls">
<div>
<label><input type="checkbox" name="dark"> Dark mode</label>
</div>
<div>
<label><input type="checkbox" name="remember"> Remember Selected Item</label>
</div>
<p>
<div class="left-column-controls">
Menu Sizes:
Expand Down Expand Up @@ -277,6 +280,11 @@
}
});

var remember = document.querySelector('input[name="remember"]');
remember.addEventListener('change', function(evt) {
menu.rememberSelection = evt.target.checked;
});

var radios = document.querySelectorAll('input[name="position"]');
var anchor = document.querySelector('.mdc-menu-anchor');
// Initialize to top left.
Expand Down
25 changes: 25 additions & 0 deletions packages/mdc-menu/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,31 @@ class MDCMenuAdapter {

/** @param {string} height */
setMaxHeight(height) {}

/**
* @param {number} index
* @param {string} attr
* @param {string} value
*/
setAttrForOptionAtIndex(index, attr, value) {}

/**
* @param {number} index
* @param {string} attr
*/
rmAttrForOptionAtIndex(index, attr) {}

/**
* @param {number} index
* @param {string} className
*/
addClassForOptionAtIndex(index, className) {}

/**
* @param {number} index
* @param {string} className
*/
rmClassForOptionAtIndex(index, className) {}
}

export {MDCMenuAdapter};
1 change: 1 addition & 0 deletions packages/mdc-menu/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const cssClasses = {
ANIMATING_OPEN: 'mdc-menu--animating-open',
ANIMATING_CLOSED: 'mdc-menu--animating-closed',
LIST_ITEM: 'mdc-list-item',
SELECTED_LIST_ITEM: 'mdc-list-item--selected',
Copy link
Contributor

Choose a reason for hiding this comment

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

you should be able to reference the constants on MDCListFoundation.strings

Copy link
Contributor Author

Choose a reason for hiding this comment

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

MDCListFoundation doesn't exist

};

/** @enum {string} */
Expand Down
67 changes: 65 additions & 2 deletions packages/mdc-menu/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ class MDCMenuFoundation extends MDCFoundation {
setTransformOrigin: () => {},
setPosition: () => {},
setMaxHeight: () => {},
setAttrForOptionAtIndex: () => {},
rmAttrForOptionAtIndex: () => {},
addClassForOptionAtIndex: () => {},
rmClassForOptionAtIndex: () => {},
});
}

Expand Down Expand Up @@ -137,6 +141,16 @@ class MDCMenuFoundation extends MDCFoundation {
this.anchorMargin_ = {top: 0, right: 0, bottom: 0, left: 0};
/** @private {?AutoLayoutMeasurements} */
this.measures_ = null;
/** @private {number} */
this.selectedIndex_ = -1;
/** @private {boolean} */
this.rememberSelection_ = false;

// A keyup event on the menu needs to have a corresponding keydown
// event on the menu. If the user opens the menu with a keydown event on a
// button, the menu will only get the key up event causing buggy behavior with selected elements.
/** @private {boolean} */
this.keyDownWithinMenu_ = false;
}

init() {
Expand Down Expand Up @@ -188,13 +202,25 @@ class MDCMenuFoundation extends MDCFoundation {
this.anchorMargin_.left = typeof margin.left === 'number' ? margin.left : 0;
}

/** @param {boolean} rememberSelection */
setRememberSelection(rememberSelection) {
this.rememberSelection_ = rememberSelection;
this.setSelectedIndex(-1);
}

/**
* @param {?number} focusIndex
* @private
*/
focusOnOpen_(focusIndex) {
if (focusIndex === null) {
// First, try focusing the menu.
// If this instance of MDCMenu remembers selections, and the user has
// made a selection, then focus the last selected item
if (this.rememberSelection_ && this.selectedIndex_ >= 0) {
this.adapter_.focusItemAtIndex(this.selectedIndex_);
return;
}

this.adapter_.focus();
// If that doesn't work, focus first item instead.
if (!this.adapter_.isFocused()) {
Expand Down Expand Up @@ -241,6 +267,9 @@ class MDCMenuFoundation extends MDCFoundation {
const isArrowUp = key === 'ArrowUp' || keyCode === 38;
const isArrowDown = key === 'ArrowDown' || keyCode === 40;
const isSpace = key === 'Space' || keyCode === 32;
const isEnter = key === 'Enter' || keyCode === 13;
// The menu needs to know if the keydown event was triggered on the menu
this.keyDownWithinMenu_ = isEnter || isSpace;

const focusedItemIndex = this.adapter_.getFocusedItemIndex();
const lastItemIndex = this.adapter_.getNumberOfItems() - 1;
Expand Down Expand Up @@ -297,7 +326,12 @@ class MDCMenuFoundation extends MDCFoundation {
const isEscape = key === 'Escape' || keyCode === 27;

if (isEnter || isSpace) {
this.handlePossibleSelected_(evt);
// If the keydown event didn't occur on the menu, then it should
// disregard the possible selected event.
if (this.keyDownWithinMenu_) {
this.handlePossibleSelected_(evt);
}
this.keyDownWithinMenu_ = false;
}

if (isEscape) {
Expand Down Expand Up @@ -327,6 +361,9 @@ class MDCMenuFoundation extends MDCFoundation {
this.selectedTriggerTimerId_ = setTimeout(() => {
this.selectedTriggerTimerId_ = 0;
this.close();
if (this.rememberSelection_) {
this.setSelectedIndex(targetIndex);
}
this.adapter_.notifySelected({index: targetIndex});
}, numbers.SELECTED_TRIGGER_DELAY);
}
Expand Down Expand Up @@ -564,6 +601,32 @@ class MDCMenuFoundation extends MDCFoundation {
isOpen() {
return this.isOpen_;
}

/** @return {number} */
getSelectedIndex() {
return this.selectedIndex_;
}

/**
* @param {number} index Index of the item to set as selected.
*/
setSelectedIndex(index) {
if (index === this.selectedIndex_) {
return;
}

const prevSelectedIndex = this.selectedIndex_;
if (prevSelectedIndex >= 0) {
this.adapter_.rmAttrForOptionAtIndex(prevSelectedIndex, 'aria-selected');
this.adapter_.rmClassForOptionAtIndex(prevSelectedIndex, cssClasses.SELECTED_LIST_ITEM);
}

this.selectedIndex_ = index >= 0 && index < this.adapter_.getNumberOfItems() ? index : -1;
if (this.selectedIndex_ >= 0) {
this.adapter_.setAttrForOptionAtIndex(this.selectedIndex_, 'aria-selected', 'true');
this.adapter_.addClassForOptionAtIndex(this.selectedIndex_, cssClasses.SELECTED_LIST_ITEM);
}
}
}

export {MDCMenuFoundation, AnchorMargin};
34 changes: 34 additions & 0 deletions packages/mdc-menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,36 @@ class MDCMenu extends MDCComponent {
return [].slice.call(itemsContainer.querySelectorAll('.mdc-list-item[role]'));
}

/**
* Return the item within the menu that is selected.
* @param {number} index
* @return {?Element}
*/
getOptionByIndex(index) {
const items = this.items;

if (index < items.length) {
return this.items[index];
} else {
return null;
}
}

/** @param {number} index */
set selectedItemIndex(index) {
this.foundation_.setSelectedIndex(index);
}

/** @return {number} */
get selectedItemIndex() {
return this.foundation_.getSelectedIndex();
}

/** @param {!boolean} rememberSelection */
set rememberSelection(rememberSelection) {
this.foundation_.setRememberSelection(rememberSelection);
}

/** @return {!MDCMenuFoundation} */
getDefaultFoundation() {
return new MDCMenuFoundation({
Expand Down Expand Up @@ -150,6 +180,10 @@ class MDCMenu extends MDCComponent {
setMaxHeight: (height) => {
this.root_.style.maxHeight = height;
},
setAttrForOptionAtIndex: (index, attr, value) => this.items[index].setAttribute(attr, value),
rmAttrForOptionAtIndex: (index, attr) => this.items[index].removeAttribute(attr),
addClassForOptionAtIndex: (index, className) => this.items[index].classList.add(className),
rmClassForOptionAtIndex: (index, className) => this.items[index].classList.remove(className),
});
}
}
Expand Down
56 changes: 56 additions & 0 deletions test/unit/mdc-menu/mdc-simple-menu.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ test('setAnchorMargin', () => {
// The method sets private variable on the foundation, nothing to verify.
});

test('selectedItem', () => {
const {component} = setupTest();
assert.isOk(!component.selectedItem);
});

test('rememberSelection', () => {
const {component} = setupTest();
component.rememberSelection = true;
// The method sets private variable on the foundation, nothing to verify.
});

test('items returns all menu items', () => {
const {root, component} = setupTest();
const items = [].slice.call(root.querySelectorAll('[role="menuitem"]'));
Expand Down Expand Up @@ -381,3 +392,48 @@ test('adapter#setMaxHeight sets the maxHeight style on the menu element', () =>
component.getDefaultFoundation().adapter_.setMaxHeight('100px');
assert.equal(root.style.maxHeight, '100px');
});

test('adapter#getOptionByIndex returns the option at the index specified', () => {
const {root, component} = setupTest();
const item1 = root.querySelectorAll('[role="menuitem"]')[1];
document.body.appendChild(root);
const element = component.getOptionByIndex(1);
assert.equal(element, item1);
document.body.removeChild(root);
});

test('adapter#setAttrForOptionAtIndex sets an attribute on the option element at the index specified', () => {
const {root, component} = setupTest();
const item1 = root.querySelectorAll('[role="menuitem"]')[1];
document.body.appendChild(root);
component.getDefaultFoundation().adapter_.setAttrForOptionAtIndex(1, 'aria-disabled', 'true');
assert.equal(root.querySelector('[aria-disabled="true"]'), item1);
document.body.removeChild(root);
});

test('adapter#rmAttrForOptionAtIndex removes an attribute on the option element at the index', () => {
const {root, component} = setupTest();
document.body.appendChild(root);
component.getDefaultFoundation().adapter_.setAttrForOptionAtIndex(1, 'aria-disabled', 'true');
component.getDefaultFoundation().adapter_.rmAttrForOptionAtIndex(1, 'aria-disabled');
assert.equal(root.querySelector('[aria-disabled="true"]'), null);
document.body.removeChild(root);
});

test('adapter#addClassForOptionAtIndex adds a class to the option at the index specified', () => {
const {root, component} = setupTest();
const item1 = root.querySelectorAll('[role="menuitem"]')[1];
document.body.appendChild(root);
component.getDefaultFoundation().adapter_.addClassForOptionAtIndex(1, 'test-class');
assert.equal(root.querySelector('.test-class'), item1);
document.body.removeChild(root);
});

test('adapter#rmClassForOptionAtIndex removes a class from the option at the index specified', () => {
const {root, component} = setupTest();
document.body.appendChild(root);
component.getDefaultFoundation().adapter_.addClassForOptionAtIndex(1, 'test-class');
component.getDefaultFoundation().adapter_.rmClassForOptionAtIndex(1, 'test-class');
assert.equal(root.querySelector('.test-class'), null);
document.body.removeChild(root);
});
Loading