Skip to content
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

document.blockingElements polyfill with inert #1

Merged
merged 41 commits into from
Sep 9, 2016
Merged

Conversation

valdrinkoshi
Copy link
Collaborator

@valdrinkoshi valdrinkoshi commented Jul 22, 2016

Implementation of document.blockingElements stack polyfill.

document.$blockingElements manages a stack of elements that inert the interaction outside them.

  • the stack can be updated with the methods push(elem), remove(elem), pop(elem)
  • the top element (document.$blockingElements.top) is the interactive part of the document
  • has(elem) returns if the element is a blocking element

This implementation uses the WICG/inert polyfill to disable interactions on the rest of the document. The algorithm will:

  • search for the path of the element to block up to document.body
  • set inert to all the siblings of each parent, skipping the parents and the element's distributed content (if any)

Why not listening to events that trigger focus change?

Another approach could be to listen for events that trigger focus change (e.g. focus, blur, keydown) and prevent those if focus moves out of the blocking element (it's the idea behind GoogleChrome/inert-polyfill and iron-overlay-behavior).

Wrapping the focus requires to find all the focusable nodes within the top blocking element, eventually sort by tabindex, in order to find first and last focusable node (e.g. see PR PolymerElements/iron-overlay-behavior#200 which works for ShadowDom v0).

This approach doesn't allow the focus to move outside the window (e.g. to the browser's url bar, dev console if opened, etc.), and is less robust when used with assistive technology (e.g. android talkback allows to move focus with swipe on screen, Apple Voiceover allows to move focus with special keyboard combinations).

Performance

Performance is dependent on the inert polyfill performance. The polyfill tries to invoke inert only if strictly needed (e.g. avoid setting it twice when updating the top blocking element).

At each toggle, scripting + rendering + painting totals to ~50ms (first toggle), ~35ms (next toggles)

The heaviest parts are:

  • unconditional paint caused by changes to cursor-event css property (see issue) => once fixed we should gain ~20-25ms
  • addition of the inert style nodes in shadow roots (done once for inert element's shadow root, see polyfill's implementation) => can be fixed only by native implementation of inert, should be a gain of at least ~5ms (cost of adding a node)

The results have been obtained by toggling the deepest x-trap-focus inside nested x-b (Chrome v52 stable for MacOs -> http://localhost:8080/components/blockingElements/demo/ce.html?ce=v0)
screen shot 2016-08-09 at 5 34 30 pm

@valdrinkoshi valdrinkoshi changed the title initial implementation with inert [WIP] initial implementation with inert Jul 22, 2016
@valdrinkoshi valdrinkoshi force-pushed the with-inert branch 2 times, most recently from f4ba650 to 5e171b8 Compare July 23, 2016 00:26
@valdrinkoshi valdrinkoshi changed the title [WIP] initial implementation with inert initial implementation with inert Jul 25, 2016
@valdrinkoshi valdrinkoshi force-pushed the with-inert branch 3 times, most recently from 677934d to 17282ec Compare July 25, 2016 21:10
// avoid setting them twice.
while (oldElParents.length || newElParents.length) {
const oldElParent = oldElParents.pop();
const newElParent = newElParents.pop();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tools team is leaning in the direction of only using const on top-level things (modules and symbols, for example), and preferring let for most things that have a relatively confined scope. What do you think of potentially standardizing on this style?

At the risk of starting a 🔥 /cc @justinfagnani @rictic

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaning, pending final disposition of a gentlemanly duel of ideas between the tools team :)

I personally prefer let for local variables.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

synced offline, and the agreement is to use const when using a variable that won't be reassigned, and let for variables that will be reused within the scope.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@valdrinkoshi
Copy link
Collaborator Author

@cdata @justinfagnani feedback incorporated, PTAL.

Everyone, I've updated the description of the PR with more info about performance & other options considered, PTAL :)

(function(document) {

/* Symbols for blocking elements and already inert elements */
const BLOCKING_ELEMS = Symbol('blockingElements');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI and in case you care, I'm going to advocate in tools that for Symbols we don't use SCREAMING_CASE, but _privateCamelCase names. I think it'll read better in the class bodies, and I also in general disagree with all caps for const, especially if const is the default.

compare:

BlockingElements[TOP_CHANGED_FN](element, oldTop, this[ALREADY_INERT_ELEMS]);

and

BlockingElements[_topChangedFn](element, oldTop, this[_alreadyInertElements]);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it's less of a punch in the eye! Will update :)

@valdrinkoshi
Copy link
Collaborator Author

@justinfagnani updated the Symbol names to be lower case, much more readable indeed.
I've also removed the all getter in favor of has() method, since I believe the only APIs needed are to know if an element is a blocking element, and if it is the top blocking element.

@valdrinkoshi
Copy link
Collaborator Author

@bicknellr FYI

* @type {Set<HTMLElement>}
* @private
*/
this[_blockingElements] = new Set();
Copy link

@bicknellr bicknellr Aug 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An array might be nicer here; they're already stacks (push, pop, includes, splice(i, 1)) so you wouldn't have to do stuff like line 113.

Copy link

@bicknellr bicknellr Aug 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you wouldn't have to track _topElement.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was an Array, but then got converted to Set, see #1 (comment)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cdata @justinfagnani So the collective decision on how to make a stack was to not use the built-in stack? 😆

Copy link
Collaborator Author

@valdrinkoshi valdrinkoshi Aug 16, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set is much faster that Array in searching if it contains an element, and that's the main use in here for _blockingElements, did some tests in chrome and there is a x5 - x10 difference code - demo (check the console)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ergo: it is painfully slow to compute the last element :x http://jsbin.com/yukizak/3/edit?html,js,output
this needs to be changed

const parents = [];
let current = element;
// Stop to body.
while (current && current !== document.body) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The syntax for this loop might be able to be tightened up a bit?

let current = element;
do {
  // ...
} while (current = current.parentNode || current.host)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, there are multiple conditions, so maybe my proposal would just be uglier..

@valdrinkoshi valdrinkoshi merged commit cc23e53 into master Sep 9, 2016
@valdrinkoshi valdrinkoshi deleted the with-inert branch September 9, 2016 01:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants