diff --git a/.gitignore b/.gitignore
index 3c3629e..a088b6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
node_modules
+bower_components
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..fa0831f
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,16 @@
+language: node_js
+node_js: stable
+dist: trusty
+sudo: required
+addons:
+ firefox: latest
+ apt:
+ sources:
+ - google-chrome
+ packages:
+ - google-chrome-stable
+before_script:
+- npm install -g bower polylint web-component-tester
+- bower install
+- polylint
+script: xvfb-run wct
diff --git a/README.md b/README.md
index 3ed7de8..5735c9e 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,37 @@
# `blockingElements` stack API
-Implementation of proposal https://github.com/whatwg/html/issues/897
+Implementation of proposal
+
+`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 polyfill 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)
+
+Use this polyfill together with the [WICG/inert](https://github.com/WICG/inert) polyfill to disable interactions on the rest of the document. See the [demo page]() as an example.
+
+## 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.
+
+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.
+
+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](https://github.com/WICG/inert/issues/21)) => 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](https://github.com/WICG/inert/blob/master/inert.js#L581)) => 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 -> ) ![results](https://cloud.githubusercontent.com/assets/6173664/17538133/914f365a-5e57-11e6-9b91-1c6b7eb22d57.png)
diff --git a/blocking-elements.html b/blocking-elements.html
index e69de29..0f3199a 100644
--- a/blocking-elements.html
+++ b/blocking-elements.html
@@ -0,0 +1 @@
+
diff --git a/blocking-elements.js b/blocking-elements.js
index e69de29..1296abe 100644
--- a/blocking-elements.js
+++ b/blocking-elements.js
@@ -0,0 +1,335 @@
+/**
+ *
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function(document) {
+
+ /* Symbols for private properties */
+ const _blockingElements = Symbol();
+ const _alreadyInertElements = Symbol();
+
+ /* Symbols for private static methods */
+ const _topChanged = Symbol();
+ const _setInertToSiblingsOfElement = Symbol();
+ const _getParents = Symbol();
+ const _getDistributedChildren = Symbol();
+ const _isInertable = Symbol();
+ const _isInert = Symbol();
+ const _setInert = Symbol();
+
+ /**
+ * `BlockingElements` manages a stack of elements that inert the interaction
+ * outside them. The top element is the interactive part of the document.
+ * The stack can be updated with the methods `push, remove, pop`.
+ */
+ class BlockingElements {
+ constructor() {
+
+ /**
+ * The blocking elements.
+ * @type {Array}
+ * @private
+ */
+ this[_blockingElements] = [];
+
+ /**
+ * Elements that are already inert before the first blocking element is pushed.
+ * @type {Set}
+ * @private
+ */
+ this[_alreadyInertElements] = new Set();
+ }
+
+ /**
+ * Call this whenever this object is about to become obsolete. This empties
+ * the blocking elements
+ */
+ destructor() {
+ // Pretend like top changed from current top to null in order to reset
+ // all its parents inertness. Ensure we keep inert what was already inert!
+ BlockingElements[_topChanged](null, this.top, this[_alreadyInertElements]);
+ this[_blockingElements] = null;
+ this[_alreadyInertElements] = null;
+ }
+
+ /**
+ * The top blocking element.
+ * @type {HTMLElement|null}
+ */
+ get top() {
+ const elems = this[_blockingElements];
+ return elems[elems.length - 1] || null;
+ }
+
+ /**
+ * Adds the element to the blocking elements.
+ * @param {!HTMLElement} element
+ */
+ push(element) {
+ if (this.has(element)) {
+ console.warn('element already added in document.blockingElements');
+ return;
+ }
+ BlockingElements[_topChanged](element, this.top, this[_alreadyInertElements]);
+ this[_blockingElements].push(element);
+ }
+
+ /**
+ * Removes the element from the blocking elements. Returns true if the element
+ * was removed.
+ * @param {!HTMLElement} element
+ * @returns {boolean}
+ */
+ remove(element) {
+ const i = this[_blockingElements].indexOf(element);
+ if (i === -1) {
+ return false;
+ }
+ this[_blockingElements].splice(i, 1);
+ // Top changed only if the removed element was the top element.
+ if (i === this[_blockingElements].length) {
+ BlockingElements[_topChanged](this.top, element, this[_alreadyInertElements]);
+ }
+ return true;
+ }
+
+ /**
+ * Remove the top blocking element and returns it.
+ * @returns {HTMLElement|null} the removed element.
+ */
+ pop() {
+ const top = this.top;
+ top && this.remove(top);
+ return top;
+ }
+
+ /**
+ * Returns if the element is a blocking element.
+ * @param {!HTMLElement} element
+ * @returns {boolean}
+ */
+ has(element) {
+ return this[_blockingElements].indexOf(element) !== -1;
+ }
+
+ /**
+ * Sets `inert` to all document elements except the new top element, its parents,
+ * and its distributed content. Pass `oldTop` to limit element updates (will look
+ * for common parents and avoid setting them twice).
+ * When the first blocking element is added (`newTop = null`), it saves the elements
+ * that are already inert into `alreadyInertElems`. When the last blocking element
+ * is removed (`oldTop = null`), `alreadyInertElems` are kept inert.
+ * @param {HTMLElement} newTop If null, it means the last blocking element was removed.
+ * @param {HTMLElement} oldTop If null, it means the first blocking element was added.
+ * @param {!Set} alreadyInertElems Elements to be kept inert.
+ * @private
+ */
+ static[_topChanged](newTop, oldTop, alreadyInertElems) {
+ const oldElParents = oldTop ? this[_getParents](oldTop) : [];
+ const newElParents = newTop ? this[_getParents](newTop) : [];
+ const elemsToSkip = newTop && newTop.shadowRoot ?
+ this[_getDistributedChildren](newTop.shadowRoot) : null;
+ // Loop from top to deepest elements, so we find the common parents and
+ // avoid setting them twice.
+ while (oldElParents.length || newElParents.length) {
+ const oldElParent = oldElParents.pop();
+ const newElParent = newElParents.pop();
+ if (oldElParent === newElParent) {
+ continue;
+ }
+ // Same parent, set only these 2 children.
+ if (oldElParent && newElParent &&
+ oldElParent.parentNode === newElParent.parentNode) {
+ if (!oldTop && this[_isInert](oldElParent)) {
+ alreadyInertElems.add(oldElParent);
+ }
+ this[_setInert](oldElParent, true);
+ this[_setInert](newElParent, alreadyInertElems.has(newElParent));
+ } else {
+ oldElParent && this[_setInertToSiblingsOfElement](oldElParent, false, elemsToSkip,
+ alreadyInertElems);
+ // Collect the already inert elements only if it is the first blocking
+ // element (if oldTop = null)
+ newElParent && this[_setInertToSiblingsOfElement](newElParent, true, elemsToSkip,
+ oldTop ? null : alreadyInertElems);
+ }
+ }
+ if (!newTop) {
+ alreadyInertElems.clear();
+ }
+ }
+
+ /**
+ * Returns if the element is not inertable.
+ * @param {!HTMLElement} element
+ * @returns {boolean}
+ * @private
+ */
+ static[_isInertable](element) {
+ return /^(style|template|script)$/.test(element.localName);
+ }
+
+ /**
+ * Sets `inert` to the siblings of the element except the elements to skip.
+ * If `inert = true`, already inert elements are added into `alreadyInertElems`.
+ * If `inert = false`, siblings that are contained in `alreadyInertElems` will
+ * be kept inert.
+ * @param {!HTMLElement} element
+ * @param {boolean} inert
+ * @param {Set} elemsToSkip
+ * @param {Set} alreadyInertElems
+ * @private
+ */
+ static[_setInertToSiblingsOfElement](element, inert, elemsToSkip, alreadyInertElems) {
+ // Previous siblings.
+ let sibling = element;
+ while ((sibling = sibling.previousElementSibling)) {
+ // If not inertable or to be skipped, skip.
+ if (this[_isInertable](sibling) || (elemsToSkip && elemsToSkip.has(sibling))) {
+ continue;
+ }
+ // Should be collected since already inerted.
+ if (alreadyInertElems && inert && this[_isInert](sibling)) {
+ alreadyInertElems.add(sibling);
+ }
+ // Should be kept inert if it's in `alreadyInertElems`.
+ this[_setInert](sibling, inert || (alreadyInertElems && alreadyInertElems.has(sibling)));
+ }
+ // Next siblings.
+ sibling = element;
+ while ((sibling = sibling.nextElementSibling)) {
+ // If not inertable or to be skipped, skip.
+ if (this[_isInertable](sibling) || (elemsToSkip && elemsToSkip.has(sibling))) {
+ continue;
+ }
+ // Should be collected since already inerted.
+ if (alreadyInertElems && inert && this[_isInert](sibling)) {
+ alreadyInertElems.add(sibling);
+ }
+ // Should be kept inert if it's in `alreadyInertElems`.
+ this[_setInert](sibling, inert || (alreadyInertElems && alreadyInertElems.has(sibling)));
+ }
+ }
+
+ /**
+ * Returns the list of parents of an element, starting from element (included)
+ * up to `document.body` (excluded).
+ * @param {!HTMLElement} element
+ * @returns {Array}
+ * @private
+ */
+ static[_getParents](element) {
+ const parents = [];
+ let current = element;
+ // Stop to body.
+ while (current && current !== document.body) {
+ // Skip shadow roots.
+ if (current.nodeType === Node.ELEMENT_NODE) {
+ parents.push(current);
+ }
+ // ShadowDom v1
+ if (current.assignedSlot) {
+ // Collect slots from deepest slot to top.
+ while ((current = current.assignedSlot)) {
+ parents.push(current);
+ }
+ // Continue the search on the top slot.
+ current = parents.pop();
+ continue;
+ }
+ // ShadowDom v0
+ const insertionPoints = current.getDestinationInsertionPoints ?
+ current.getDestinationInsertionPoints() : [];
+ if (insertionPoints.length) {
+ for (let i = 0; i < insertionPoints.length - 1; i++) {
+ parents.push(current);
+ }
+ // Continue the search on the top content.
+ current = insertionPoints[insertionPoints.length - 1];
+ continue;
+ }
+ current = current.parentNode || current.host;
+ }
+ return parents;
+ }
+
+ /**
+ * Returns the distributed children of a shadow root.
+ * @param {!DocumentFragment} shadowRoot
+ * @returns {Set}
+ * @private
+ */
+ static[_getDistributedChildren](shadowRoot) {
+ const result = new Set();
+ let i, j, nodes;
+ // ShadowDom v1
+ const slots = shadowRoot.querySelectorAll('slot');
+ if (slots.length && slots[0].assignedNodes) {
+ for (i = 0; i < slots.length; i++) {
+ nodes = slots[i].assignedNodes({
+ flatten: true
+ });
+ for (j = 0; j < nodes.length; j++) {
+ if (nodes[j].nodeType === Node.ELEMENT_NODE) {
+ result.add(nodes[j]);
+ }
+ }
+ }
+ // No need to search for .
+ return result;
+ }
+ // ShadowDom v0
+ const contents = shadowRoot.querySelectorAll('content');
+ if (contents.length && contents[0].getDistributedNodes) {
+ for (i = 0; i < contents.length; i++) {
+ nodes = contents[i].getDistributedNodes();
+ for (j = 0; j < nodes.length; j++) {
+ if (nodes[j].nodeType === Node.ELEMENT_NODE) {
+ result.add(nodes[j]);
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns if an element is inert.
+ * @param {!HTMLElement} element
+ * @returns {boolean}
+ * @private
+ */
+ static[_isInert](element) {
+ return element.inert;
+ }
+
+ /**
+ * Sets inert to an element.
+ * @param {!HTMLElement} element
+ * @param {boolean} inert
+ * @private
+ */
+ static[_setInert](element, inert) {
+ // Prefer setting the property over the attribute since the inert spec
+ // doesn't specify if it should be reflected.
+ // https://html.spec.whatwg.org/multipage/interaction.html#inert
+ element.inert = inert;
+ }
+ }
+
+ document.$blockingElements = new BlockingElements();
+
+})(document);
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..37c3424
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,33 @@
+{
+ "name": "blockingElements",
+ "description": "A polyfill for the proposed blocking elments stack API",
+ "main": "blocking-elements.html",
+ "authors": [
+ "Valdrin Koshi "
+ ],
+ "license": "Apache-2.0",
+ "keywords": [
+ "blocking",
+ "elements",
+ "polyfill",
+ "browser"
+ ],
+ "homepage": "https://github.com/PolymerLabs/blockingElements",
+ "private": true,
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests"
+ ],
+ "devDependencies": {
+ "test-fixture": "PolymerElements/test-fixture#ce-v1",
+ "inert": "WICG/inert#master",
+ "web-component-tester": "^4.0.0",
+ "webcomponentsjs": "webcomponents/webcomponentsjs#v1-polymer-edits"
+ },
+ "resolutions": {
+ "test-fixture": "ce-v1"
+ }
+}
diff --git a/demo/ce.html b/demo/ce.html
new file mode 100644
index 0000000..a0d3ec6
--- /dev/null
+++ b/demo/ce.html
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+ blockingElements polyfill test page
+
+
+
+
+
+
+
+
+
+
+
+
+ inert x-trap-focus
+
+
+
+
The National Institutes of Health had banned funding for these kinds of experiments last September, but is reconsidering allowing some under strict conditions.
The civil war ended on paper months ago, but clashes between rival factions halted food distribution for the country’s displaced, and sexual violence has risen.
+ Holding a steak knife in each hand, a guest announced: “Nobody’s getting out of here alive.” A chaotic party turned deadly in a Bronx precinct where The Times is covering every murder.
+ Alumni from a range of generations say they are baffled by today’s college culture. Among their laments: Students are too wrapped up in racial and identity politics.
+ David and Linda Gordon’s relationship simmered for decades, until that first summer at the Silver Gull Beach Club in Queens ushered in a Labor Day marriage proposal.
+ The afterlives of William Shakespeare and Jane Austen are the subject of “Will & Jane: Shakespeare, Austen, and the Cult of Celebrity,” an exhibition in Washington.