Chromium Code Reviews| Index: ui/webui/resources/cr_elements/cr_shared_menu/cr_shared_menu.js |
| diff --git a/ui/webui/resources/cr_elements/cr_shared_menu/cr_shared_menu.js b/ui/webui/resources/cr_elements/cr_shared_menu/cr_shared_menu.js |
| index ab746bdf9f4df1b0af4a6f2f285f95b5f4d39b22..78fb44d51692db764d659d575dc19441dba8a328 100644 |
| --- a/ui/webui/resources/cr_elements/cr_shared_menu/cr_shared_menu.js |
| +++ b/ui/webui/resources/cr_elements/cr_shared_menu/cr_shared_menu.js |
| @@ -4,14 +4,16 @@ |
| /** Same as paper-menu-button's custom easing cubic-bezier param. */ |
| var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)'; |
| -var FADE_CUBIC_BEZIER = 'cubic-bezier(0.4, 0, 0.2, 1)'; |
| Polymer({ |
| is: 'cr-shared-menu', |
| + behaviors: [Polymer.IronA11yKeysBehavior], |
| + |
| properties: { |
| menuOpen: { |
| type: Boolean, |
| + observer: 'menuOpenChanged_', |
| value: false, |
| }, |
| @@ -24,48 +26,118 @@ Polymer({ |
| type: Object, |
| value: null, |
| }, |
| + |
| + keyEventTarget: { |
| + type: Object, |
| + value: function() { |
| + return this.$.menu; |
| + } |
| + }, |
| + |
| + openAnimationConfig: { |
| + type: Object, |
| + value: function() { |
| + return [{ |
| + name: 'fade-in-animation', |
| + timing: { |
| + delay: 50, |
| + duration: 200 |
| + } |
| + }, { |
| + name: 'paper-menu-grow-width-animation', |
| + timing: { |
| + delay: 50, |
| + duration: 150, |
| + easing: SLIDE_CUBIC_BEZIER |
| + } |
| + }, { |
| + name: 'paper-menu-grow-height-animation', |
| + timing: { |
| + delay: 100, |
| + duration: 275, |
| + easing: SLIDE_CUBIC_BEZIER |
| + } |
| + }]; |
| + } |
| + }, |
| + |
| + closeAnimationConfig: { |
| + type: Object, |
| + value: function() { |
| + return [{ |
| + name: 'fade-out-animation', |
| + timing: { |
| + duration: 150 |
| + } |
| + }, { |
| + name: 'paper-menu-shrink-width-animation', |
| + timing: { |
| + delay: 100, |
| + duration: 50, |
| + easing: SLIDE_CUBIC_BEZIER |
| + } |
| + }, { |
| + name: 'paper-menu-shrink-height-animation', |
| + timing: { |
| + delay: 200, |
| + easing: 'ease-in' |
| + } |
| + }]; |
| + } |
| + } |
| + }, |
| + |
| + keyBindings: { |
| + 'tab': 'onTabPressed_', |
| }, |
| /** |
| - * Current animation being played, or null if there is none. |
| - * @type {?Animation} |
| + * The last anchor that was used to open a menu. It's necessary for toggling. |
| + * @type {?Element} |
| * @private |
| */ |
| - animation_: null, |
| + lastAnchor_: null, |
| /** |
| - * The last anchor that was used to open a menu. It's necessary for toggling. |
| + * The first focusable child in the menu's light DOM. |
| + * @private |
| * @type {?Element} |
| */ |
| - lastAnchor_: null, |
| + firstFocus_: null, |
| /** |
| - * Adds listeners to the window in order to dismiss the menu on resize and |
| - * when escape is pressed. |
| + * The last focusable child in the menu's light DOM. |
| + * @private |
| + * @type {?Element} |
| */ |
| + lastFocus_: null, |
| + |
| + /** @override */ |
| attached: function() { |
| window.addEventListener('resize', this.closeMenu.bind(this)); |
| - window.addEventListener('keydown', function(e) { |
| - // Escape button on keyboard |
| - if (e.keyCode == 27) |
| - this.closeMenu(); |
| - }.bind(this)); |
| + |
| + var focusableChildren = Polymer.dom(this).querySelectorAll( |
| + '[tabindex],button'); |
| + if (focusableChildren.length > 0) { |
| + this.$.dropdown.focusTarget = focusableChildren[0]; |
| + this.firstFocus_ = focusableChildren[0]; |
| + this.lastFocus_ = focusableChildren[focusableChildren.length - 1]; |
|
calamity
2016/07/06 06:49:15
Do you need to recompute this if new elements are
tsergeant
2016/07/06 07:16:29
Ideally, yes. However, I'd like to keep this as si
|
| + } |
| }, |
| /** Closes the menu. */ |
| closeMenu: function() { |
| - if (!this.menuOpen) |
| - return; |
| - // If there is a open menu animation going, cancel it and start closing. |
| - this.cancelAnimation_(); |
| this.menuOpen = false; |
| - this.itemData = null; |
| - this.animation_ = this.animateClose_(); |
| - this.animation_.addEventListener('finish', function() { |
| - this.style.display = 'none'; |
| - // Reset the animation for the next time the menu opens. |
| - this.cancelAnimation_(); |
| - }.bind(this)); |
| + }, |
| + |
| + /** |
| + * Close the menu without refocusing the menu button which opened it. Should |
| + * be used when the menu action causes another element to be focused. |
| + */ |
| + closeMenuNoRefocus: function() { |
| + this.$.dropdown.restoreFocusOnClose = false; |
| + this.closeMenu(); |
| + this.$.dropdown.restoreFocusOnClose = true; |
| }, |
| /** |
| @@ -74,30 +146,12 @@ Polymer({ |
| * @param {!Object} itemData The contextual item's data. |
| */ |
| openMenu: function(anchor, itemData) { |
| - this.menuOpen = true; |
| - this.style.display = 'block'; |
| this.itemData = itemData; |
| this.lastAnchor_ = anchor; |
| // Move the menu to the anchor. |
| - var anchorRect = anchor.getBoundingClientRect(); |
| - var parentRect = this.offsetParent.getBoundingClientRect(); |
| - |
| - var left = (isRTL() ? anchorRect.left : anchorRect.right) - parentRect.left; |
| - var top = anchorRect.top - parentRect.top; |
| - |
| - cr.ui.positionPopupAtPoint(left, top, this, cr.ui.AnchorType.BEFORE); |
| - |
| - // Handle the bottom of the screen. |
| - if (this.getBoundingClientRect().top != anchorRect.top) { |
| - var bottom = anchorRect.bottom - parentRect.top; |
| - cr.ui.positionPopupAtPoint(left, bottom, this, cr.ui.AnchorType.BEFORE); |
| - } |
| - |
| - this.$.menu.focus(); |
| - |
| - this.cancelAnimation_(); |
| - this.animation_ = this.animateOpen_(); |
| + this.$.dropdown.positionTarget = anchor; |
| + this.menuOpen = true; |
| }, |
| /** |
| @@ -106,128 +160,40 @@ Polymer({ |
| * @param {!Object} itemData The contextual item's data. |
| */ |
| toggleMenu: function(anchor, itemData) { |
| - // If there is an animation going (e.g. user clicks too fast), cancel it and |
| - // start the new action. |
| - this.cancelAnimation_(); |
| if (anchor == this.lastAnchor_ && this.menuOpen) |
| this.closeMenu(); |
| else |
| this.openMenu(anchor, itemData); |
| }, |
| - /** @private */ |
| - cancelAnimation_: function() { |
| - if (this.animation_) { |
| - this.animation_.cancel(); |
| - this.animation_ = null; |
| - } |
| - }, |
| - |
| /** |
| - * @param {!Array<!KeyframeEffect>} effects |
| - * @return {!Animation} |
| + * Trap focus inside the menu. As a very basic heuristic, will wrap focus from |
| + * the first element with a nonzero tabindex to the last such element. |
| + * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available |
| + * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179). |
| + * @param {CustomEvent} e |
| */ |
| - playEffects: function(effects) { |
| - /** @type {function(new:Object, !Array<!KeyframeEffect>)} */ |
| - window.GroupEffect; |
| - |
| - /** @type {{play: function(Object): !Animation}} */ |
| - document.timeline; |
| - |
| - return document.timeline.play(new window.GroupEffect(effects)); |
| - }, |
| - |
| - /** |
| - * Slide-in animation when opening the menu. The animation configuration is |
| - * the same as paper-menu-button except for a shorter delay time. |
| - * @private |
| - * @return {!Animation} |
| - */ |
| - animateOpen_: function() { |
| - var rect = this.getBoundingClientRect(); |
| - var height = rect.height; |
| - var width = rect.width; |
| - |
| - var fadeIn = new KeyframeEffect(/** @type {Animatable} */(this), [{ |
| - 'opacity': '0' |
| - }, { |
| - 'opacity': '1' |
| - }], /** @type {!KeyframeEffectOptions} */({ |
| - delay: 50, |
| - duration: 200, |
| - easing: FADE_CUBIC_BEZIER, |
| - fill: 'both' |
| - })); |
| - |
| - var growHeight = new KeyframeEffect(/** @type {Animatable} */(this), [{ |
| - height: (height / 2) + 'px' |
| - }, { |
| - height: height + 'px' |
| - }], /** @type {!KeyframeEffectOptions} */({ |
| - delay: 50, |
| - duration: 275, |
| - easing: SLIDE_CUBIC_BEZIER, |
| - fill: 'both' |
| - })); |
| - |
| - var growWidth = new KeyframeEffect(/** @type {Animatable} */(this), [{ |
| - width: (width / 2) + 'px' |
| - }, { |
| - width: width + 'px' |
| - }], /** @type {!KeyframeEffectOptions} */({ |
| - delay: 50, |
| - duration: 150, |
| - easing: SLIDE_CUBIC_BEZIER, |
| - fill: 'both' |
| - })); |
| - |
| - return this.playEffects([fadeIn, growHeight, growWidth]); |
| + onTabPressed_: function(e) { |
| + var keyEvent = e.detail.keyboardEvent; |
| + if (this.firstFocus_ && this.lastFocus_) { |
| + if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_) { |
| + e.preventDefault(); |
| + this.lastFocus_.focus(); |
| + } else if (keyEvent.target == this.lastFocus_) { |
| + e.preventDefault(); |
| + this.firstFocus_.focus(); |
| + } |
| + } |
| }, |
| /** |
| - * Slide-out animation when closing the menu. The animation configuration is |
| - * the same as paper-menu-button. |
| + * Ensure the menu is reset properly when it is closed by the dropdown (eg, |
| + * clicking outside). |
| * @private |
| - * @return {!Animation} |
| */ |
| - animateClose_: function() { |
| - var rect = this.getBoundingClientRect(); |
| - var height = rect.height; |
| - var width = rect.width; |
| - |
| - var fadeOut = new KeyframeEffect(/** @type {Animatable} */(this), [{ |
| - 'opacity': '1' |
| - }, { |
| - 'opacity': '0' |
| - }], /** @type {!KeyframeEffectOptions} */({ |
| - duration: 150, |
| - easing: FADE_CUBIC_BEZIER, |
| - fill: 'both' |
| - })); |
| - |
| - var shrinkHeight = new KeyframeEffect(/** @type {Animatable} */(this), [{ |
| - height: height + 'px', |
| - transform: 'translateY(0)' |
| - }, { |
| - height: height / 2 + 'px', |
| - transform: 'translateY(-20px)' |
| - }], /** @type {!KeyframeEffectOptions} */({ |
| - duration: 200, |
| - easing: 'ease-in', |
| - fill: 'both' |
| - })); |
| - |
| - var shrinkWidth = new KeyframeEffect(/** @type {Animatable} */(this), [{ |
| - width: width + 'px' |
| - }, { |
| - width: width - (width / 20) + 'px' |
| - }], /** @type {!KeyframeEffectOptions} */({ |
| - delay: 100, |
| - duration: 50, |
| - easing: SLIDE_CUBIC_BEZIER, |
| - fill: 'both' |
| - })); |
| - |
| - return this.playEffects([fadeOut, shrinkHeight, shrinkWidth]); |
| + menuOpenChanged_: function() { |
| + if (!this.menuOpen) { |
| + this.itemData = null; |
| + } |
| }, |
| }); |