This repository has been archived by the owner on Dec 19, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add iron-focusables-helper * comments for isFocusable. Remove _isStyleVisible method. Fix isFocusable to discard disabled elements that have tabindex * check for tabindex attribute instead of tabIndex property * _normalizedTabIndex instead of isTabbable * return needsSortByTabIndex boolean * implement a stable sort (merge sort) * filter out node types that are not an element. Updated jsdocs * use matches instead of looking at the localName * fix test, add comment for supporting ShadowDom v1
- Loading branch information
1 parent
54c5496
commit 7ed18e8
Showing
7 changed files
with
494 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
<!doctype html> | ||
<!-- | ||
@license | ||
Copyright (c) 2016 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"> | ||
|
||
<script> | ||
(function() { | ||
'use strict'; | ||
|
||
var p = Element.prototype; | ||
var matches = p.matches || p.matchesSelector || p.mozMatchesSelector || | ||
p.msMatchesSelector || p.oMatchesSelector || p.webkitMatchesSelector; | ||
|
||
Polymer.IronFocusablesHelper = { | ||
|
||
/** | ||
* Returns a sorted array of tabbable nodes, including the root node. | ||
* It searches the tabbable nodes in the light and shadow dom of the chidren, | ||
* sorting the result by tabindex. | ||
* @param {!Node} node | ||
* @return {Array<HTMLElement>} | ||
*/ | ||
getTabbableNodes: function(node) { | ||
var result = []; | ||
// If there is at least one element with tabindex > 0, we need to sort | ||
// the final array by tabindex. | ||
var needsSortByTabIndex = this._collectTabbableNodes(node, result); | ||
if (needsSortByTabIndex) { | ||
return this._sortByTabIndex(result); | ||
} | ||
return result; | ||
}, | ||
|
||
/** | ||
* Returns if a element is focusable. | ||
* @param {!HTMLElement} element | ||
* @return {boolean} | ||
*/ | ||
isFocusable: function(element) { | ||
// From http://stackoverflow.com/a/1600194/4228703: | ||
// There isn't a definite list, it's up to the browser. The only | ||
// standard we have is DOM Level 2 HTML https://www.w3.org/TR/DOM-Level-2-HTML/html.html, | ||
// according to which the only elements that have a focus() method are | ||
// HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement and | ||
// HTMLAnchorElement. This notably omits HTMLButtonElement and | ||
// HTMLAreaElement. | ||
// Referring to these tests with tabbables in different browsers | ||
// http://allyjs.io/data-tables/focusable.html | ||
|
||
// Elements that cannot be focused if they have [disabled] attribute. | ||
if (matches.call(element, 'input, select, textarea, button, object')) { | ||
return matches.call(element, ':not([disabled])'); | ||
} | ||
// Elements that can be focused even if they have [disabled] attribute. | ||
return matches.call(element, | ||
'a[href], area[href], iframe, [tabindex], [contentEditable]'); | ||
}, | ||
|
||
/** | ||
* Returns if a element is tabbable. To be tabbable, a element must be | ||
* focusable, visible, and with a tabindex !== -1. | ||
* @param {!HTMLElement} element | ||
* @return {boolean} | ||
*/ | ||
isTabbable: function(element) { | ||
return this.isFocusable(element) && | ||
matches.call(element, ':not([tabindex="-1"])') && | ||
this._isVisible(element); | ||
}, | ||
|
||
/** | ||
* Returns the normalized element tabindex. If not focusable, returns -1. | ||
* It checks for the attribute "tabindex" instead of the element property | ||
* `tabIndex` since browsers assign different values to it. | ||
* e.g. in Firefox `<div contenteditable>` has `tabIndex = -1` | ||
* @param {!HTMLElement} element | ||
* @return {Number} | ||
* @private | ||
*/ | ||
_normalizedTabIndex: function(element) { | ||
if (this.isFocusable(element)) { | ||
var tabIndex = element.getAttribute('tabindex') || 0; | ||
return Number(tabIndex); | ||
} | ||
return -1; | ||
}, | ||
|
||
/** | ||
* Searches for nodes that are tabbable and adds them to the `result` array. | ||
* Returns if the `result` array needs to be sorted by tabindex. | ||
* @param {!Node} node The starting point for the search; added to `result` | ||
* if tabbable. | ||
* @param {!Array<HTMLElement>} result | ||
* @return {boolean} | ||
* @private | ||
*/ | ||
_collectTabbableNodes: function(node, result) { | ||
// If not an element or not visible, no need to explore children. | ||
if (node.nodeType !== Node.ELEMENT_NODE || !this._isVisible(node)) { | ||
return false; | ||
} | ||
var element = /** @type {HTMLElement} */ (node); | ||
var tabIndex = this._normalizedTabIndex(element); | ||
var needsSortByTabIndex = tabIndex > 0; | ||
if (tabIndex >= 0) { | ||
result.push(element); | ||
} | ||
|
||
// In ShadowDOM v1, tab order is affected by the order of distrubution. | ||
// E.g. getTabbableNodes(#root) in ShadowDOM v1 should return [#A, #B]; | ||
// in ShadowDOM v0 tab order is not affected by the distrubution order, | ||
// in fact getTabbableNodes(#root) returns [#B, #A]. | ||
// <div id="root"> | ||
// <!-- shadow --> | ||
// <slot name="a"> | ||
// <slot name="b"> | ||
// <!-- /shadow --> | ||
// <input id="A" slot="a"> | ||
// <input id="B" slot="b" tabindex="1"> | ||
// </div> | ||
// TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0. | ||
var children; | ||
if (element.localName === 'content') { | ||
children = Polymer.dom(element).getDistributedNodes(); | ||
} else { | ||
// Use shadow root if possible, will check for distributed nodes. | ||
children = Polymer.dom(element.root || element).children; | ||
} | ||
for (var i = 0; i < children.length; i++) { | ||
// Ensure method is always invoked to collect tabbable children. | ||
var needsSort = this._collectTabbableNodes(children[i], result); | ||
needsSortByTabIndex = needsSortByTabIndex || needsSort; | ||
} | ||
return needsSortByTabIndex; | ||
}, | ||
|
||
/** | ||
* Returns false if the element has `visibility: hidden` or `display: none` | ||
* @param {!HTMLElement} element | ||
* @return {boolean} | ||
* @private | ||
*/ | ||
_isVisible: function(element) { | ||
// Check inline style first to save a re-flow. If looks good, check also | ||
// computed style. | ||
var style = element.style; | ||
if (style.visibility !== 'hidden' && style.display !== 'none') { | ||
style = window.getComputedStyle(element); | ||
return (style.visibility !== 'hidden' && style.display !== 'none'); | ||
} | ||
return false; | ||
}, | ||
|
||
/** | ||
* Sorts an array of tabbable elements by tabindex. Returns a new array. | ||
* @param {!Array<HTMLElement>} tabbables | ||
* @return {Array<HTMLElement>} | ||
* @private | ||
*/ | ||
_sortByTabIndex: function(tabbables) { | ||
// Implement a merge sort as Array.prototype.sort does a non-stable sort | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort | ||
var len = tabbables.length; | ||
if (len < 2) { | ||
return tabbables; | ||
} | ||
var pivot = Math.ceil(len / 2); | ||
var left = this._sortByTabIndex(tabbables.slice(0, pivot)); | ||
var right = this._sortByTabIndex(tabbables.slice(pivot)); | ||
return this._mergeSortByTabIndex(left, right); | ||
}, | ||
|
||
/** | ||
* Merge sort iterator, merges the two arrays into one, sorted by tab index. | ||
* @param {!Array<HTMLElement>} left | ||
* @param {!Array<HTMLElement>} right | ||
* @return {Array<HTMLElement>} | ||
* @private | ||
*/ | ||
_mergeSortByTabIndex: function(left, right) { | ||
var result = []; | ||
while ((left.length > 0) && (right.length > 0)) { | ||
if (this._hasLowerTabOrder(left[0], right[0])) { | ||
result.push(right.shift()); | ||
} else { | ||
result.push(left.shift()); | ||
} | ||
} | ||
|
||
return result.concat(left, right); | ||
}, | ||
|
||
/** | ||
* Returns if element `a` has lower tab order compared to element `b` | ||
* (both elements are assumed to be focusable and tabbable). | ||
* Elements with tabindex = 0 have lower tab order compared to elements | ||
* with tabindex > 0. | ||
* If both have same tabindex, it returns false. | ||
* @param {!HTMLElement} a | ||
* @param {!HTMLElement} b | ||
* @return {boolean} | ||
* @private | ||
*/ | ||
_hasLowerTabOrder: function(a, b) { | ||
// Normalize tabIndexes | ||
// e.g. in Firefox `<div contenteditable>` has `tabIndex = -1` | ||
var ati = Math.max(a.tabIndex, 0); | ||
var bti = Math.max(b.tabIndex, 0); | ||
return (ati === 0 || bti === 0) ? bti > ati : ati > bti; | ||
} | ||
}; | ||
})(); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.