diff options
Diffstat (limited to 'catapult/third_party/polymer/components/iron-menu-behavior/iron-menu-behavior.html')
-rw-r--r-- | catapult/third_party/polymer/components/iron-menu-behavior/iron-menu-behavior.html | 396 |
1 files changed, 396 insertions, 0 deletions
diff --git a/catapult/third_party/polymer/components/iron-menu-behavior/iron-menu-behavior.html b/catapult/third_party/polymer/components/iron-menu-behavior/iron-menu-behavior.html new file mode 100644 index 00000000..92a24450 --- /dev/null +++ b/catapult/third_party/polymer/components/iron-menu-behavior/iron-menu-behavior.html @@ -0,0 +1,396 @@ +<!-- +@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 + }, + + disabled: { + type: Boolean, + value: false, + observer: '_disabledChanged', + }, + }, + + _SEARCH_RESET_TIMEOUT_MS: 1000, + + _previousTabIndex: 0, + + hostAttributes: { + 'role': 'menu', + }, + + 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) { + this.cancelDebouncer('_clearSearchText'); + + var searchText = this._searchText || ''; + var key = event.key && event.key.length == 1 ? event.key : + String.fromCharCode(event.keyCode); + searchText += key.toLocaleLowerCase(); + + var searchLength = searchText.length; + + for (var i = 0, item; item = this.items[i]; i++) { + if (item.hasAttribute('disabled')) { + continue; + } + + var attr = this.attrForItemTitle || 'textContent'; + var title = (item[attr] || item.getAttribute(attr) || '').trim(); + + if (title.length < searchLength) { + continue; + } + + if (title.slice(0, searchLength).toLocaleLowerCase() == searchText) { + this._setFocusedItem(item); + break; + } + } + + this._searchText = searchText; + this.debounce('_clearSearchText', this._clearSearchText, + this._SEARCH_RESET_TIMEOUT_MS); + }, + + _clearSearchText: function() { + this._searchText = ''; + }, + + /** + * Focuses the previous item (relative to the currently focused item) in the + * menu, disabled items will be skipped. + * Loop until length + 1 to handle case of single item in menu. + */ + _focusPrevious: function() { + var length = this.items.length; + var curFocusIndex = Number(this.indexOf(this.focusedItem)); + + for (var i = 1; i < length + 1; i++) { + var item = this.items[(curFocusIndex - i + length) % length]; + if (!item.hasAttribute('disabled')) { + var owner = Polymer.dom(item).getOwnerRoot() || document; + this._setFocusedItem(item); + + // Focus might not have worked, if the element was hidden or not + // focusable. In that case, try again. + if (Polymer.dom(owner).activeElement == item) { + return; + } + } + } + }, + + /** + * Focuses the next item (relative to the currently focused item) in the + * menu, disabled items will be skipped. + * Loop until length + 1 to handle case of single item in menu. + */ + _focusNext: function() { + var length = this.items.length; + var curFocusIndex = Number(this.indexOf(this.focusedItem)); + + for (var i = 1; i < length + 1; i++) { + var item = this.items[(curFocusIndex + i) % length]; + if (!item.hasAttribute('disabled')) { + var owner = Polymer.dom(item).getOwnerRoot() || document; + this._setFocusedItem(item); + + // Focus might not have worked, if the element was hidden or not + // focusable. In that case, try again. + if (Polymer.dom(owner).activeElement == 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.hasAttribute('disabled') && !this.disabled) { + 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(); + }, + + /** + * Updates this element's tab index when it's enabled/disabled. + * @param {boolean} disabled + */ + _disabledChanged: function(disabled) { + if (disabled) { + this._previousTabIndex = this.hasAttribute('tabindex') ? this.tabIndex : 0; + this.removeAttribute('tabindex'); // No tabindex means not tab-able or select-able. + } else if (!this.hasAttribute('tabindex')) { + this.setAttribute('tabindex', this._previousTabIndex); + } + } + }; + + Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; + + /** @polymerBehavior Polymer.IronMenuBehavior */ + Polymer.IronMenuBehavior = [ + Polymer.IronMultiSelectableBehavior, + Polymer.IronA11yKeysBehavior, + Polymer.IronMenuBehaviorImpl + ]; + +</script> |