| OLD | NEW |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** Same as paper-menu-button's custom easing cubic-bezier param. */ | 5 /** Same as paper-menu-button's custom easing cubic-bezier param. */ |
| 6 var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)'; | 6 var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)'; |
| 7 var FADE_CUBIC_BEZIER = 'cubic-bezier(0.4, 0, 0.2, 1)'; | |
| 8 | 7 |
| 9 Polymer({ | 8 Polymer({ |
| 10 is: 'cr-shared-menu', | 9 is: 'cr-shared-menu', |
| 11 | 10 |
| 11 behaviors: [Polymer.IronA11yKeysBehavior], |
| 12 |
| 12 properties: { | 13 properties: { |
| 13 menuOpen: { | 14 menuOpen: { |
| 14 type: Boolean, | 15 type: Boolean, |
| 16 observer: 'menuOpenChanged_', |
| 15 value: false, | 17 value: false, |
| 16 }, | 18 }, |
| 17 | 19 |
| 18 /** | 20 /** |
| 19 * The contextual item that this menu was clicked for. | 21 * The contextual item that this menu was clicked for. |
| 20 * e.g. the data used to render an item in an <iron-list> or <dom-repeat> | 22 * e.g. the data used to render an item in an <iron-list> or <dom-repeat> |
| 21 * @type {?Object} | 23 * @type {?Object} |
| 22 */ | 24 */ |
| 23 itemData: { | 25 itemData: { |
| 24 type: Object, | 26 type: Object, |
| 25 value: null, | 27 value: null, |
| 26 }, | 28 }, |
| 29 |
| 30 /** @override */ |
| 31 keyEventTarget: { |
| 32 type: Object, |
| 33 value: function() { |
| 34 return this.$.menu; |
| 35 } |
| 36 }, |
| 37 |
| 38 openAnimationConfig: { |
| 39 type: Object, |
| 40 value: function() { |
| 41 return [{ |
| 42 name: 'fade-in-animation', |
| 43 timing: { |
| 44 delay: 50, |
| 45 duration: 200 |
| 46 } |
| 47 }, { |
| 48 name: 'paper-menu-grow-width-animation', |
| 49 timing: { |
| 50 delay: 50, |
| 51 duration: 150, |
| 52 easing: SLIDE_CUBIC_BEZIER |
| 53 } |
| 54 }, { |
| 55 name: 'paper-menu-grow-height-animation', |
| 56 timing: { |
| 57 delay: 100, |
| 58 duration: 275, |
| 59 easing: SLIDE_CUBIC_BEZIER |
| 60 } |
| 61 }]; |
| 62 } |
| 63 }, |
| 64 |
| 65 closeAnimationConfig: { |
| 66 type: Object, |
| 67 value: function() { |
| 68 return [{ |
| 69 name: 'fade-out-animation', |
| 70 timing: { |
| 71 duration: 150 |
| 72 } |
| 73 }, { |
| 74 name: 'paper-menu-shrink-width-animation', |
| 75 timing: { |
| 76 delay: 100, |
| 77 duration: 50, |
| 78 easing: SLIDE_CUBIC_BEZIER |
| 79 } |
| 80 }, { |
| 81 name: 'paper-menu-shrink-height-animation', |
| 82 timing: { |
| 83 delay: 200, |
| 84 easing: 'ease-in' |
| 85 } |
| 86 }]; |
| 87 } |
| 88 } |
| 89 }, |
| 90 |
| 91 keyBindings: { |
| 92 'tab': 'onTabPressed_', |
| 27 }, | 93 }, |
| 28 | 94 |
| 29 /** | 95 /** |
| 30 * Current animation being played, or null if there is none. | |
| 31 * @type {?Animation} | |
| 32 * @private | |
| 33 */ | |
| 34 animation_: null, | |
| 35 | |
| 36 /** | |
| 37 * The last anchor that was used to open a menu. It's necessary for toggling. | 96 * The last anchor that was used to open a menu. It's necessary for toggling. |
| 38 * @type {?Element} | 97 * @private {?Element} |
| 39 */ | 98 */ |
| 40 lastAnchor_: null, | 99 lastAnchor_: null, |
| 41 | 100 |
| 42 /** | 101 /** |
| 43 * Adds listeners to the window in order to dismiss the menu on resize and | 102 * The first focusable child in the menu's light DOM. |
| 44 * when escape is pressed. | 103 * @private {?Element} |
| 45 */ | 104 */ |
| 105 firstFocus_: null, |
| 106 |
| 107 /** |
| 108 * The last focusable child in the menu's light DOM. |
| 109 * @private {?Element} |
| 110 */ |
| 111 lastFocus_: null, |
| 112 |
| 113 /** @override */ |
| 46 attached: function() { | 114 attached: function() { |
| 47 window.addEventListener('resize', this.closeMenu.bind(this)); | 115 window.addEventListener('resize', this.closeMenu.bind(this)); |
| 48 window.addEventListener('keydown', function(e) { | 116 |
| 49 // Escape button on keyboard | 117 var focusableChildren = Polymer.dom(this).querySelectorAll( |
| 50 if (e.keyCode == 27) | 118 '[tabindex],button'); |
| 51 this.closeMenu(); | 119 if (focusableChildren.length > 0) { |
| 52 }.bind(this)); | 120 this.$.dropdown.focusTarget = focusableChildren[0]; |
| 121 this.firstFocus_ = focusableChildren[0]; |
| 122 this.lastFocus_ = focusableChildren[focusableChildren.length - 1]; |
| 123 } |
| 53 }, | 124 }, |
| 54 | 125 |
| 55 /** Closes the menu. */ | 126 /** Closes the menu. */ |
| 56 closeMenu: function() { | 127 closeMenu: function() { |
| 57 if (!this.menuOpen) | 128 if (this.root.activeElement == null) { |
| 58 return; | 129 // Something else has taken focus away from the menu. Do not attempt to |
| 59 // If there is a open menu animation going, cancel it and start closing. | 130 // restore focus to the button which opened the menu. |
| 60 this.cancelAnimation_(); | 131 this.$.dropdown.restoreFocusOnClose = false; |
| 132 } |
| 61 this.menuOpen = false; | 133 this.menuOpen = false; |
| 62 this.itemData = null; | 134 this.$.dropdown.restoreFocusOnClose = true; |
| 63 this.animation_ = this.animateClose_(); | |
| 64 this.animation_.addEventListener('finish', function() { | |
| 65 this.style.display = 'none'; | |
| 66 // Reset the animation for the next time the menu opens. | |
| 67 this.cancelAnimation_(); | |
| 68 }.bind(this)); | |
| 69 }, | 135 }, |
| 70 | 136 |
| 71 /** | 137 /** |
| 72 * Opens the menu at the anchor location. | 138 * Opens the menu at the anchor location. |
| 73 * @param {!Element} anchor The location to display the menu. | 139 * @param {!Element} anchor The location to display the menu. |
| 74 * @param {!Object} itemData The contextual item's data. | 140 * @param {!Object} itemData The contextual item's data. |
| 75 */ | 141 */ |
| 76 openMenu: function(anchor, itemData) { | 142 openMenu: function(anchor, itemData) { |
| 77 this.menuOpen = true; | |
| 78 this.style.display = 'block'; | |
| 79 this.itemData = itemData; | 143 this.itemData = itemData; |
| 80 this.lastAnchor_ = anchor; | 144 this.lastAnchor_ = anchor; |
| 81 | 145 |
| 82 // Move the menu to the anchor. | 146 // Move the menu to the anchor. |
| 83 var anchorRect = anchor.getBoundingClientRect(); | 147 this.$.dropdown.positionTarget = anchor; |
| 84 var parentRect = this.offsetParent.getBoundingClientRect(); | 148 this.menuOpen = true; |
| 85 | |
| 86 var left = (isRTL() ? anchorRect.left : anchorRect.right) - parentRect.left; | |
| 87 var top = anchorRect.top - parentRect.top; | |
| 88 | |
| 89 cr.ui.positionPopupAtPoint(left, top, this, cr.ui.AnchorType.BEFORE); | |
| 90 | |
| 91 // Handle the bottom of the screen. | |
| 92 if (this.getBoundingClientRect().top != anchorRect.top) { | |
| 93 var bottom = anchorRect.bottom - parentRect.top; | |
| 94 cr.ui.positionPopupAtPoint(left, bottom, this, cr.ui.AnchorType.BEFORE); | |
| 95 } | |
| 96 | |
| 97 this.$.menu.focus(); | |
| 98 | |
| 99 this.cancelAnimation_(); | |
| 100 this.animation_ = this.animateOpen_(); | |
| 101 }, | 149 }, |
| 102 | 150 |
| 103 /** | 151 /** |
| 104 * Toggles the menu for the anchor that is passed in. | 152 * Toggles the menu for the anchor that is passed in. |
| 105 * @param {!Element} anchor The location to display the menu. | 153 * @param {!Element} anchor The location to display the menu. |
| 106 * @param {!Object} itemData The contextual item's data. | 154 * @param {!Object} itemData The contextual item's data. |
| 107 */ | 155 */ |
| 108 toggleMenu: function(anchor, itemData) { | 156 toggleMenu: function(anchor, itemData) { |
| 109 // If there is an animation going (e.g. user clicks too fast), cancel it and | |
| 110 // start the new action. | |
| 111 this.cancelAnimation_(); | |
| 112 if (anchor == this.lastAnchor_ && this.menuOpen) | 157 if (anchor == this.lastAnchor_ && this.menuOpen) |
| 113 this.closeMenu(); | 158 this.closeMenu(); |
| 114 else | 159 else |
| 115 this.openMenu(anchor, itemData); | 160 this.openMenu(anchor, itemData); |
| 116 }, | 161 }, |
| 117 | 162 |
| 118 /** @private */ | 163 /** |
| 119 cancelAnimation_: function() { | 164 * Trap focus inside the menu. As a very basic heuristic, will wrap focus from |
| 120 if (this.animation_) { | 165 * the first element with a nonzero tabindex to the last such element. |
| 121 this.animation_.cancel(); | 166 * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available |
| 122 this.animation_ = null; | 167 * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179). |
| 123 } | 168 * @param {CustomEvent} e |
| 169 */ |
| 170 onTabPressed_: function(e) { |
| 171 if (!this.firstFocus_ || !this.lastFocus_) |
| 172 return; |
| 173 |
| 174 var toFocus; |
| 175 var keyEvent = e.detail.keyboardEvent; |
| 176 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_) |
| 177 toFocus = this.lastFocus_; |
| 178 else if (keyEvent.target == this.lastFocus_) |
| 179 toFocus = this.firstFocus_; |
| 180 |
| 181 if (!toFocus) |
| 182 return; |
| 183 |
| 184 e.preventDefault(); |
| 185 toFocus.focus(); |
| 124 }, | 186 }, |
| 125 | 187 |
| 126 /** | 188 /** |
| 127 * @param {!Array<!KeyframeEffect>} effects | 189 * Ensure the menu is reset properly when it is closed by the dropdown (eg, |
| 128 * @return {!Animation} | 190 * clicking outside). |
| 191 * @private |
| 129 */ | 192 */ |
| 130 playEffects: function(effects) { | 193 menuOpenChanged_: function() { |
| 131 /** @type {function(new:Object, !Array<!KeyframeEffect>)} */ | 194 if (!this.menuOpen) |
| 132 window.GroupEffect; | 195 this.itemData = null; |
| 133 | |
| 134 /** @type {{play: function(Object): !Animation}} */ | |
| 135 document.timeline; | |
| 136 | |
| 137 return document.timeline.play(new window.GroupEffect(effects)); | |
| 138 }, | |
| 139 | |
| 140 /** | |
| 141 * Slide-in animation when opening the menu. The animation configuration is | |
| 142 * the same as paper-menu-button except for a shorter delay time. | |
| 143 * @private | |
| 144 * @return {!Animation} | |
| 145 */ | |
| 146 animateOpen_: function() { | |
| 147 var rect = this.getBoundingClientRect(); | |
| 148 var height = rect.height; | |
| 149 var width = rect.width; | |
| 150 | |
| 151 var fadeIn = new KeyframeEffect(/** @type {Animatable} */(this), [{ | |
| 152 'opacity': '0' | |
| 153 }, { | |
| 154 'opacity': '1' | |
| 155 }], /** @type {!KeyframeEffectOptions} */({ | |
| 156 delay: 50, | |
| 157 duration: 200, | |
| 158 easing: FADE_CUBIC_BEZIER, | |
| 159 fill: 'both' | |
| 160 })); | |
| 161 | |
| 162 var growHeight = new KeyframeEffect(/** @type {Animatable} */(this), [{ | |
| 163 height: (height / 2) + 'px' | |
| 164 }, { | |
| 165 height: height + 'px' | |
| 166 }], /** @type {!KeyframeEffectOptions} */({ | |
| 167 delay: 50, | |
| 168 duration: 275, | |
| 169 easing: SLIDE_CUBIC_BEZIER, | |
| 170 fill: 'both' | |
| 171 })); | |
| 172 | |
| 173 var growWidth = new KeyframeEffect(/** @type {Animatable} */(this), [{ | |
| 174 width: (width / 2) + 'px' | |
| 175 }, { | |
| 176 width: width + 'px' | |
| 177 }], /** @type {!KeyframeEffectOptions} */({ | |
| 178 delay: 50, | |
| 179 duration: 150, | |
| 180 easing: SLIDE_CUBIC_BEZIER, | |
| 181 fill: 'both' | |
| 182 })); | |
| 183 | |
| 184 return this.playEffects([fadeIn, growHeight, growWidth]); | |
| 185 }, | |
| 186 | |
| 187 /** | |
| 188 * Slide-out animation when closing the menu. The animation configuration is | |
| 189 * the same as paper-menu-button. | |
| 190 * @private | |
| 191 * @return {!Animation} | |
| 192 */ | |
| 193 animateClose_: function() { | |
| 194 var rect = this.getBoundingClientRect(); | |
| 195 var height = rect.height; | |
| 196 var width = rect.width; | |
| 197 | |
| 198 var fadeOut = new KeyframeEffect(/** @type {Animatable} */(this), [{ | |
| 199 'opacity': '1' | |
| 200 }, { | |
| 201 'opacity': '0' | |
| 202 }], /** @type {!KeyframeEffectOptions} */({ | |
| 203 duration: 150, | |
| 204 easing: FADE_CUBIC_BEZIER, | |
| 205 fill: 'both' | |
| 206 })); | |
| 207 | |
| 208 var shrinkHeight = new KeyframeEffect(/** @type {Animatable} */(this), [{ | |
| 209 height: height + 'px', | |
| 210 transform: 'translateY(0)' | |
| 211 }, { | |
| 212 height: height / 2 + 'px', | |
| 213 transform: 'translateY(-20px)' | |
| 214 }], /** @type {!KeyframeEffectOptions} */({ | |
| 215 duration: 200, | |
| 216 easing: 'ease-in', | |
| 217 fill: 'both' | |
| 218 })); | |
| 219 | |
| 220 var shrinkWidth = new KeyframeEffect(/** @type {Animatable} */(this), [{ | |
| 221 width: width + 'px' | |
| 222 }, { | |
| 223 width: width - (width / 20) + 'px' | |
| 224 }], /** @type {!KeyframeEffectOptions} */({ | |
| 225 delay: 100, | |
| 226 duration: 50, | |
| 227 easing: SLIDE_CUBIC_BEZIER, | |
| 228 fill: 'both' | |
| 229 })); | |
| 230 | |
| 231 return this.playEffects([fadeOut, shrinkHeight, shrinkWidth]); | |
| 232 }, | 196 }, |
| 233 }); | 197 }); |
| OLD | NEW |