| <!-- |
| @license |
| Copyright (c) 2015 The Polymer Project Authors. All rights reserved. |
| This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt |
| The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt |
| Code distributed by Google as part of the polymer project is also |
| subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt |
| --> |
| |
| <link rel="import" href="../polymer/polymer.html"> |
| <link rel="import" href="../iron-selector/iron-multi-selectable.html"> |
| <link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> |
| |
| <script> |
| |
| /** |
| * `Polymer.IronMenuBehavior` implements accessible menu behavior. |
| * |
| * @demo demo/index.html |
| * @polymerBehavior Polymer.IronMenuBehavior |
| */ |
| Polymer.IronMenuBehaviorImpl = { |
| |
| properties: { |
| |
| /** |
| * Returns the currently focused item. |
| * @type {?Object} |
| */ |
| focusedItem: { |
| observer: '_focusedItemChanged', |
| readOnly: true, |
| type: Object |
| }, |
| |
| /** |
| * The attribute to use on menu items to look up the item title. Typing the first |
| * letter of an item when the menu is open focuses that item. If unset, `textContent` |
| * will be used. |
| */ |
| attrForItemTitle: { |
| type: String |
| } |
| }, |
| |
| hostAttributes: { |
| 'role': 'menu', |
| 'tabindex': '0' |
| }, |
| |
| observers: [ |
| '_updateMultiselectable(multi)' |
| ], |
| |
| listeners: { |
| 'focus': '_onFocus', |
| 'keydown': '_onKeydown', |
| 'iron-items-changed': '_onIronItemsChanged' |
| }, |
| |
| keyBindings: { |
| 'up': '_onUpKey', |
| 'down': '_onDownKey', |
| 'esc': '_onEscKey', |
| 'shift+tab:keydown': '_onShiftTabDown' |
| }, |
| |
| attached: function() { |
| this._resetTabindices(); |
| }, |
| |
| /** |
| * Selects the given value. If the `multi` property is true, then the selected state of the |
| * `value` will be toggled; otherwise the `value` will be selected. |
| * |
| * @param {string|number} value the value to select. |
| */ |
| select: function(value) { |
| // Cancel automatically focusing a default item if the menu received focus |
| // through a user action selecting a particular item. |
| if (this._defaultFocusAsync) { |
| this.cancelAsync(this._defaultFocusAsync); |
| this._defaultFocusAsync = null; |
| } |
| var item = this._valueToItem(value); |
| if (item && item.hasAttribute('disabled')) return; |
| this._setFocusedItem(item); |
| Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); |
| }, |
| |
| /** |
| * Resets all tabindex attributes to the appropriate value based on the |
| * current selection state. The appropriate value is `0` (focusable) for |
| * the default selected item, and `-1` (not keyboard focusable) for all |
| * other items. |
| */ |
| _resetTabindices: function() { |
| var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[0]) : this.selectedItem; |
| |
| this.items.forEach(function(item) { |
| item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); |
| }, this); |
| }, |
| |
| /** |
| * Sets appropriate ARIA based on whether or not the menu is meant to be |
| * multi-selectable. |
| * |
| * @param {boolean} multi True if the menu should be multi-selectable. |
| */ |
| _updateMultiselectable: function(multi) { |
| if (multi) { |
| this.setAttribute('aria-multiselectable', 'true'); |
| } else { |
| this.removeAttribute('aria-multiselectable'); |
| } |
| }, |
| |
| /** |
| * Given a KeyboardEvent, this method will focus the appropriate item in the |
| * menu (if there is a relevant item, and it is possible to focus it). |
| * |
| * @param {KeyboardEvent} event A KeyboardEvent. |
| */ |
| _focusWithKeyboardEvent: function(event) { |
| for (var i = 0, item; item = this.items[i]; i++) { |
| var attr = this.attrForItemTitle || 'textContent'; |
| var title = item[attr] || item.getAttribute(attr); |
| |
| if (!item.hasAttribute('disabled') && title && |
| title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.keyCode).toLowerCase()) { |
| this._setFocusedItem(item); |
| break; |
| } |
| } |
| }, |
| |
| /** |
| * Focuses the previous item (relative to the currently focused item) in the |
| * menu, disabled items will be skipped. |
| */ |
| _focusPrevious: function() { |
| var length = this.items.length; |
| var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| for (var i = 1; i < length; i++) { |
| var item = this.items[(curFocusIndex - i + length) % length]; |
| if (!item.hasAttribute('disabled')) { |
| this._setFocusedItem(item); |
| return; |
| } |
| } |
| }, |
| |
| /** |
| * Focuses the next item (relative to the currently focused item) in the |
| * menu, disabled items will be skipped. |
| */ |
| _focusNext: function() { |
| var length = this.items.length; |
| var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| for (var i = 1; i < length; i++) { |
| var item = this.items[(curFocusIndex + i) % length]; |
| if (!item.hasAttribute('disabled')) { |
| this._setFocusedItem(item); |
| return; |
| } |
| } |
| }, |
| |
| /** |
| * Mutates items in the menu based on provided selection details, so that |
| * all items correctly reflect selection state. |
| * |
| * @param {Element} item An item in the menu. |
| * @param {boolean} isSelected True if the item should be shown in a |
| * selected state, otherwise false. |
| */ |
| _applySelection: function(item, isSelected) { |
| if (isSelected) { |
| item.setAttribute('aria-selected', 'true'); |
| } else { |
| item.removeAttribute('aria-selected'); |
| } |
| Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); |
| }, |
| |
| /** |
| * Discretely updates tabindex values among menu items as the focused item |
| * changes. |
| * |
| * @param {Element} focusedItem The element that is currently focused. |
| * @param {?Element} old The last element that was considered focused, if |
| * applicable. |
| */ |
| _focusedItemChanged: function(focusedItem, old) { |
| old && old.setAttribute('tabindex', '-1'); |
| if (focusedItem) { |
| focusedItem.setAttribute('tabindex', '0'); |
| focusedItem.focus(); |
| } |
| }, |
| |
| /** |
| * A handler that responds to mutation changes related to the list of items |
| * in the menu. |
| * |
| * @param {CustomEvent} event An event containing mutation records as its |
| * detail. |
| */ |
| _onIronItemsChanged: function(event) { |
| if (event.detail.addedNodes.length) { |
| this._resetTabindices(); |
| } |
| }, |
| |
| /** |
| * Handler that is called when a shift+tab keypress is detected by the menu. |
| * |
| * @param {CustomEvent} event A key combination event. |
| */ |
| _onShiftTabDown: function(event) { |
| var oldTabIndex = this.getAttribute('tabindex'); |
| |
| Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; |
| |
| this._setFocusedItem(null); |
| |
| this.setAttribute('tabindex', '-1'); |
| |
| this.async(function() { |
| this.setAttribute('tabindex', oldTabIndex); |
| Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| // NOTE(cdata): polymer/polymer#1305 |
| }, 1); |
| }, |
| |
| /** |
| * Handler that is called when the menu receives focus. |
| * |
| * @param {FocusEvent} event A focus event. |
| */ |
| _onFocus: function(event) { |
| if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { |
| // do not focus the menu itself |
| return; |
| } |
| |
| // Do not focus the selected tab if the deepest target is part of the |
| // menu element's local DOM and is focusable. |
| var rootTarget = /** @type {?HTMLElement} */( |
| Polymer.dom(event).rootTarget); |
| if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !this.isLightDescendant(rootTarget)) { |
| return; |
| } |
| |
| // clear the cached focus item |
| this._defaultFocusAsync = this.async(function() { |
| // focus the selected item when the menu receives focus, or the first item |
| // if no item is selected |
| var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[0]) : this.selectedItem; |
| |
| this._setFocusedItem(null); |
| |
| if (selectedItem) { |
| this._setFocusedItem(selectedItem); |
| } else if (this.items[0]) { |
| // We find the first none-disabled item (if one exists) |
| this._focusNext(); |
| } |
| }); |
| }, |
| |
| /** |
| * Handler that is called when the up key is pressed. |
| * |
| * @param {CustomEvent} event A key combination event. |
| */ |
| _onUpKey: function(event) { |
| // up and down arrows moves the focus |
| this._focusPrevious(); |
| event.detail.keyboardEvent.preventDefault(); |
| }, |
| |
| /** |
| * Handler that is called when the down key is pressed. |
| * |
| * @param {CustomEvent} event A key combination event. |
| */ |
| _onDownKey: function(event) { |
| this._focusNext(); |
| event.detail.keyboardEvent.preventDefault(); |
| }, |
| |
| /** |
| * Handler that is called when the esc key is pressed. |
| * |
| * @param {CustomEvent} event A key combination event. |
| */ |
| _onEscKey: function(event) { |
| // esc blurs the control |
| this.focusedItem.blur(); |
| }, |
| |
| /** |
| * Handler that is called when a keydown event is detected. |
| * |
| * @param {KeyboardEvent} event A keyboard event. |
| */ |
| _onKeydown: function(event) { |
| if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { |
| // all other keys focus the menu item starting with that character |
| this._focusWithKeyboardEvent(event); |
| } |
| event.stopPropagation(); |
| }, |
| |
| // override _activateHandler |
| _activateHandler: function(event) { |
| Polymer.IronSelectableBehavior._activateHandler.call(this, event); |
| event.stopPropagation(); |
| } |
| }; |
| |
| Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| |
| /** @polymerBehavior Polymer.IronMenuBehavior */ |
| Polymer.IronMenuBehavior = [ |
| Polymer.IronMultiSelectableBehavior, |
| Polymer.IronA11yKeysBehavior, |
| Polymer.IronMenuBehaviorImpl |
| ]; |
| |
| </script> |