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
add iron-focusables-helper #200
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
8a84257
add iron-focusables-helper
valdrinkoshi 8d4fc0e
comments for isFocusable. Remove _isStyleVisible method. Fix isFocusa…
valdrinkoshi 8a5dfe6
check for tabindex attribute instead of tabIndex property
valdrinkoshi 3a370ed
_normalizedTabIndex instead of isTabbable
valdrinkoshi 7e2c362
return needsSortByTabIndex boolean
valdrinkoshi a9b2158
implement a stable sort (merge sort)
valdrinkoshi 7451cd3
filter out node types that are not an element. Updated jsdocs
valdrinkoshi 8b7f616
use matches instead of looking at the localName
valdrinkoshi 1f4558a
fix test, add comment for supporting ShadowDom v1
valdrinkoshi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was totally about to leave comments about how everything's going to break if either is empty and the concat with 'empty' arrays until I noticed that this is |
||
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this method accounts for focus navigation scopes. Here, all the nodes are sorted at the end but each scope needs to be sorted separately. For example:
I think calling
getTabbableNodes(#root)
would return [#C
,#A
,#B
], but it should return [#A
,#C
,#B
] becauseslot[name=b]
creates a focus navigation scope.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true, but I wouldn't encourage the support of
tabindex > 0
for distributed content (I woudn't encourage its usage at all). The current implementation offers a solution that works in 80% of the cases, mainly for shady dom. But there are so many messed up permutations (e.g. ShadowDom v1 + delegatesFocus = false) where it is not clear yet what's the correct, desired behavior for tab order 😕There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something I didn't think about yesterday: if this doesn't intend to support tabindex, what's the point of the sort at all? If everything is assumed to have
tabindex
0, document order is sequential focus navigation order.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That wasn't particularly well worded.. What I mean is, if calling
getTabbableNodes
with an ancestor of an element withtabindex > 0
is expected to break, then why attempt to sort? Why not just filter them out?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uhm, tested your example and indeed slots will scope the focus within their content..even without tabindex sorting this will be a problem for v1. So I should solve it. Will come up with a solution for this ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(from offline discussion) As this isn't part of the Polymer 2.0 upgrade yet and Polymer prior to 2 only uses Shadow DOM v0, there's no need for this to actually cover v1 focus behavior.
<slot>
to<content>
conversion in Polymer 1.7 means that Shadow DOM v1-like markup ends up as v0 anyway. However, this means handling Shadow DOM v1 focus behavior will be part of porting to hybrid / 2.0. As long as you're ok with that, ignoring v1 here SGTM.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, I've added a comment for the future maintainer at line 116 👌