• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!--
2@license
3Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7Code distributed by Google as part of the polymer project is also
8subject 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