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

feat(menu): Add quickOpen option. #2127

Merged
merged 9 commits into from
Feb 1, 2018
8 changes: 8 additions & 0 deletions demos/menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@
<div>
<label><input type="checkbox" name="remember"> Remember Selected Item</label>
</div>
<div>
<label><input type="checkbox" name="animation"> Disable Open Animation</label>
</div>
<p>
<div class="left-column-controls">
Menu Sizes:
Expand Down Expand Up @@ -265,6 +268,11 @@
menu.rememberSelection = evt.target.checked;
});

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

var radios = document.querySelectorAll('input[name="position"]');
var anchor = document.querySelector('.mdc-menu-anchor');
// Initialize to top left.
Expand Down
5 changes: 5 additions & 0 deletions packages/mdc-menu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ CSS Class | Description

// Set Anchor Corner to Bottom End
menu.setAnchorCorner(Corner.BOTTOM_END);

// Turn off menu open animations
menu.quickOpen = false;
```

### `MDCMenu`
Expand All @@ -150,6 +153,7 @@ Property | Value Type | Description
`open` | Boolean | Proxies to the foundation's `isOpen`/(`open`, `close`) methods.
`items` | Array<Element> | Proxies to the foundation's container to query for all `.mdc-list-item[role]` elements.
`itemsContainer` | Element | Queries the foundation's root element for the `mdc-menu__items` container element.
`quickOpen` | Boolean | Proxies to the foundation's `setQuickOpen()` method.

Method Signature | Description
--- | ---
Expand Down Expand Up @@ -201,6 +205,7 @@ Method Signature | Description
`open({focusIndex: ?number}) => void` | Opens the menu. Optionally accepts an object with a `focusIndex` parameter to indicate which list item should receive focus when the menu is opened.
`close(evt: ?Event)` | Closes the menu. Optionally accepts the event to check if the target is disabled before closing the menu.
`isOpen() => boolean` | Returns a boolean indicating whether the menu is open.
`setQuickOpen(quickOpen: boolean) => void` | Sets whether the menu should open and close without animation when the `open`/`close` methods are called.

### Events

Expand Down
38 changes: 28 additions & 10 deletions packages/mdc-menu/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ class MDCMenuFoundation extends MDCFoundation {
this.selectedIndex_ = -1;
/** @private {boolean} */
this.rememberSelection_ = false;
/** @private {boolean} */
this.quickOpen_ = 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
Expand Down Expand Up @@ -208,6 +210,11 @@ class MDCMenuFoundation extends MDCFoundation {
this.setSelectedIndex(-1);
}

/** @param {boolean} quickOpen */
setQuickOpen(quickOpen) {
this.quickOpen_ = quickOpen;
}

/**
* @param {?number} focusIndex
* @private
Expand Down Expand Up @@ -555,18 +562,23 @@ class MDCMenuFoundation extends MDCFoundation {
*/
open({focusIndex = null} = {}) {
this.adapter_.saveFocus();
this.adapter_.addClass(MDCMenuFoundation.cssClasses.ANIMATING_OPEN);

if (!this.quickOpen_) {
this.adapter_.addClass(MDCMenuFoundation.cssClasses.ANIMATING_OPEN);
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably also conditionalize the setTimeout code below (and same for within close), although it's effectively a no-op in this case right now.

}

this.animationRequestId_ = requestAnimationFrame(() => {
this.dimensions_ = this.adapter_.getInnerDimensions();
this.autoPosition_();
this.adapter_.addClass(MDCMenuFoundation.cssClasses.OPEN);
this.focusOnOpen_(focusIndex);
this.adapter_.registerBodyClickHandler(this.documentClickHandler_);
this.openAnimationEndTimerId_ = setTimeout(() => {
this.openAnimationEndTimerId_ = 0;
this.adapter_.removeClass(MDCMenuFoundation.cssClasses.ANIMATING_OPEN);
}, numbers.TRANSITION_OPEN_DURATION);
if (!this.quickOpen_) {
this.openAnimationEndTimerId_ = setTimeout(() => {
this.openAnimationEndTimerId_ = 0;
this.adapter_.removeClass(MDCMenuFoundation.cssClasses.ANIMATING_OPEN);
}, numbers.TRANSITION_OPEN_DURATION);
}
});
this.isOpen_ = true;
}
Expand All @@ -585,13 +597,19 @@ class MDCMenuFoundation extends MDCFoundation {
}

this.adapter_.deregisterBodyClickHandler(this.documentClickHandler_);
this.adapter_.addClass(MDCMenuFoundation.cssClasses.ANIMATING_CLOSED);

if (!this.quickOpen_) {
this.adapter_.addClass(MDCMenuFoundation.cssClasses.ANIMATING_CLOSED);
}

requestAnimationFrame(() => {
this.adapter_.removeClass(MDCMenuFoundation.cssClasses.OPEN);
this.closeAnimationEndTimerId_ = setTimeout(() => {
this.closeAnimationEndTimerId_ = 0;
this.adapter_.removeClass(MDCMenuFoundation.cssClasses.ANIMATING_CLOSED);
}, numbers.TRANSITION_CLOSE_DURATION);
if (!this.quickOpen_) {
this.closeAnimationEndTimerId_ = setTimeout(() => {
this.closeAnimationEndTimerId_ = 0;
this.adapter_.removeClass(MDCMenuFoundation.cssClasses.ANIMATING_CLOSED);
}, numbers.TRANSITION_CLOSE_DURATION);
}
});
this.isOpen_ = false;
this.adapter_.restoreFocus();
Expand Down
5 changes: 5 additions & 0 deletions packages/mdc-menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ class MDCMenu extends MDCComponent {
this.foundation_.setRememberSelection(rememberSelection);
}

/** @param {boolean} quickOpen */
set quickOpen(quickOpen) {
this.foundation_.setQuickOpen(quickOpen);
}

/** @return {!MDCMenuFoundation} */
getDefaultFoundation() {
return new MDCMenuFoundation({
Expand Down
6 changes: 6 additions & 0 deletions test/unit/mdc-menu/mdc-simple-menu.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ test('rememberSelection', () => {
// The method sets private variable on the foundation, nothing to verify.
});

test('setQuickOpen', () => {
const {component} = setupTest();
component.quickOpen = false;
// 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
18 changes: 16 additions & 2 deletions test/unit/mdc-menu/simple.foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,15 @@ test('#init throws error when the necessary DOM is not present', () => {
testFoundation('#open adds the animation class to start an animation',
({foundation, mockAdapter}) => {
foundation.open();
td.verify(mockAdapter.addClass(cssClasses.ANIMATING_OPEN));
td.verify(mockAdapter.addClass(cssClasses.ANIMATING_OPEN), {times: 1});
});

testFoundation('#open does not add the animation class to start an animation when setQuickOpen is false',
({foundation, mockAdapter}) => {
foundation.setQuickOpen(true);
foundation.open();
td.verify(mockAdapter.addClass(cssClasses.ANIMATING_OPEN), {times: 0});
td.verify(mockAdapter.removeClass(cssClasses.ANIMATING_OPEN), {times: 0});
});

testFoundation('#open adds the open class to the menu', ({foundation, mockAdapter, mockRaf}) => {
Expand Down Expand Up @@ -476,6 +484,13 @@ testFoundation('#close adds the animation class to start an animation', ({founda
td.verify(mockAdapter.addClass(cssClasses.ANIMATING_CLOSED));
});

testFoundation('#close does not add animation class if quickOpen is set to true', ({foundation, mockAdapter}) => {
foundation.setQuickOpen(true);
foundation.close();
td.verify(mockAdapter.addClass(cssClasses.ANIMATING_CLOSED), {times: 0});
td.verify(mockAdapter.removeClass(cssClasses.ANIMATING_CLOSED), {times: 0});
});

testFoundation('#close removes the open class from the menu', ({foundation, mockAdapter, mockRaf}) => {
foundation.close();
mockRaf.flush();
Expand Down Expand Up @@ -1229,4 +1244,3 @@ test('getSelectedValue should return the last selected item', () => {
assert.isTrue(foundation.getSelectedIndex() === expectedIndex);
clock.uninstall();
});