| 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 | 7 |
| 8 Polymer({ | 8 Polymer({ |
| 9 is: 'cr-shared-menu', | 9 is: 'cr-shared-menu', |
| 10 | 10 |
| 11 behaviors: [Polymer.IronA11yKeysBehavior], | |
| 12 | |
| 13 properties: { | 11 properties: { |
| 14 menuOpen: { | 12 menuOpen: { |
| 15 type: Boolean, | 13 type: Boolean, |
| 16 observer: 'menuOpenChanged_', | 14 observer: 'menuOpenChanged_', |
| 17 value: false, | 15 value: false, |
| 18 notify: true, | 16 notify: true, |
| 19 }, | 17 }, |
| 20 | 18 |
| 21 /** | 19 /** |
| 22 * The contextual item that this menu was clicked for, e.g. the data used to | 20 * The contextual item that this menu was clicked for, e.g. the data used to |
| 23 * render an item in an <iron-list> or <dom-repeat>. | 21 * render an item in an <iron-list> or <dom-repeat>. |
| 24 * @type {?Object} | 22 * @type {?Object} |
| 25 */ | 23 */ |
| 26 itemData: { | 24 itemData: { |
| 27 type: Object, | 25 type: Object, |
| 28 value: null, | 26 value: null, |
| 29 }, | 27 }, |
| 30 | 28 |
| 31 /** @override */ | |
| 32 keyEventTarget: { | |
| 33 type: Object, | |
| 34 value: function() { | |
| 35 return this.$.menu; | |
| 36 } | |
| 37 }, | |
| 38 | |
| 39 openAnimationConfig: { | 29 openAnimationConfig: { |
| 40 type: Object, | 30 type: Object, |
| 41 value: function() { | 31 value: function() { |
| 42 return [{ | 32 return [{ |
| 43 name: 'fade-in-animation', | 33 name: 'fade-in-animation', |
| 44 timing: { | 34 timing: { |
| 45 delay: 50, | 35 delay: 50, |
| 46 duration: 200 | 36 duration: 200 |
| 47 } | 37 } |
| 48 }, { | 38 }, { |
| (...skipping 20 matching lines...) Expand all Loading... |
| 69 return [{ | 59 return [{ |
| 70 name: 'fade-out-animation', | 60 name: 'fade-out-animation', |
| 71 timing: { | 61 timing: { |
| 72 duration: 150 | 62 duration: 150 |
| 73 } | 63 } |
| 74 }]; | 64 }]; |
| 75 } | 65 } |
| 76 } | 66 } |
| 77 }, | 67 }, |
| 78 | 68 |
| 79 keyBindings: { | |
| 80 'tab': 'onTabPressed_', | |
| 81 }, | |
| 82 | |
| 83 listeners: { | 69 listeners: { |
| 84 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_', | 70 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_', |
| 85 }, | 71 }, |
| 86 | 72 |
| 87 /** | 73 /** |
| 88 * The last anchor that was used to open a menu. It's necessary for toggling. | 74 * The last anchor that was used to open a menu. It's necessary for toggling. |
| 89 * @private {?Element} | 75 * @private {?Element} |
| 90 */ | 76 */ |
| 91 lastAnchor_: null, | 77 lastAnchor_: null, |
| 92 | 78 |
| 93 /** | 79 /** @private {?function(!Event)} */ |
| 94 * The first focusable child in the menu's light DOM. | 80 keyHandler_: null, |
| 95 * @private {?Element} | |
| 96 */ | |
| 97 firstFocus_: null, | |
| 98 | |
| 99 /** | |
| 100 * The last focusable child in the menu's light DOM. | |
| 101 * @private {?Element} | |
| 102 */ | |
| 103 lastFocus_: null, | |
| 104 | 81 |
| 105 /** @override */ | 82 /** @override */ |
| 106 attached: function() { | 83 attached: function() { |
| 107 window.addEventListener('resize', this.closeMenu.bind(this)); | 84 window.addEventListener('resize', this.closeMenu.bind(this)); |
| 85 this.keyHandler_ = this.onCaptureKeyDown_.bind(this); |
| 86 this.$.menu.addEventListener('keydown', this.keyHandler_, true); |
| 87 }, |
| 88 |
| 89 /** @override */ |
| 90 detached: function() { |
| 91 this.$.menu.removeEventListener('keydown', this.keyHandler_, true); |
| 108 }, | 92 }, |
| 109 | 93 |
| 110 /** Closes the menu. */ | 94 /** Closes the menu. */ |
| 111 closeMenu: function() { | 95 closeMenu: function() { |
| 112 if (this.root.activeElement == null) { | 96 if (this.root.activeElement == null) { |
| 113 // Something else has taken focus away from the menu. Do not attempt to | 97 // Something else has taken focus away from the menu. Do not attempt to |
| 114 // restore focus to the button which opened the menu. | 98 // restore focus to the button which opened the menu. |
| 115 this.$.dropdown.restoreFocusOnClose = false; | 99 this.$.dropdown.restoreFocusOnClose = false; |
| 116 } | 100 } |
| 117 this.menuOpen = false; | 101 this.menuOpen = false; |
| 118 }, | 102 }, |
| 119 | 103 |
| 120 /** | 104 /** |
| 121 * Opens the menu at the anchor location. | 105 * Opens the menu at the anchor location. |
| 122 * @param {!Element} anchor The location to display the menu. | 106 * @param {!Element} anchor The location to display the menu. |
| 123 * @param {!Object=} opt_itemData The contextual item's data. | 107 * @param {!Object=} opt_itemData The contextual item's data. |
| 124 */ | 108 */ |
| 125 openMenu: function(anchor, opt_itemData) { | 109 openMenu: function(anchor, opt_itemData) { |
| 126 if (this.lastAnchor_ == anchor && this.menuOpen) | 110 if (this.lastAnchor_ == anchor && this.menuOpen) |
| 127 return; | 111 return; |
| 128 | 112 |
| 129 if (this.menuOpen) | 113 if (this.menuOpen) |
| 130 this.closeMenu(); | 114 this.closeMenu(); |
| 131 | 115 |
| 132 this.itemData = opt_itemData || null; | 116 this.itemData = opt_itemData || null; |
| 133 this.lastAnchor_ = anchor; | 117 this.lastAnchor_ = anchor; |
| 134 this.$.dropdown.restoreFocusOnClose = true; | 118 this.$.dropdown.restoreFocusOnClose = true; |
| 135 | 119 this.$.menu.selected = -1; |
| 136 var focusableChildren = Polymer.dom(this).querySelectorAll( | |
| 137 '[tabindex]:not([disabled]):not([hidden]),' + | |
| 138 'button:not([disabled]):not([hidden])'); | |
| 139 if (focusableChildren.length > 0) { | |
| 140 this.$.dropdown.focusTarget = focusableChildren[0]; | |
| 141 this.firstFocus_ = focusableChildren[0]; | |
| 142 this.lastFocus_ = focusableChildren[focusableChildren.length - 1]; | |
| 143 } | |
| 144 | 120 |
| 145 // Move the menu to the anchor. | 121 // Move the menu to the anchor. |
| 146 this.$.dropdown.positionTarget = anchor; | 122 this.$.dropdown.positionTarget = anchor; |
| 147 this.menuOpen = true; | 123 this.menuOpen = true; |
| 148 }, | 124 }, |
| 149 | 125 |
| 150 /** | 126 /** |
| 151 * Toggles the menu for the anchor that is passed in. | 127 * Toggles the menu for the anchor that is passed in. |
| 152 * @param {!Element} anchor The location to display the menu. | 128 * @param {!Element} anchor The location to display the menu. |
| 153 * @param {!Object=} opt_itemData The contextual item's data. | 129 * @param {!Object=} opt_itemData The contextual item's data. |
| 154 */ | 130 */ |
| 155 toggleMenu: function(anchor, opt_itemData) { | 131 toggleMenu: function(anchor, opt_itemData) { |
| 156 if (anchor == this.lastAnchor_ && this.menuOpen) | 132 if (anchor == this.lastAnchor_ && this.menuOpen) |
| 157 this.closeMenu(); | 133 this.closeMenu(); |
| 158 else | 134 else |
| 159 this.openMenu(anchor, opt_itemData); | 135 this.openMenu(anchor, opt_itemData); |
| 160 }, | 136 }, |
| 161 | 137 |
| 162 /** | 138 /** |
| 163 * Trap focus inside the menu. As a very basic heuristic, will wrap focus from | 139 * Close the menu when tab is pressed. Note that we must |
| 164 * the first element with a nonzero tabindex to the last such element. | 140 * explicitly add a capture event listener to do this as iron-menu-behavior |
| 165 * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available | 141 * eats all key events during bubbling. See |
| 166 * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179). | 142 * https://github.com/PolymerElements/iron-menu-behavior/issues/56. |
| 167 * @param {CustomEvent} e | 143 * This will move focus to the next focusable element before/after the |
| 144 * anchor. |
| 145 * @private |
| 168 */ | 146 */ |
| 169 onTabPressed_: function(e) { | 147 onCaptureKeyDown_: function(e) { |
| 170 if (!this.firstFocus_ || !this.lastFocus_) | 148 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(e, 'tab')) { |
| 171 return; | 149 // Need to refocus the anchor synchronously so that the tab event takes |
| 172 | 150 // effect on it. |
| 173 var toFocus; | 151 this.$.dropdown.restoreFocusOnClose = false; |
| 174 var keyEvent = e.detail.keyboardEvent; | 152 this.lastAnchor_.focus(); |
| 175 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_) | 153 this.closeMenu(); |
| 176 toFocus = this.lastFocus_; | 154 } |
| 177 else if (!keyEvent.shiftKey && keyEvent.target == this.lastFocus_) | |
| 178 toFocus = this.firstFocus_; | |
| 179 | |
| 180 if (!toFocus) | |
| 181 return; | |
| 182 | |
| 183 e.preventDefault(); | |
| 184 toFocus.focus(); | |
| 185 }, | 155 }, |
| 186 | 156 |
| 187 /** | 157 /** |
| 188 * Ensure the menu is reset properly when it is closed by the dropdown (eg, | 158 * Ensure the menu is reset properly when it is closed by the dropdown (eg, |
| 189 * clicking outside). | 159 * clicking outside). |
| 190 * @private | 160 * @private |
| 191 */ | 161 */ |
| 192 menuOpenChanged_: function() { | 162 menuOpenChanged_: function() { |
| 193 if (!this.menuOpen) { | 163 if (!this.menuOpen) { |
| 194 this.itemData = null; | 164 this.itemData = null; |
| 195 this.lastAnchor_ = null; | 165 this.lastAnchor_ = null; |
| 196 } | 166 } |
| 197 }, | 167 }, |
| 198 | 168 |
| 199 /** | 169 /** |
| 200 * Prevent focus restoring when tapping outside the menu. This stops the | 170 * Prevent focus restoring when tapping outside the menu. This stops the |
| 201 * focus moving around unexpectedly when closing the menu with the mouse. | 171 * focus moving around unexpectedly when closing the menu with the mouse. |
| 202 * @param {CustomEvent} e | 172 * @param {CustomEvent} e |
| 203 * @private | 173 * @private |
| 204 */ | 174 */ |
| 205 onOverlayCanceled_: function(e) { | 175 onOverlayCanceled_: function(e) { |
| 206 if (e.detail.type == 'tap') | 176 if (e.detail.type == 'tap') |
| 207 this.$.dropdown.restoreFocusOnClose = false; | 177 this.$.dropdown.restoreFocusOnClose = false; |
| 208 }, | 178 }, |
| 209 }); | 179 }); |
| OLD | NEW |