-
Notifications
You must be signed in to change notification settings - Fork 71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add iron-focusables-helper #200
Conversation
// If there is at least one element with tabindex > 0, we need to sort | ||
// the final array by tabindex. | ||
var maxTabIndex = this._findTabbableNodes(node, result); | ||
maxTabIndex > 0 && result.sort(this._tabIndexSort); |
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.
Array.prototype.sort
isn't guaranteed to be stable.
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, in which way/which browsers? I've written unit tests that check for the result of this method & if it returns a correctly sorted array
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.
Practically speaking, I doubt any browsers use an unstable sort, but I figured it might be worth pointing 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.
I couldn't experience any issue so far, and the tests ensured it works as expected. Will keep an eye on it if it shows a weird behavior!
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.
@bicknellr you were right, the issue was reported here #209
Will fix this!
* @return {Number} | ||
* @private | ||
*/ | ||
_findTabbableNodes: function(node, result) { |
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.
The signature of this function feels very specific to its use in getTabbableNodes
; given the name _findTabbableNodes
, I'd expect this to return the list rather than taking a list to modify. You'd be able to take lines 74, 79, 88-90 out of this function too if you computed maxTabIndex
in getTabbableNodes
.
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.
The reasoning for this layout is to avoid too many searches & achieve recursion, as we need to do a depth first search in order to keep the tabindex order. The fact that we return the maxTabIndex
is just to support cases where we might have a tabindex > 0 (which is an anti-pattern, but I guess we have to support it, right?).
I could move the searching of the max tabindex outside, but that would require another for loop over the result :/
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.
Either way, the tab index of each element in the list has to be reviewed. As written now, the check is interleaved with the traversal; splitting it out won't change the time complexity but would let this function do one less thing. If there was some sort of early return behavior that could be exploited here, I'd agree, but I don't think there is.
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.
Yes, the tab index of each element has to be reviewed. Currently, I do that on the same loop where i collect the tabbables. This allows me to save another loop over the tabbables after they get collected.
// o(node-depth) search
var tabbables = [];
var maxTabIndex = this._findTabbables(node, tabbables);
// o(node-depth) + o(tabbables) search
var tabbables = this._findTabbables(node);
var maxTabIndex = this._findMaxTabIndex(tabbables);
The second option is more readable but less performant. How to proceed on this one?
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.
All the work done in _findMaxTabIndex
still happens in the original _findTabbables
. I think the second case can even potentially be more performant: considering only work done to find the max tab index, the original _findTabbables
has best case O(n) because line 105 occurs for every node traversed, even when the nodes in question are not in the tab order. However, splitting this out into a separate loop gives a best case of O(1) because the list, which will be some subset of the traversed elements, can potentially be empty. (Both still have worst case O(n).)
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.
Just to recap:
- we don't want to sort if all elements have
tabindex = 0
-> look if there is one element withtabIndex > 0
- we have to traverse all the children to find the tabbables -> minimum is
O(n)
- to filter the tabbable elements, we must look at their
tabindex
to be>= 0
Let's compare the two options in some scenarios for n
nodes of which m
are tabbable, and i
is the index of the tabbable with tabindex > 0
:
// option 1
var tabbables = [];
var maxTabIndex = this._findTabbables(node, tabbables);
// option 2
var tabbables = this._findTabbables(node);
var maxTabIndex = this._findMaxTabIndex(tabbables);
scenario 1 (worst case scenario)
m = n
(all are tabbable), i = -1
(no tabbable with tabindex > 0
)
- option 1:
O(n)
- option 2:
O(n) + O(n)
(needs to search until the end of tabbables)
scenario 2
m = n/10
(only 10% tabbable), i = -1
(no tabbable with tabindex > 0
)
- option 1:
O(n)
- option 2:
O(n) + O(n/10)
(needs to search until the end of tabbables)
scenario 3
m = n/10
(only 10% tabbable), i = m/2
(the tabbable on the middle has tabindex > 0
)
- option 1:
O(n)
- option 2:
O(n) + O(n/20)
(can stop as soon as it finds the tabbable withtabindex > 0
)
scenario 4 (best case scenario)
m = 0
(no tabbables)
- option 1:
O(n)
- option 2:
O(n)
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.
If we average of worst and best scenarios, we see that on average option 2 is 50% slower than option 1.
In practice probably this doesn't make a huge difference, I just would like to understand if you see a better, more performant way to tackle this. Please correct me if I wrote something wrong :)
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'm pretty sure all of those are O(n) since "O(n) + O(n)" = O(n) but, expanding out the two pieces of work (gather and max), option 1 would be "O(n) + O(n)" (maybe more clearly written as something like "n * 2O(1)" in this case) for all of those situations, even though it uses a single loop.
function sumAndConcat1(list) {
var sum = 0;
var string = "";
for (var i = 0; i < list.length; i++) {
sum += list[i];
string += list[i];
}
return {sum, string};
}
function sumAndConcat2(list) {
var sum = 0;
var string = "";
for (var i = 0; i < list.length; i++) {
sum += list[i];
}
for (var i = 0; i < list.length; i++) {
string += list[i];
}
return {sum, string};
}
I don't think the use of a single for loop in sumAndConcat1
shouldn't give it any performance advantage over sumAndConcat2
. And, if the second loop in sumAndConcat2
could perform fewer than list.length
iterations (assuming the function was doing something else), wouldn't that be an advantage for sumAndConcat2
?
However, I suppose the end to my argument wouldn't have much benefit anyway given the operation being optimized out (max) has such a small cost. :P I still think the function's signature is weird but, yeah, the rest of my argument is more pedantic than practical.
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.
Since we only need to know if the maxTabIndex is greater than zero, and not its actual value, I could remove the Math.max
part and make it return a boolean. I'll check also a better/clearer signature for this method 👍
27b4e7d
to
284fdda
Compare
980b33a
to
6067107
Compare
Got a bug in iron-focusables-helper in _isVisible.
node.style does not exist on #comment nodes. |
c9ae30b
to
fc214ea
Compare
Good catch @kornalius, fixed in the last commit! |
fc214ea
to
ce768cf
Compare
Did some tests with
Querying for tabbable nodes is the part that takes the most of the time:
<button onclick="plain.open()">overlay</button>
<simple-overlay with-backdrop id="plain">
<a href="#">click</a>
<a href="#">click</a>
<a href="#">click</a>
<input>
<paper-input></paper-input>
<paper-input></paper-input>
<paper-input></paper-input>
<paper-input></paper-input>
<paper-input></paper-input>
<div contenteditable>contenteditable</div>
<div tabindex="0">tabindex 0</div>
<a href="#">click</a>
<paper-dropdown-menu>
<ul class="dropdown-content">
<button>one</button>
<button>two</button>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
</ul>
</paper-dropdown-menu>
<paper-dropdown-menu>
<ul class="dropdown-content">
<button>one</button>
<button>two</button>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
</ul>
</paper-dropdown-menu>
<paper-dropdown-menu>
<ul class="dropdown-content">
<button>one</button>
<button>two</button>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
</ul>
</paper-dropdown-menu>
<paper-dropdown-menu>
<ul class="dropdown-content">
<button>one</button>
<button>two</button>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
</ul>
</paper-dropdown-menu>
<paper-dropdown-menu>
<ul class="dropdown-content">
<button>one</button>
<button>two</button>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
</ul>
</paper-dropdown-menu>
<paper-dropdown-menu>
<ul class="dropdown-content">
<button>one</button>
<button>two</button>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
</ul>
</paper-dropdown-menu>
<paper-dropdown-menu>
<ul class="dropdown-content">
<button>one</button>
<button>two</button>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
<li><a href="#">click</a></li>
</ul>
</paper-dropdown-menu>
<button onclick="plain.close()">Close</button>
</simple-overlay> |
@cdata FYI ^ |
going to unassign myself from this since I don't seem needed 😂 |
*/ | ||
_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 comment
The 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 &&
and not ||
!
Thanks for your work on this @valdrinkoshi . This is the kind of contribution I feel I will never be able to make. It would be great to get a proper solution to PolymerElements/paper-dialog#125 ! |
@valdrinkoshi - Is there any update on this PR? I'm interested in having this PR merged in. Please let me know if there's anything I can do to help. Thanks! |
…ble to discard disabled elements that have tabindex
ce768cf
to
e5fa398
Compare
// <input id="A" slot="a"> | ||
// <input id="B" slot="b" tabindex="1"> | ||
// </div> | ||
// TODO(valdrin) implement ShadowDOM v1 when upgrading to Polymer v2.0. |
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.
👍
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.
Ok, minor comment and LGTM.
}, | ||
|
||
/** | ||
* Returns if an element has lower tab order compared to another element |
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.
Which is a
and which is b
?
e5fa398
to
1f4558a
Compare
@bicknellr good catch, clarified the jsdoc of |
Files App's quick view is suffered from the bug PolymerElements/iron-overlay-behavior#200, and focus doesn't move inside files-quick-view element if a button inside it is hidden. Updating iron-overlay-behavior fixes the issue. This CL was created by the following steps. 1. Change the version in third_party/polymer/v1_0/bower.json 2. Run third_party/polymer/v1_0/reproduce.sh 3. tools/polymer/polymer_grdp_to_txt.py ui/webui/resources/polymer_resources.grdp > polymer.txt vim polymer.txt # add iron-overlay-behavior/iron-focusables-helper{-extracted.js,.html} tools/polymer/txt_to_polymer_grdp.py polymer.txt > ui/webui/resources/polymer_resources.grdp BUG=663636 TEST=git cl try. third_party/closure_compiler/run_compiler. The issue mentioned above is fixed. CQ_INCLUDE_TRYBOTS=master.tryserver.chromium.linux:closure_compilation Review-Url: https://codereview.chromium.org/2633003003 Cr-Commit-Position: refs/heads/master@{#444313}
Fixes #196, fixes #194, fixes #192.
iron-focusables-helper
providesgetTabbableNodes
method, used to compute the tabbable elements within a specific node, searching both in the light and shadow dom, sorting by tabindex._focusableNodes
has been changed to invoke this method. Tests have been updated to look for the focusable nodes once the overlays are opened (as it is required for a node to be visible in order to be considered tabbable).Sorting by tabindex is a stable sort (merge sort), since
Array.prototype.sort
is not guaranteed to be stable (fixes #209).