Home | History | Annotate | Download | only in iron-menu-behavior
      1 <!--
      2 @license
      3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
      4 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
      5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
      6 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
      7 Code distributed by Google as part of the polymer project is also
      8 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
      9 -->
     10 
     11 <link rel="import" href="../polymer/polymer.html">
     12 <link rel="import" href="../iron-selector/iron-multi-selectable.html">
     13 <link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
     14 
     15 <script>
     16 
     17   /**
     18    * `Polymer.IronMenuBehavior` implements accessible menu behavior.
     19    *
     20    * @demo demo/index.html
     21    * @polymerBehavior Polymer.IronMenuBehavior
     22    */
     23   Polymer.IronMenuBehaviorImpl = {
     24 
     25     properties: {
     26 
     27       /**
     28        * Returns the currently focused item.
     29        * @type {?Object}
     30        */
     31       focusedItem: {
     32         observer: '_focusedItemChanged',
     33         readOnly: true,
     34         type: Object
     35       },
     36 
     37       /**
     38        * The attribute to use on menu items to look up the item title. Typing the first
     39        * letter of an item when the menu is open focuses that item. If unset, `textContent`
     40        * will be used.
     41        */
     42       attrForItemTitle: {
     43         type: String
     44       }
     45     },
     46 
     47     hostAttributes: {
     48       'role': 'menu',
     49       'tabindex': '0'
     50     },
     51 
     52     observers: [
     53       '_updateMultiselectable(multi)'
     54     ],
     55 
     56     listeners: {
     57       'focus': '_onFocus',
     58       'keydown': '_onKeydown',
     59       'iron-items-changed': '_onIronItemsChanged'
     60     },
     61 
     62     keyBindings: {
     63       'up': '_onUpKey',
     64       'down': '_onDownKey',
     65       'esc': '_onEscKey',
     66       'shift+tab:keydown': '_onShiftTabDown'
     67     },
     68 
     69     attached: function() {
     70       this._resetTabindices();
     71     },
     72 
     73     /**
     74      * Selects the given value. If the `multi` property is true, then the selected state of the
     75      * `value` will be toggled; otherwise the `value` will be selected.
     76      *
     77      * @param {string|number} value the value to select.
     78      */
     79     select: function(value) {
     80       // Cancel automatically focusing a default item if the menu received focus
     81       // through a user action selecting a particular item.
     82       if (this._defaultFocusAsync) {
     83         this.cancelAsync(this._defaultFocusAsync);
     84         this._defaultFocusAsync = null;
     85       }
     86       var item = this._valueToItem(value);
     87       if (item && item.hasAttribute('disabled')) return;
     88       this._setFocusedItem(item);
     89       Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments);
     90     },
     91 
     92     /**
     93      * Resets all tabindex attributes to the appropriate value based on the
     94      * current selection state. The appropriate value is `0` (focusable) for
     95      * the default selected item, and `-1` (not keyboard focusable) for all
     96      * other items.
     97      */
     98     _resetTabindices: function() {
     99       var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[0]) : this.selectedItem;
    100 
    101       this.items.forEach(function(item) {
    102         item.setAttribute('tabindex', item === selectedItem ? '0' : '-1');
    103       }, this);
    104     },
    105 
    106     /**
    107      * Sets appropriate ARIA based on whether or not the menu is meant to be
    108      * multi-selectable.
    109      *
    110      * @param {boolean} multi True if the menu should be multi-selectable.
    111      */
    112     _updateMultiselectable: function(multi) {
    113       if (multi) {
    114         this.setAttribute('aria-multiselectable', 'true');
    115       } else {
    116         this.removeAttribute('aria-multiselectable');
    117       }
    118     },
    119 
    120     /**
    121      * Given a KeyboardEvent, this method will focus the appropriate item in the
    122      * menu (if there is a relevant item, and it is possible to focus it).
    123      *
    124      * @param {KeyboardEvent} event A KeyboardEvent.
    125      */
    126     _focusWithKeyboardEvent: function(event) {
    127       for (var i = 0, item; item = this.items[i]; i++) {
    128         var attr = this.attrForItemTitle || 'textContent';
    129         var title = item[attr] || item.getAttribute(attr);
    130 
    131         if (!item.hasAttribute('disabled') && title &&
    132             title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.keyCode).toLowerCase()) {
    133           this._setFocusedItem(item);
    134           break;
    135         }
    136       }
    137     },
    138 
    139     /**
    140      * Focuses the previous item (relative to the currently focused item) in the
    141      * menu, disabled items will be skipped.
    142      */
    143     _focusPrevious: function() {
    144       var length = this.items.length;
    145       var curFocusIndex = Number(this.indexOf(this.focusedItem));
    146       for (var i = 1; i < length; i++) {
    147         var item = this.items[(curFocusIndex - i + length) % length];
    148         if (!item.hasAttribute('disabled')) {
    149           this._setFocusedItem(item);
    150           return;
    151         }
    152       }
    153     },
    154 
    155     /**
    156      * Focuses the next item (relative to the currently focused item) in the
    157      * menu, disabled items will be skipped.
    158      */
    159     _focusNext: function() {
    160       var length = this.items.length;
    161       var curFocusIndex = Number(this.indexOf(this.focusedItem));
    162       for (var i = 1; i < length; i++) {
    163         var item = this.items[(curFocusIndex + i) % length];
    164         if (!item.hasAttribute('disabled')) {
    165           this._setFocusedItem(item);
    166           return;
    167         }
    168       }
    169     },
    170 
    171     /**
    172      * Mutates items in the menu based on provided selection details, so that
    173      * all items correctly reflect selection state.
    174      *
    175      * @param {Element} item An item in the menu.
    176      * @param {boolean} isSelected True if the item should be shown in a
    177      * selected state, otherwise false.
    178      */
    179     _applySelection: function(item, isSelected) {
    180       if (isSelected) {
    181         item.setAttribute('aria-selected', 'true');
    182       } else {
    183         item.removeAttribute('aria-selected');
    184       }
    185       Polymer.IronSelectableBehavior._applySelection.apply(this, arguments);
    186     },
    187 
    188     /**
    189      * Discretely updates tabindex values among menu items as the focused item
    190      * changes.
    191      *
    192      * @param {Element} focusedItem The element that is currently focused.
    193      * @param {?Element} old The last element that was considered focused, if
    194      * applicable.
    195      */
    196     _focusedItemChanged: function(focusedItem, old) {
    197       old && old.setAttribute('tabindex', '-1');
    198       if (focusedItem) {
    199         focusedItem.setAttribute('tabindex', '0');
    200         focusedItem.focus();
    201       }
    202     },
    203 
    204     /**
    205      * A handler that responds to mutation changes related to the list of items
    206      * in the menu.
    207      *
    208      * @param {CustomEvent} event An event containing mutation records as its
    209      * detail.
    210      */
    211     _onIronItemsChanged: function(event) {
    212       if (event.detail.addedNodes.length) {
    213         this._resetTabindices();
    214       }
    215     },
    216 
    217     /**
    218      * Handler that is called when a shift+tab keypress is detected by the menu.
    219      *
    220      * @param {CustomEvent} event A key combination event.
    221      */
    222     _onShiftTabDown: function(event) {
    223       var oldTabIndex = this.getAttribute('tabindex');
    224 
    225       Polymer.IronMenuBehaviorImpl._shiftTabPressed = true;
    226 
    227       this._setFocusedItem(null);
    228 
    229       this.setAttribute('tabindex', '-1');
    230 
    231       this.async(function() {
    232         this.setAttribute('tabindex', oldTabIndex);
    233         Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
    234         // NOTE(cdata): polymer/polymer#1305
    235       }, 1);
    236     },
    237 
    238     /**
    239      * Handler that is called when the menu receives focus.
    240      *
    241      * @param {FocusEvent} event A focus event.
    242      */
    243     _onFocus: function(event) {
    244       if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) {
    245         // do not focus the menu itself
    246         return;
    247       }
    248 
    249       // Do not focus the selected tab if the deepest target is part of the
    250       // menu element's local DOM and is focusable.
    251       var rootTarget = /** @type {?HTMLElement} */(
    252           Polymer.dom(event).rootTarget);
    253       if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !this.isLightDescendant(rootTarget)) {
    254         return;
    255       }
    256 
    257       // clear the cached focus item
    258       this._defaultFocusAsync = this.async(function() {
    259         // focus the selected item when the menu receives focus, or the first item
    260         // if no item is selected
    261         var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[0]) : this.selectedItem;
    262 
    263         this._setFocusedItem(null);
    264 
    265         if (selectedItem) {
    266           this._setFocusedItem(selectedItem);
    267         } else if (this.items[0]) {
    268           // We find the first none-disabled item (if one exists)
    269           this._focusNext();
    270         }
    271       });
    272     },
    273 
    274     /**
    275      * Handler that is called when the up key is pressed.
    276      *
    277      * @param {CustomEvent} event A key combination event.
    278      */
    279     _onUpKey: function(event) {
    280       // up and down arrows moves the focus
    281       this._focusPrevious();
    282       event.detail.keyboardEvent.preventDefault();
    283     },
    284 
    285     /**
    286      * Handler that is called when the down key is pressed.
    287      *
    288      * @param {CustomEvent} event A key combination event.
    289      */
    290     _onDownKey: function(event) {
    291       this._focusNext();
    292       event.detail.keyboardEvent.preventDefault();
    293     },
    294 
    295     /**
    296      * Handler that is called when the esc key is pressed.
    297      *
    298      * @param {CustomEvent} event A key combination event.
    299      */
    300     _onEscKey: function(event) {
    301       // esc blurs the control
    302       this.focusedItem.blur();
    303     },
    304 
    305     /**
    306      * Handler that is called when a keydown event is detected.
    307      *
    308      * @param {KeyboardEvent} event A keyboard event.
    309      */
    310     _onKeydown: function(event) {
    311       if (!this.keyboardEventMatchesKeys(event, 'up down esc')) {
    312         // all other keys focus the menu item starting with that character
    313         this._focusWithKeyboardEvent(event);
    314       }
    315       event.stopPropagation();
    316     },
    317 
    318     // override _activateHandler
    319     _activateHandler: function(event) {
    320       Polymer.IronSelectableBehavior._activateHandler.call(this, event);
    321       event.stopPropagation();
    322     }
    323   };
    324 
    325   Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
    326 
    327   /** @polymerBehavior Polymer.IronMenuBehavior */
    328   Polymer.IronMenuBehavior = [
    329     Polymer.IronMultiSelectableBehavior,
    330     Polymer.IronA11yKeysBehavior,
    331     Polymer.IronMenuBehaviorImpl
    332   ];
    333 
    334 </script>
    335