├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── demo └── index.html ├── explainer.md ├── karma.conf.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── security-privacy.md ├── src └── inert.js ├── test ├── .eslintrc ├── fixtures │ ├── aria-hidden.html │ ├── basic.html │ ├── interactives.html │ ├── nested.html │ └── tabindex.html └── specs │ ├── basic.spec.js │ ├── element.spec.js │ ├── helpers │ └── index.js │ ├── interactives.spec.js │ ├── nested.spec.js │ ├── reapply-aria-hidden.spec.js │ ├── reapply-tabindex.spec.js │ ├── shadow-dom-v0.spec.js │ └── shadow-dom-v1.spec.js └── w3c.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es3"], 4 | ["env", { 5 | "modules": false, 6 | "targets": { 7 | "browsers": ["last 2 versions", "ie 9"] 8 | } 9 | }] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/**/*.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "parserOptions": { 6 | "sourceType": "module" 7 | }, 8 | "extends": "google", 9 | "rules": { 10 | "no-var": "off", 11 | "max-len": [2, { 12 | "code": 100, 13 | "tabWidth": 2, 14 | "ignoreUrls": true 15 | }] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist 4 | .vscode 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | # 10 is good enough 4 | - '10' 5 | before_install: 6 | # package-lock.json was introduced in npm@5 7 | - '[[ $(node -v) =~ ^v9.*$ ]] || npm install -g npm@latest' # skipped when using node 9 8 | - npm install -g greenkeeper-lockfile 9 | install: npm install 10 | before_script: greenkeeper-lockfile-update 11 | after_script: greenkeeper-lockfile-upload 12 | env: 13 | global: 14 | secure: C1EunBv4YMNkx3Oba78uVnT+6UTeJjAx2PbGVtTG/MseiYWnYsn4srA9MBogbWT9rU8AckmsTII2GvAyMfWSozUcY8Z1JWr/nKp//mY4PHXuySMe/tqnATMNDsNPmJL6EyV+I7rmhcER+CoZtkq8YDrNyfoM88q6sBL70F5BpiMeCzKlyuVwPfaTkjhjtdmt4j8RFfRUSw62qw8Ji3i0JvMIjp+zrT8UtO1Ncf+lT8TQhUSPw6eiDj5yQYoJWTBbi/rhWHGRMBx9DLnEEbIm8uiVzHOTjnCLfG1Gh0UZaYdbRT6FW/tmp02ma/OJZu+gZG42TDgG5w34g+UwUcoKVnBkeLxhv/ceJGO6j5lRLbEnhyQAqMx9GzecuZfOFJNw2gW97oRnkXw19iFdSGS8/eoS8nOpN2x2hRTzhV4IImES0Xk787yNyKYfatBlojOC7tPf2TkXwGAotDdU23VooTRbNvCsWubtuJYx0C2GUM2qCoEzmCF1HmsGHwyFE9jc7OGiaiXLotOYnVUbI88S5gLMRKEDbNjreMXShk4fpyZ+n+LQ4YMIFzzl9Yk5O3C8a3kvzBDDI1K6GwiS0fNJrodV6jYtz8NbHr02S4BmANFW5F2mhvtkWrG91h/w+/WTkTGnmIfdWJaYui0BeBhSMuPllYILx1K+pReowrZnbi0= 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Web Platform Incubator Community Group 2 | 3 | This repository is being used for work in the W3C Web Platform Incubator 4 | Community Group, governed by the [W3C Community License Agreement 5 | (CLA)](http://www.w3.org/community/about/agreements/cla/). To make substantive 6 | contributions, you must join the CG. 7 | 8 | If you are not the sole contributor to a contribution (pull request), please 9 | identify all contributors in the pull request comment. 10 | 11 | To add a contributor (other than yourself, that's automatic), mark them one per 12 | line as follows: 13 | 14 | ``` 15 | +@github_username 16 | ``` 17 | 18 | If you added a contributor by mistake, you can remove them in a comment with: 19 | 20 | ``` 21 | -@github_username 22 | ``` 23 | 24 | If you are making a pull request on behalf of someone else but you had no part 25 | in designing the feature, you can remove yourself with the above syntax. 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All Reports in this Repository are licensed by Contributors 2 | under the 3 | [W3C Software and Document License](https://www.w3.org/Consortium/Legal/2023/software-license.html). 4 | 5 | Contributions to Specifications are made under the 6 | [W3C CLA](https://www.w3.org/community/about/agreements/cla/). 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The `inert` attribute/property allows web authors to mark parts of the DOM tree 2 | as [inert](https://html.spec.whatwg.org/multipage/interaction.html#inert): 3 | 4 | > When a node is inert, then the user agent must act as if the node was absent 5 | > for the purposes of targeting user interaction events, may ignore the node for 6 | > the purposes of text search user interfaces (commonly known as "find in 7 | > page"), and may prevent the user from selecting text in that node. 8 | 9 | Furthermore, a node which is **inert** should also be hidden from assistive 10 | technology. 11 | 12 | # Details 13 | 14 | - Read the [Explainer](explainer.md). 15 | - Read the [Spec](https://whatpr.org/html/4288/interaction.html#the-inert-attribute). 16 | - Try the [Demo](https://wicg.github.io/inert/demo/). 17 | - [Give feedback!](https://github.com/WICG/inert/issues) 18 | 19 | # Polyfill 20 | 21 | ## Installation 22 | 23 | `npm install --save wicg-inert` 24 | 25 | We recommend only using versions of the polyfill that have been published to 26 | npm, rather than cloning the repo and using the source directly. This helps 27 | ensure the version you're using is stable and thoroughly tested. 28 | 29 | This project provides two versions of the polyfill in `package.json`. 30 | 31 | - `main`: Points at `dist/inert.js` which is transpiled to ES3. 32 | - `module`: Points at `dist/inert.esm.js` which is transpiled to ES3. 33 | 34 | _If you do want to build from source, make sure you clone the latest tag!_ 35 | 36 | ## Usage 37 | 38 | ### 1. Either import the polyfill, or add the script to your page 39 | 40 | ``` 41 | import "wicg-inert"; 42 | ``` 43 | 44 | OR… 45 | 46 | ```html 47 | ... 48 | 49 | 50 | 51 | ``` 52 | 53 | ### 2. Toggle `inert` on an element 54 | 55 | ```js 56 | const el = document.querySelector('#my-element'); 57 | el.inert = true; // alternatively, el.setAttribute('inert', ''); 58 | ``` 59 | 60 | ### Legacy Browser Support 61 | 62 | If you want to use `inert` with an older browser you'll need to include 63 | additional polyfills for 64 | [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), 65 | [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set), 66 | [Element.prototype.matches](https://developer.mozilla.org/en-US/docs/Web/API/Element/matches), 67 | and [Node.prototype.contains](https://developer.mozilla.org/en-US/docs/Web/API/Node/contains). 68 | 69 | In accordance with the W3C's new [polyfill 70 | guidance](https://www.w3.org/2001/tag/doc/polyfills/#don-t-serve-unnecessary-polyfills), 71 | the `inert` polyfill does not bundle other polyfills. 72 | 73 | You can use a service like [https://cdnjs.cloudflare.com/polyfill](https://cdnjs.cloudflare.com/polyfill) 74 | to download only the polyfills needed by the current browser. Just add the 75 | following line to the start of your page: 76 | 77 | ```html 78 | 79 | ``` 80 | 81 | ### Strict Content Security Policy 82 | 83 | By default, this polyfill will dynamically insert some CSS, which requires the 84 | style-src rule of your Content Security Policy to allow 'unsafe-inline'. 85 | 86 | It is possible avoid doing so by including those rules from a CSS stylesheet, as 87 | follows (the `id` property is used by the polyfill to know if it needs to 88 | dynamically add the missing rules): 89 | 90 | ```html 91 | 92 | ``` 93 | 94 | The stylesheet should include the following rules: 95 | 96 | ```css 97 | [inert] { 98 | pointer-events: none; 99 | cursor: default; 100 | } 101 | 102 | [inert], [inert] * { 103 | user-select: none; 104 | -webkit-user-select: none; 105 | -ms-user-select: none; 106 | } 107 | ``` 108 | 109 | ### Performance and gotchas 110 | 111 | The polyfill attempts to provide a reasonable fidelity polyfill for the `inert` 112 | **attribute**, however please note: 113 | 114 | - It relies on mutation observers to detect the addition of the `inert` 115 | attribute, and to detect dynamically added content within inert subtrees. 116 | Testing for _inert_-ness in any way immediately after either type of mutation 117 | will therefore give inconsistent results; please allow the current task to end 118 | before relying on mutation-related changes to take effect, for example via 119 | `setTimeout(fn, 0)` or `Promise.resolve()`. 120 | 121 | Example: 122 | ```js 123 | const newButton = document.createElement('button'); 124 | const inertContainer = document.querySelector('[inert]'); 125 | inertContainer.appendChild(newButton); 126 | // Wait for the next microtask to allow mutation observers to react to the 127 | // DOM change 128 | Promise.resolve().then(() => { 129 | expect(isUnfocusable(newButton)).to.equal(true); 130 | }); 131 | ``` 132 | - Using the `inert` **property**, however, is synchronous. 133 | 134 | - The polyfill will be expensive, performance-wise, compared to a native `inert` 135 | implementation, because it requires a fair amount of tree-walking. To mitigate 136 | this, we recommend not using `inert` during performance sensitive actions 137 | (like during animations or scrolling). Instead, wait till these events are 138 | complete, or consider using 139 | [requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) 140 | to set `inert`. 141 | 142 | # Testing 143 | 144 | Tests are written using ES5 syntax. This is to avoid needing to transpile them 145 | for older browsers. There are a few modern features they rely upon, e.g. 146 | `Array.from` and `Promises`. These are polyfilled for the tests using 147 | [https://cdnjs.cloudflare.com/polyfill](https://cdnjs.cloudflare.com/polyfill). For a list of polyfilled features, check out 148 | the `polyfill` section in `karma.conf.js`. 149 | 150 | ### Big Thanks 151 | 152 | Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs][homepage] 153 | 154 | 155 | 156 | [homepage]: https://saucelabs.com 157 | 158 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | inert polyfill test page 10 | 58 | 59 | 60 | 61 |

What even is this?

62 |

A demo/test page for an experimental inert prollyfill. See the readme for the inert github project for more information.

63 | 64 | 65 |
66 |

The container below is marked declaratively in the source with the inert attribute

67 |
68 |

By adding the inert attribute to any element you make the elements inside it unfocusable (by mouse, pointer events or keyboard tabbing. 69 | You shouldn't be able to click this or set focus to it because it is inside a container that is inert.

70 | 73 | 74 | 77 |
78 |
79 | 80 |
81 |

82 | 83 |
84 |

85 | Clicking the checkbox above will toggle the element's inert property via

element.inert = evt.target.checked == true;
86 | You shouldn't be able to click this or set focus to it.. 87 | 88 |
89 |

This inner section is marked inert

90 |

Toggling the outer inert should not make this link become clickable.

91 |
92 | 93 |

94 |
95 |
96 | 97 | 98 | 99 |
100 |

101 | 102 |
103 |

104 | Clicking the checkbox above will toggle the element's inert property via

element.inert = evt.target.checked == true;
105 | You shouldn't be able to click this or set focus to it.. 106 | 107 |
108 |

This inner section is not marked inert

109 |

Toggling the inner inert should not make this link become clickable.

110 |
111 | 112 |

113 |
114 |
115 | 116 |
117 |

119 | 120 |
121 |

122 | Clicking the checkbox above will toggle the element's inert property via

element.inert = evt.target.checked == true;
123 | You shouldn't be able to click this or set focus to it.. 124 | 125 |
126 |

This inner section is not marked inert

127 |

Toggling the inner inert should not make this link become clickable or tabbable.

128 | Toggling the inner inert should not make this fake link become tabbable or clickable either. 129 |
130 | 131 |

132 |
133 |
134 | 135 | 136 | 137 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /explainer.md: -------------------------------------------------------------------------------- 1 | - [Background](#background) 2 | - [Spec](#spec) 3 | * [Spec gaps](#spec-gaps) 4 | - [The case for `inert` as a primitive](#the-case-for-inert-as-a-primitive) 5 | * [Use cases](#use-cases) 6 | - [Wouldn't this be better as...](#wouldnt-this-be-better-as) 7 | 8 | # Background 9 | 10 | The `inert` attribute was [originally specced](https://github.com/whatwg/html/commit/2fb24fcf) as part of the `` element specification. 11 | `` required the concept of `inert` to be defined in order to describe the blocking behaviour of dialogs, 12 | and the `inert` attribute was introduced ["so you could do `` without ``"](https://www.w3.org/Bugs/Public/show_bug.cgi?id=24983#c1). 13 | 14 | The attribute was later [removed](https://github.com/whatwg/html/commit/5ddfc78b1f82e86cc202d72ccc752a0e15f1e4ad) as it was argued that its only use case was subsumed by ``. However, later discussion on the [original bug](https://www.w3.org/Bugs/Public/show_bug.cgi?id=24983) proposed several use cases which could not be handled, or only handled poorly, using ``. 15 | 16 | ## Spec 17 | 18 | The spec for the `inert` attribute, 19 | with [the existing definition of "inert" already specified](https://html.spec.whatwg.org/multipage/interaction.html#inert), 20 | is extremely straightforward: 21 | 22 | >

The inert attribute

23 | 24 | >

The inert attribute is a 25 | > boolean attribute that indicates, by its presence, that 26 | > the element is to be made inert.

27 | 28 | >
29 | 30 | >

When an element has an inert 31 | > attribute, the user agent must mark that element as 32 | > inert.

33 | 34 | >
35 | 36 | >

By default, there is no visual indication of a 37 | > subtree being inert. Authors are encouraged to clearly mark what 38 | > parts of their document are active and which are inert, to avoid 39 | > user confusion. In particular, it is worth remembering that not all 40 | > users can see all parts of a page at once; for example, users of 41 | > screen readers, users on small devices or with magnifiers, and even 42 | > users just using particularly small windows might not be able to see 43 | > the active part of a page and may get frustrated if inert sections 44 | > are not obviously inert. For individual controls, the title="attr-input-disabled">disabled attribute is probably 46 | > more appropriate.

47 | 48 | ### Spec gaps 49 | 50 | - The spec does not explicitly state what effect `inert` has on the subtree of the element marked as `inert`, 51 | however it is implied by the note that `inert` causes the entire subtree of the element with the `inert` attribute to be made [_inert_](https://html.spec.whatwg.org/multipage/interaction.html#inert). 52 | The polyfill makes the assumption that the entire subtree becomes _inert_. 53 | - Furthermore, the spec is unclear as to whether the attribute applies into [shadow trees](https://dom.spec.whatwg.org/#concept-shadow-tree). 54 | Consistency with CSS attributes and with inheriting attributes like [`aria-hidden`](https://www.w3.org/TR/wai-aria/#aria-hidden) and [`lang`](https://html.spec.whatwg.org/multipage/dom.html#the-lang-and-xml:lang-attributes) imply that it should. 55 | The polyfill assumes that it does so. 56 | - The [existing description of _inert_](https://html.spec.whatwg.org/multipage/interaction.html#inert) is not specific about where pointer events which would have been targeted to an element in an inert subtree should go. 57 | (See also: discussion on the [WHATWG pull request](https://github.com/whatwg/html/pull/1474).) 58 | Does the event: 59 | 60 | 1. go to the next non-inert element in the hit test stack? 61 | (The inert element is "transparent" for pointer events.) 62 | 2. go to the next non-inert parent element? 63 | 3. simply not fire? 64 | 65 | Consistency with `pointer-events` would suggest (ii). The polyfill uses `pointer-events: none` and so models its behaviour. 66 | - The spec is also not explicit about whether the attribute should be [reflected](https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes). The polyfill assumes that it is. 67 | - The spec does not explicitly state that inert content should be [hidden from assistive technology](https://www.w3.org/WAI/PF/aria-implementation/#exclude_elements2). 68 | However, typically, the HTML spec does not provide this type of information. The polyfill makes _inert_ content hidden from assistive technology (via `aria-hidden`). 69 | - The spec does not make explicit that there is no way to "un-inert" a subtree of an inert subtree. 70 | 71 | ## The case for `inert` as a primitive 72 | 73 | Developers find themselves in situations where they'd like to be able to mark a part of the page "un-tabbable". 74 | Rob Dodson lays out one such example in his article ["Building better accessibility primitives"](https://robdodson.me/building-better-accessibility-primitives/#problem2disablingtabindex): 75 | 76 | > One problem: to [achieve a performance optimisation for animation] we must leave the drawer in the DOM at all times. 77 | > Meaning its focusable children are just sitting there offscreen, 78 | > and as the user is tabbing through the page eventually their focus will just disappear into the drawer and they won't know where it went. 79 | > I see this on responsive websites all the time. 80 | > This is just one example but I've also run into the need to disable tabindex when I'm animating between elements with opacity: 0, 81 | > or temporarily disabling large lists of custom controls, 82 | > and as others have pointed out, 83 | > you'd hit if you tried to build something like coverflow where you can see a preview of the next element but can't actually interact with it yet. 84 | 85 | `inert` would also allow slightly more straightforward polyfilling of both `` 86 | and the proposed, more primitive 87 | [`blockingElements`](https://github.com/whatwg/html/issues/897) 88 | API. 89 | See 90 | [Polymer Labs' `blockingElements` polyfill](https://github.com/PolymerLabs/blockingElements/blob/master/demo/index.html), 91 | based on this polyfill, 92 | for an example of how `inert` may be used for this purpose. 93 | Currently, since there is no way to express the "inertness" concept, 94 | polyfilling these APIs requires both focus event trapping 95 | to avoid focus cycling out of the dialog/blocking element 96 | (and thus as a side effect may prevent focus from walking out of the page at all) 97 | and a tree-walk 98 | (usually neglected by developers) 99 | to set `aria-hidden` on all sibling elements of the dialog or blocking element. 100 | 101 | On the implementer side, 102 | the vast majority of work involved in implementing `inert` is a necessary pre-cursor to both `` and `blockingElements` implementations, 103 | so by implementing `inert` first, 104 | implementers may get useful functionality into the hands of developers sooner while still laying the groundwork for one or both of these more complex APIs. 105 | 106 | ### Use cases 107 | 108 | - **Temporarily offscreen/hidden content** 109 | 110 | As discussed in the [article](https://robdodson.me/building-better-accessibility-primitives/#problem2disablingtabindex), 111 | there are a range of circumstances in which case it's desirable to add content to the DOM to be rendered but remain offscreen. 112 | 113 | In these cases, without `inert`, authors are forced to choose between 114 | an accessible experience for keyboard and assistive technology users, 115 | or the factors (such as performance) which make offscreen rendering desirable - 116 | or, performing all the contortions necessary to keep the offscreen content functionally "inert". 117 | 118 | These cases include: 119 | 120 | + rendering content, such as a menu, offscreen, before having it animate on-screen; 121 | + similarly, for content like a menu which may be repeatedly shown to the user, 122 | avoiding re-rendering this content each time; 123 | + disabling an element as you fade it in or out, without needing to rely on `transitionend` events; 124 | + a carousel or other type of content cycler (such as a "tweet cycler") 125 | which visually hides non-current items by placing them at a lower z-index than the active item, 126 | or by setting their `opacity` to zero, 127 | and animates transitions between items; 128 | + "infinitely scrolling" UI which re-uses and/or pre-renders nodes. 129 | 130 | - **On-screen but non-interactive content** 131 | 132 | Occasionally, UI designs require that certain content be visible or partially visible, 133 | but clearly non-interactive. 134 | Typically, this content is made non-interactive for pointer device users 135 | either via a semi-transparent overlay which provides a visual cue as well as intercepting pointer events, 136 | or via using `pointer-events: none`. 137 | 138 | In these cases developers are once again required to perform contortions in order to ensure that this content is not an accessibility issue. 139 | 140 | These cases include: 141 | 142 | + Any of the use cases for [`blockingElement[s]`](https://github.com/whatwg/html/issues/897): 143 | * a modal dialog; 144 | * a focus-trapping menu; 145 | * a [side nav](https://material.io/design/components/navigation-drawer.html). 146 | 147 | + A slide show or "cover flow" style carousel may have non-active items partially visible, 148 | as a preview - 149 | they may be transformed or partially obscured to indicate that they are non-interactive. 150 | 151 | + Form content which is not currently relevant, 152 | e.g. fading out and disabling the "Shipping Address" fields when the "Same as billing address" checkbox has been checked. 153 | 154 | + Disabling the entire UI while in an inconsistent state, 155 | such as showing a throbber/loading bar during unexpectedly slow loading. 156 | 157 | ## Wouldn't this be better as... 158 | 159 | - A **CSS property**? 160 | 161 | `inert` encompasses the behaviour of at least two other things which are CSS properties - 162 | `pointer-events: none` and `user-select: none`, plus another attribute, `aria-hidden`. 163 | These behaviours, along with the currently near-impossible to achieve behaviour of preventing tabbing/programmatic focus, are very frequently applied together 164 | (or if one, such as `aria-hidden`, is omitted, it is more often through lack of awareness than deliberate). 165 | 166 | There is scope for a more primitive CSS property to "explain" the ability of `inert` to prevent focus, however that could easily coexist with the `inert` attribute. 167 | 168 | - [`blockingElements`](https://github.com/whatwg/html/issues/897)? 169 | 170 | `blockingElements` (or, potentially, a single `blockingElement`) represents roughly the opposite use case to `inert`: 171 | a per-document, single element which blocks the document, analogous to the [blocking behaviour of a modal dialog](https://html.spec.whatwg.org/multipage/interaction.html#blocked-by-a-modal-dialog). 172 | 173 | It's not always the case that we will want a single subtree to be non-inert. Ideally, we would have both concepts available; 174 | however, `inert` allows reasonable approximation of `blockingElements` whereas the reverse is not true. 175 | - To approximate a `blockingElement` using `inert`, it's most straightforward to insert a non-_inert_ element as a sibling element to the main page content, and then use `inert` to mark the main page content as _inert_. 176 | More generally, all siblings of the desired "blocking" element, plus all siblings of all of its ancestors, could be marked _inert_. 177 | 178 | - A **programmatic API**? 179 | 180 | Something like `document.makeInert(el)`. 181 | 182 | This would require waiting for script execution before parts of the page became inert, which can take some time. 183 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | plugins: [ 6 | 'karma-chrome-launcher', 7 | 'karma-firefox-launcher', 8 | 'karma-mocha', 9 | 'karma-chai', 10 | 'karma-polyfill', 11 | 'karma-spec-reporter', 12 | 'karma-sourcemap-loader', 13 | 'karma-fixture', 14 | 'karma-html2js-preprocessor', 15 | 'karma-sauce-launcher', 16 | ], 17 | // Change this to 'Chrome' if you would like to debug. 18 | // Can also add additional local browsers like 'Firefox'. 19 | browsers: ['FirefoxHeadless'], 20 | // Set this to false to leave the browser open for debugging. 21 | // You'll probably also need to remove the afterEach block in your tests 22 | // so the content is not removed from the page you're trying to debug. 23 | // To isolate a test, use `it.only`. 24 | // https://mochajs.org/#exclusive-tests 25 | singleRun: true, 26 | // Use the mocha test framework with chai assertions. 27 | // Use polyfills loaded 28 | // Use an html fixture loader. 29 | frameworks: ['mocha', 'chai', 'polyfill', 'fixture'], 30 | // List of polyfills to load 31 | polyfill: [ 32 | 'Array.from', // Used in tests. 33 | 'Promise', 34 | 'Map', 35 | 'Set', 36 | 'Element.prototype.matches', 37 | 'Node.prototype.contains', 38 | ], 39 | preprocessors: { 40 | '**/*.html': ['html2js'], 41 | }, 42 | // The root path location that will be used to resolve all relative paths 43 | // defined in files and exclude. 44 | basePath: path.resolve(__dirname), 45 | // Things in the files array will be injected into the page. 46 | files: [ 47 | 'dist/inert.js', 48 | 'test/fixtures/**/*', 49 | 'test/specs/helpers/**/*', 50 | 'test/specs/**/*.spec.js', 51 | ], 52 | // Report output to console. 53 | reporters: ['spec'], 54 | }); 55 | 56 | // If we're on Travis, override config settings and run tests on SauceLabs. 57 | if (process.env.TRAVIS || process.env.SAUCE) { 58 | // List of browsers to test on SauceLabs. 59 | // To add more browsers, use: 60 | // https://docs.saucelabs.com/visual/e2e-testing/supported-browsers/#browser-versions-supported 61 | const customLaunchers = { 62 | 'SL_Chrome': { 63 | base: 'SauceLabs', 64 | browserName: 'chrome', 65 | version: '100', 66 | }, 67 | 'SL_Firefox': { 68 | base: 'SauceLabs', 69 | browserName: 'firefox', 70 | version: '100', 71 | }, 72 | 'SL_Safari': { 73 | base: 'SauceLabs', 74 | browserName: 'safari', 75 | platform: 'OS X 11.00', 76 | version: '14', 77 | }, 78 | }; 79 | 80 | config.set({ 81 | sauceLabs: { 82 | testName: 'Inert Polyfill Tests', 83 | username: 'robdodson_inert', 84 | accessKey: 'a844aee9-d3ec-4566-94e3-dba3d0c30248', 85 | }, 86 | // Increase timeout in case connection in CI is slow 87 | captureTimeout: 120000, 88 | customLaunchers: customLaunchers, 89 | browsers: Object.keys(customLaunchers), 90 | reporters: ['spec', 'saucelabs'], 91 | }); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wicg-inert", 3 | "version": "3.1.3", 4 | "description": "A polyfill for the proposed inert API", 5 | "main": "dist/inert.js", 6 | "module": "dist/inert.esm.js", 7 | "scripts": { 8 | "build": "rollup -c" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/WICG/inert.git" 13 | }, 14 | "keywords": [ 15 | "inert", 16 | "polyfill", 17 | "browser" 18 | ], 19 | "author": "Alice Boxhall ", 20 | "contributors": [ 21 | "Rob Dodson ", 22 | "Jesse Beach", 23 | "Brian Kardell", 24 | "Valdrin Koshi" 25 | ], 26 | "bugs": { 27 | "url": "https://github.com/WICG/inert/issues" 28 | }, 29 | "homepage": "https://github.com/WICG/inert#readme", 30 | "devDependencies": { 31 | "babel-core": "^6.26.3", 32 | "babel-preset-env": "^1.6.1", 33 | "babel-preset-es3": "^1.0.1", 34 | "chai": "^4.1.2", 35 | "eslint": "^5.0.0", 36 | "eslint-config-google": "^0.9.1", 37 | "eslint-plugin-es5": "^1.3.1", 38 | "husky": "^0.14.3", 39 | "karma": "^6.3.20", 40 | "karma-chai": "^0.1.0", 41 | "karma-chrome-launcher": "^2.2.0", 42 | "karma-firefox-launcher": "^1.1.0", 43 | "karma-fixture": "^0.2.6", 44 | "karma-html2js-preprocessor": "^1.1.0", 45 | "karma-mocha": "^2.0.1", 46 | "karma-polyfill": "^1.0.0", 47 | "karma-safari-launcher": "^1.0.0", 48 | "karma-sauce-launcher": "^1.2.0", 49 | "karma-sourcemap-loader": "^0.3.7", 50 | "karma-spec-reporter": "0.0.32", 51 | "lint-staged": "^7.0.0", 52 | "mocha": "^10.0.0", 53 | "rollup": "^0.65.0", 54 | "rollup-plugin-babel": "^3.0.4", 55 | "rollup-plugin-uglify": "^4.0.0" 56 | }, 57 | "license": "W3C-20150513" 58 | } 59 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import {uglify} from 'rollup-plugin-uglify'; 2 | import babel from 'rollup-plugin-babel'; 3 | 4 | export default [ 5 | { 6 | input: 'src/inert.js', 7 | output: { 8 | file: 'dist/inert.esm.js', 9 | format: 'esm', 10 | }, 11 | plugins: [ 12 | babel({ 13 | exclude: 'node_modules/**', 14 | }), 15 | ], 16 | }, 17 | { 18 | input: 'src/inert.js', 19 | output: { 20 | file: 'dist/inert.js', 21 | format: 'umd', 22 | amd: { 23 | id: 'inert', 24 | }, 25 | }, 26 | plugins: [ 27 | babel({ 28 | exclude: 'node_modules/**', 29 | }), 30 | ], 31 | }, 32 | { 33 | input: 'src/inert.js', 34 | output: { 35 | file: 'dist/inert.min.js', 36 | format: 'umd', 37 | amd: { 38 | id: 'inert', 39 | }, 40 | sourcemap: true, 41 | }, 42 | plugins: [ 43 | babel({ 44 | exclude: 'node_modules/**', 45 | }), 46 | uglify(), 47 | ], 48 | }, 49 | ]; 50 | -------------------------------------------------------------------------------- /security-privacy.md: -------------------------------------------------------------------------------- 1 | # [Self-Review Questionnaire: Security and Privacy](https://w3ctag.github.io/security-questionnaire/) 2 | 3 | > 01. What information might this feature expose to Web sites or other parties, 4 | and for what purposes is that exposure necessary? 5 | 6 | None. 7 | 8 | > 02. Do features in your specification expose the minimum amount of information 9 | necessary to enable their intended uses? 10 | 11 | Yes. 12 | 13 | > 03. How do the features in your specification deal with personal information, 14 | personally-identifiable information (PII), or information derived from 15 | them? 16 | 17 | No PII is used. 18 | 19 | > 04. How do the features in your specification deal with sensitive information? 20 | 21 | No sensitive information is used. 22 | 23 | > 05. Do the features in your specification introduce new state for an origin 24 | that persists across browsing sessions? 25 | 26 | No. 27 | 28 | > 06. Do the features in your specification expose information about the 29 | underlying platform to origins? 30 | 31 | No. 32 | 33 | > 07. Does this specification allow an origin to send data to the underlying 34 | platform? 35 | 36 | No. 37 | 38 | > 08. Do features in this specification allow an origin access to sensors on a user’s 39 | device 40 | 41 | No. 42 | 43 | > 09. What data do the features in this specification expose to an origin? Please 44 | also document what data is identical to data exposed by other features, in the 45 | same or different contexts. 46 | 47 | No data is exposed to any origin. 48 | 49 | > 10. Do feautres in this specification enable new script execution/loading 50 | mechanisms? 51 | 52 | No. 53 | 54 | > 11. Do features in this specification allow an origin to access other devices? 55 | 56 | No. 57 | 58 | > 12. Do features in this specification allow an origin some measure of control over 59 | a user agent's native UI? 60 | 61 | No. 62 | 63 | > 13. What temporary identifiers do the feautures in this specification create or 64 | expose to the web? 65 | 66 | None. 67 | 68 | > 14. How does this specification distinguish between behavior in first-party and 69 | third-party contexts? 70 | 71 | It doesn't. 72 | 73 | > 15. How do the features in this specification work in the context of a browser’s 74 | Private Browsing or Incognito mode? 75 | 76 | This feature behaves identically in any mode. 77 | 78 | > 16. Does this specification have both "Security Considerations" and "Privacy 79 | Considerations" sections? 80 | 81 | No. 82 | 83 | > 17. Do features in your specification enable origins to downgrade default 84 | security protections? 85 | 86 | No. 87 | 88 | > 18. What should this questionnaire have asked? 89 | 90 | 🤷 91 | -------------------------------------------------------------------------------- /src/inert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This work is licensed under the W3C Software and Document License 3 | * (http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). 4 | */ 5 | 6 | (function() { 7 | // Return early if we're not running inside of the browser. 8 | if (typeof window === 'undefined' || typeof Element === 'undefined') { 9 | return; 10 | } 11 | 12 | // Convenience function for converting NodeLists. 13 | /** @type {typeof Array.prototype.slice} */ 14 | const slice = Array.prototype.slice; 15 | 16 | /** 17 | * IE has a non-standard name for "matches". 18 | * @type {typeof Element.prototype.matches} 19 | */ 20 | const matches = 21 | Element.prototype.matches || Element.prototype.msMatchesSelector; 22 | 23 | /** @type {string} */ 24 | const _focusableElementsString = ['a[href]', 25 | 'area[href]', 26 | 'input:not([disabled])', 27 | 'select:not([disabled])', 28 | 'textarea:not([disabled])', 29 | 'button:not([disabled])', 30 | 'details', 31 | 'summary', 32 | 'iframe', 33 | 'object', 34 | 'embed', 35 | 'video', 36 | '[contenteditable]'].join(','); 37 | 38 | /** 39 | * `InertRoot` manages a single inert subtree, i.e. a DOM subtree whose root element has an `inert` 40 | * attribute. 41 | * 42 | * Its main functions are: 43 | * 44 | * - to create and maintain a set of managed `InertNode`s, including when mutations occur in the 45 | * subtree. The `makeSubtreeUnfocusable()` method handles collecting `InertNode`s via registering 46 | * each focusable node in the subtree with the singleton `InertManager` which manages all known 47 | * focusable nodes within inert subtrees. `InertManager` ensures that a single `InertNode` 48 | * instance exists for each focusable node which has at least one inert root as an ancestor. 49 | * 50 | * - to notify all managed `InertNode`s when this subtree stops being inert (i.e. when the `inert` 51 | * attribute is removed from the root node). This is handled in the destructor, which calls the 52 | * `deregister` method on `InertManager` for each managed inert node. 53 | */ 54 | class InertRoot { 55 | /** 56 | * @param {!HTMLElement} rootElement The HTMLElement at the root of the inert subtree. 57 | * @param {!InertManager} inertManager The global singleton InertManager object. 58 | */ 59 | constructor(rootElement, inertManager) { 60 | /** @type {!InertManager} */ 61 | this._inertManager = inertManager; 62 | 63 | /** @type {!HTMLElement} */ 64 | this._rootElement = rootElement; 65 | 66 | /** 67 | * @type {!Set} 68 | * All managed focusable nodes in this InertRoot's subtree. 69 | */ 70 | this._managedNodes = new Set(); 71 | 72 | // Make the subtree hidden from assistive technology 73 | if (this._rootElement.hasAttribute('aria-hidden')) { 74 | /** @type {?string} */ 75 | this._savedAriaHidden = this._rootElement.getAttribute('aria-hidden'); 76 | } else { 77 | this._savedAriaHidden = null; 78 | } 79 | this._rootElement.setAttribute('aria-hidden', 'true'); 80 | 81 | // Make all focusable elements in the subtree unfocusable and add them to _managedNodes 82 | this._makeSubtreeUnfocusable(this._rootElement); 83 | 84 | // Watch for: 85 | // - any additions in the subtree: make them unfocusable too 86 | // - any removals from the subtree: remove them from this inert root's managed nodes 87 | // - attribute changes: if `tabindex` is added, or removed from an intrinsically focusable 88 | // element, make that node a managed node. 89 | this._observer = new MutationObserver(this._onMutation.bind(this)); 90 | this._observer.observe(this._rootElement, {attributes: true, childList: true, subtree: true}); 91 | } 92 | 93 | /** 94 | * Call this whenever this object is about to become obsolete. This unwinds all of the state 95 | * stored in this object and updates the state of all of the managed nodes. 96 | */ 97 | destructor() { 98 | this._observer.disconnect(); 99 | 100 | if (this._rootElement) { 101 | if (this._savedAriaHidden !== null) { 102 | this._rootElement.setAttribute('aria-hidden', this._savedAriaHidden); 103 | } else { 104 | this._rootElement.removeAttribute('aria-hidden'); 105 | } 106 | } 107 | 108 | this._managedNodes.forEach(function(inertNode) { 109 | this._unmanageNode(inertNode.node); 110 | }, this); 111 | 112 | // Note we cast the nulls to the ANY type here because: 113 | // 1) We want the class properties to be declared as non-null, or else we 114 | // need even more casts throughout this code. All bets are off if an 115 | // instance has been destroyed and a method is called. 116 | // 2) We don't want to cast "this", because we want type-aware optimizations 117 | // to know which properties we're setting. 118 | this._observer = /** @type {?} */ (null); 119 | this._rootElement = /** @type {?} */ (null); 120 | this._managedNodes = /** @type {?} */ (null); 121 | this._inertManager = /** @type {?} */ (null); 122 | } 123 | 124 | /** 125 | * @return {!Set} A copy of this InertRoot's managed nodes set. 126 | */ 127 | get managedNodes() { 128 | return new Set(this._managedNodes); 129 | } 130 | 131 | /** @return {boolean} */ 132 | get hasSavedAriaHidden() { 133 | return this._savedAriaHidden !== null; 134 | } 135 | 136 | /** @param {?string} ariaHidden */ 137 | set savedAriaHidden(ariaHidden) { 138 | this._savedAriaHidden = ariaHidden; 139 | } 140 | 141 | /** @return {?string} */ 142 | get savedAriaHidden() { 143 | return this._savedAriaHidden; 144 | } 145 | 146 | /** 147 | * @param {!Node} startNode 148 | */ 149 | _makeSubtreeUnfocusable(startNode) { 150 | composedTreeWalk(startNode, (node) => this._visitNode(node)); 151 | 152 | let activeElement = document.activeElement; 153 | 154 | if (!document.body.contains(startNode)) { 155 | // startNode may be in shadow DOM, so find its nearest shadowRoot to get the activeElement. 156 | let node = startNode; 157 | /** @type {!ShadowRoot|undefined} */ 158 | let root = undefined; 159 | while (node) { 160 | if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { 161 | root = /** @type {!ShadowRoot} */ (node); 162 | break; 163 | } 164 | node = node.parentNode; 165 | } 166 | if (root) { 167 | activeElement = root.activeElement; 168 | } 169 | } 170 | if (startNode.contains(activeElement)) { 171 | activeElement.blur(); 172 | // In IE11, if an element is already focused, and then set to tabindex=-1 173 | // calling blur() will not actually move the focus. 174 | // To work around this we call focus() on the body instead. 175 | if (activeElement === document.activeElement) { 176 | document.body.focus(); 177 | } 178 | } 179 | } 180 | 181 | /** 182 | * @param {!Node} node 183 | */ 184 | _visitNode(node) { 185 | if (node.nodeType !== Node.ELEMENT_NODE) { 186 | return; 187 | } 188 | const element = /** @type {!HTMLElement} */ (node); 189 | 190 | // If a descendant inert root becomes un-inert, its descendants will still be inert because of 191 | // this inert root, so all of its managed nodes need to be adopted by this InertRoot. 192 | if (element !== this._rootElement && element.hasAttribute('inert')) { 193 | this._adoptInertRoot(element); 194 | } 195 | 196 | if (matches.call(element, _focusableElementsString) || element.hasAttribute('tabindex')) { 197 | this._manageNode(element); 198 | } 199 | } 200 | 201 | /** 202 | * Register the given node with this InertRoot and with InertManager. 203 | * @param {!Node} node 204 | */ 205 | _manageNode(node) { 206 | const inertNode = this._inertManager.register(node, this); 207 | this._managedNodes.add(inertNode); 208 | } 209 | 210 | /** 211 | * Unregister the given node with this InertRoot and with InertManager. 212 | * @param {!Node} node 213 | */ 214 | _unmanageNode(node) { 215 | const inertNode = this._inertManager.deregister(node, this); 216 | if (inertNode) { 217 | this._managedNodes.delete(inertNode); 218 | } 219 | } 220 | 221 | /** 222 | * Unregister the entire subtree starting at `startNode`. 223 | * @param {!Node} startNode 224 | */ 225 | _unmanageSubtree(startNode) { 226 | composedTreeWalk(startNode, (node) => this._unmanageNode(node)); 227 | } 228 | 229 | /** 230 | * If a descendant node is found with an `inert` attribute, adopt its managed nodes. 231 | * @param {!HTMLElement} node 232 | */ 233 | _adoptInertRoot(node) { 234 | let inertSubroot = this._inertManager.getInertRoot(node); 235 | 236 | // During initialisation this inert root may not have been registered yet, 237 | // so register it now if need be. 238 | if (!inertSubroot) { 239 | this._inertManager.setInert(node, true); 240 | inertSubroot = this._inertManager.getInertRoot(node); 241 | } 242 | 243 | inertSubroot.managedNodes.forEach(function(savedInertNode) { 244 | this._manageNode(savedInertNode.node); 245 | }, this); 246 | } 247 | 248 | /** 249 | * Callback used when mutation observer detects subtree additions, removals, or attribute changes. 250 | * @param {!Array} records 251 | * @param {!MutationObserver} self 252 | */ 253 | _onMutation(records, self) { 254 | records.forEach(function(record) { 255 | const target = /** @type {!HTMLElement} */ (record.target); 256 | if (record.type === 'childList') { 257 | // Manage added nodes 258 | slice.call(record.addedNodes).forEach(function(node) { 259 | this._makeSubtreeUnfocusable(node); 260 | }, this); 261 | 262 | // Un-manage removed nodes 263 | slice.call(record.removedNodes).forEach(function(node) { 264 | this._unmanageSubtree(node); 265 | }, this); 266 | } else if (record.type === 'attributes') { 267 | if (record.attributeName === 'tabindex') { 268 | // Re-initialise inert node if tabindex changes 269 | this._manageNode(target); 270 | } else if (target !== this._rootElement && 271 | record.attributeName === 'inert' && 272 | target.hasAttribute('inert')) { 273 | // If a new inert root is added, adopt its managed nodes and make sure it knows about the 274 | // already managed nodes from this inert subroot. 275 | this._adoptInertRoot(target); 276 | const inertSubroot = this._inertManager.getInertRoot(target); 277 | this._managedNodes.forEach(function(managedNode) { 278 | if (target.contains(managedNode.node)) { 279 | inertSubroot._manageNode(managedNode.node); 280 | } 281 | }); 282 | } 283 | } 284 | }, this); 285 | } 286 | } 287 | 288 | /** 289 | * `InertNode` initialises and manages a single inert node. 290 | * A node is inert if it is a descendant of one or more inert root elements. 291 | * 292 | * On construction, `InertNode` saves the existing `tabindex` value for the node, if any, and 293 | * either removes the `tabindex` attribute or sets it to `-1`, depending on whether the element 294 | * is intrinsically focusable or not. 295 | * 296 | * `InertNode` maintains a set of `InertRoot`s which are descendants of this `InertNode`. When an 297 | * `InertRoot` is destroyed, and calls `InertManager.deregister()`, the `InertManager` notifies the 298 | * `InertNode` via `removeInertRoot()`, which in turn destroys the `InertNode` if no `InertRoot`s 299 | * remain in the set. On destruction, `InertNode` reinstates the stored `tabindex` if one exists, 300 | * or removes the `tabindex` attribute if the element is intrinsically focusable. 301 | */ 302 | class InertNode { 303 | /** 304 | * @param {!Node} node A focusable element to be made inert. 305 | * @param {!InertRoot} inertRoot The inert root element associated with this inert node. 306 | */ 307 | constructor(node, inertRoot) { 308 | /** @type {!Node} */ 309 | this._node = node; 310 | 311 | /** @type {boolean} */ 312 | this._overrodeFocusMethod = false; 313 | 314 | /** 315 | * @type {!Set} The set of descendant inert roots. 316 | * If and only if this set becomes empty, this node is no longer inert. 317 | */ 318 | this._inertRoots = new Set([inertRoot]); 319 | 320 | /** @type {?number} */ 321 | this._savedTabIndex = null; 322 | 323 | /** @type {boolean} */ 324 | this._destroyed = false; 325 | 326 | // Save any prior tabindex info and make this node untabbable 327 | this.ensureUntabbable(); 328 | } 329 | 330 | /** 331 | * Call this whenever this object is about to become obsolete. 332 | * This makes the managed node focusable again and deletes all of the previously stored state. 333 | */ 334 | destructor() { 335 | this._throwIfDestroyed(); 336 | 337 | if (this._node && this._node.nodeType === Node.ELEMENT_NODE) { 338 | const element = /** @type {!HTMLElement} */ (this._node); 339 | if (this._savedTabIndex !== null) { 340 | element.setAttribute('tabindex', this._savedTabIndex); 341 | } else { 342 | element.removeAttribute('tabindex'); 343 | } 344 | 345 | // Use `delete` to restore native focus method. 346 | if (this._overrodeFocusMethod) { 347 | delete element.focus; 348 | } 349 | } 350 | 351 | // See note in InertRoot.destructor for why we cast these nulls to ANY. 352 | this._node = /** @type {?} */ (null); 353 | this._inertRoots = /** @type {?} */ (null); 354 | this._destroyed = true; 355 | } 356 | 357 | /** 358 | * @type {boolean} Whether this object is obsolete because the managed node is no longer inert. 359 | * If the object has been destroyed, any attempt to access it will cause an exception. 360 | */ 361 | get destroyed() { 362 | return /** @type {!InertNode} */ (this)._destroyed; 363 | } 364 | 365 | /** 366 | * Throw if user tries to access destroyed InertNode. 367 | */ 368 | _throwIfDestroyed() { 369 | if (this.destroyed) { 370 | throw new Error('Trying to access destroyed InertNode'); 371 | } 372 | } 373 | 374 | /** @return {boolean} */ 375 | get hasSavedTabIndex() { 376 | return this._savedTabIndex !== null; 377 | } 378 | 379 | /** @return {!Node} */ 380 | get node() { 381 | this._throwIfDestroyed(); 382 | return this._node; 383 | } 384 | 385 | /** @param {?number} tabIndex */ 386 | set savedTabIndex(tabIndex) { 387 | this._throwIfDestroyed(); 388 | this._savedTabIndex = tabIndex; 389 | } 390 | 391 | /** @return {?number} */ 392 | get savedTabIndex() { 393 | this._throwIfDestroyed(); 394 | return this._savedTabIndex; 395 | } 396 | 397 | /** Save the existing tabindex value and make the node untabbable and unfocusable */ 398 | ensureUntabbable() { 399 | if (this.node.nodeType !== Node.ELEMENT_NODE) { 400 | return; 401 | } 402 | const element = /** @type {!HTMLElement} */ (this.node); 403 | if (matches.call(element, _focusableElementsString)) { 404 | if (/** @type {!HTMLElement} */ (element).tabIndex === -1 && 405 | this.hasSavedTabIndex) { 406 | return; 407 | } 408 | 409 | if (element.hasAttribute('tabindex')) { 410 | this._savedTabIndex = /** @type {!HTMLElement} */ (element).tabIndex; 411 | } 412 | element.setAttribute('tabindex', '-1'); 413 | if (element.nodeType === Node.ELEMENT_NODE) { 414 | element.focus = function() {}; 415 | this._overrodeFocusMethod = true; 416 | } 417 | } else if (element.hasAttribute('tabindex')) { 418 | this._savedTabIndex = /** @type {!HTMLElement} */ (element).tabIndex; 419 | element.removeAttribute('tabindex'); 420 | } 421 | } 422 | 423 | /** 424 | * Add another inert root to this inert node's set of managing inert roots. 425 | * @param {!InertRoot} inertRoot 426 | */ 427 | addInertRoot(inertRoot) { 428 | this._throwIfDestroyed(); 429 | this._inertRoots.add(inertRoot); 430 | } 431 | 432 | /** 433 | * Remove the given inert root from this inert node's set of managing inert roots. 434 | * If the set of managing inert roots becomes empty, this node is no longer inert, 435 | * so the object should be destroyed. 436 | * @param {!InertRoot} inertRoot 437 | */ 438 | removeInertRoot(inertRoot) { 439 | this._throwIfDestroyed(); 440 | this._inertRoots.delete(inertRoot); 441 | if (this._inertRoots.size === 0) { 442 | this.destructor(); 443 | } 444 | } 445 | } 446 | 447 | /** 448 | * InertManager is a per-document singleton object which manages all inert roots and nodes. 449 | * 450 | * When an element becomes an inert root by having an `inert` attribute set and/or its `inert` 451 | * property set to `true`, the `setInert` method creates an `InertRoot` object for the element. 452 | * The `InertRoot` in turn registers itself as managing all of the element's focusable descendant 453 | * nodes via the `register()` method. The `InertManager` ensures that a single `InertNode` instance 454 | * is created for each such node, via the `_managedNodes` map. 455 | */ 456 | class InertManager { 457 | /** 458 | * @param {!Document} document 459 | */ 460 | constructor(document) { 461 | if (!document) { 462 | throw new Error('Missing required argument; InertManager needs to wrap a document.'); 463 | } 464 | 465 | /** @type {!Document} */ 466 | this._document = document; 467 | 468 | /** 469 | * All managed nodes known to this InertManager. In a map to allow looking up by Node. 470 | * @type {!Map} 471 | */ 472 | this._managedNodes = new Map(); 473 | 474 | /** 475 | * All inert roots known to this InertManager. In a map to allow looking up by Node. 476 | * @type {!Map} 477 | */ 478 | this._inertRoots = new Map(); 479 | 480 | /** 481 | * Observer for mutations on `document.body`. 482 | * @type {!MutationObserver} 483 | */ 484 | this._observer = new MutationObserver(this._watchForInert.bind(this)); 485 | 486 | // Add inert style. 487 | addInertStyle(document.head || document.body || document.documentElement); 488 | 489 | // Wait for document to be loaded. 490 | if (document.readyState === 'loading') { 491 | document.addEventListener('DOMContentLoaded', this._onDocumentLoaded.bind(this)); 492 | } else { 493 | this._onDocumentLoaded(); 494 | } 495 | } 496 | 497 | /** 498 | * Set whether the given element should be an inert root or not. 499 | * @param {!HTMLElement} root 500 | * @param {boolean} inert 501 | */ 502 | setInert(root, inert) { 503 | if (inert) { 504 | if (this._inertRoots.has(root)) { // element is already inert 505 | return; 506 | } 507 | 508 | const inertRoot = new InertRoot(root, this); 509 | root.setAttribute('inert', ''); 510 | this._inertRoots.set(root, inertRoot); 511 | // If not contained in the document, it must be in a shadowRoot. 512 | // Ensure inert styles are added there. 513 | if (!this._document.body.contains(root)) { 514 | let parent = root.parentNode; 515 | while (parent) { 516 | if (parent.nodeType === 11) { 517 | addInertStyle(parent); 518 | } 519 | parent = parent.parentNode; 520 | } 521 | } 522 | } else { 523 | if (!this._inertRoots.has(root)) { // element is already non-inert 524 | return; 525 | } 526 | 527 | const inertRoot = this._inertRoots.get(root); 528 | inertRoot.destructor(); 529 | this._inertRoots.delete(root); 530 | root.removeAttribute('inert'); 531 | } 532 | } 533 | 534 | /** 535 | * Get the InertRoot object corresponding to the given inert root element, if any. 536 | * @param {!Node} element 537 | * @return {!InertRoot|undefined} 538 | */ 539 | getInertRoot(element) { 540 | return this._inertRoots.get(element); 541 | } 542 | 543 | /** 544 | * Register the given InertRoot as managing the given node. 545 | * In the case where the node has a previously existing inert root, this inert root will 546 | * be added to its set of inert roots. 547 | * @param {!Node} node 548 | * @param {!InertRoot} inertRoot 549 | * @return {!InertNode} inertNode 550 | */ 551 | register(node, inertRoot) { 552 | let inertNode = this._managedNodes.get(node); 553 | if (inertNode !== undefined) { // node was already in an inert subtree 554 | inertNode.addInertRoot(inertRoot); 555 | } else { 556 | inertNode = new InertNode(node, inertRoot); 557 | } 558 | 559 | this._managedNodes.set(node, inertNode); 560 | 561 | return inertNode; 562 | } 563 | 564 | /** 565 | * De-register the given InertRoot as managing the given inert node. 566 | * Removes the inert root from the InertNode's set of managing inert roots, and remove the inert 567 | * node from the InertManager's set of managed nodes if it is destroyed. 568 | * If the node is not currently managed, this is essentially a no-op. 569 | * @param {!Node} node 570 | * @param {!InertRoot} inertRoot 571 | * @return {?InertNode} The potentially destroyed InertNode associated with this node, if any. 572 | */ 573 | deregister(node, inertRoot) { 574 | const inertNode = this._managedNodes.get(node); 575 | if (!inertNode) { 576 | return null; 577 | } 578 | 579 | inertNode.removeInertRoot(inertRoot); 580 | if (inertNode.destroyed) { 581 | this._managedNodes.delete(node); 582 | } 583 | 584 | return inertNode; 585 | } 586 | 587 | /** 588 | * Callback used when document has finished loading. 589 | */ 590 | _onDocumentLoaded() { 591 | // Find all inert roots in document and make them actually inert. 592 | const inertElements = slice.call(this._document.querySelectorAll('[inert]')); 593 | inertElements.forEach(function(inertElement) { 594 | this.setInert(inertElement, true); 595 | }, this); 596 | 597 | // Comment this out to use programmatic API only. 598 | this._observer.observe(this._document.body || this._document.documentElement, {attributes: true, subtree: true, childList: true}); 599 | } 600 | 601 | /** 602 | * Callback used when mutation observer detects attribute changes. 603 | * @param {!Array} records 604 | * @param {!MutationObserver} self 605 | */ 606 | _watchForInert(records, self) { 607 | const _this = this; 608 | records.forEach(function(record) { 609 | switch (record.type) { 610 | case 'childList': 611 | slice.call(record.addedNodes).forEach(function(node) { 612 | if (node.nodeType !== Node.ELEMENT_NODE) { 613 | return; 614 | } 615 | const inertElements = slice.call(node.querySelectorAll('[inert]')); 616 | if (matches.call(node, '[inert]')) { 617 | inertElements.unshift(node); 618 | } 619 | inertElements.forEach(function(inertElement) { 620 | this.setInert(inertElement, true); 621 | }, _this); 622 | }, _this); 623 | break; 624 | case 'attributes': 625 | if (record.attributeName !== 'inert') { 626 | return; 627 | } 628 | const target = /** @type {!HTMLElement} */ (record.target); 629 | const inert = target.hasAttribute('inert'); 630 | _this.setInert(target, inert); 631 | break; 632 | } 633 | }, this); 634 | } 635 | } 636 | 637 | /** 638 | * Recursively walk the composed tree from |node|. 639 | * @param {!Node} node 640 | * @param {(function (!HTMLElement))=} callback Callback to be called for each element traversed, 641 | * before descending into child nodes. 642 | * @param {?ShadowRoot=} shadowRootAncestor The nearest ShadowRoot ancestor, if any. 643 | */ 644 | function composedTreeWalk(node, callback, shadowRootAncestor) { 645 | if (node.nodeType == Node.ELEMENT_NODE) { 646 | const element = /** @type {!HTMLElement} */ (node); 647 | if (callback) { 648 | callback(element); 649 | } 650 | 651 | // Descend into node: 652 | // If it has a ShadowRoot, ignore all child elements - these will be picked 653 | // up by the or elements. Descend straight into the 654 | // ShadowRoot. 655 | const shadowRoot = /** @type {!HTMLElement} */ (element).shadowRoot; 656 | if (shadowRoot) { 657 | composedTreeWalk(shadowRoot, callback, shadowRoot); 658 | return; 659 | } 660 | 661 | // If it is a element, descend into distributed elements - these 662 | // are elements from outside the shadow root which are rendered inside the 663 | // shadow DOM. 664 | if (element.localName == 'content') { 665 | const content = /** @type {!HTMLContentElement} */ (element); 666 | // Verifies if ShadowDom v0 is supported. 667 | const distributedNodes = content.getDistributedNodes ? 668 | content.getDistributedNodes() : []; 669 | for (let i = 0; i < distributedNodes.length; i++) { 670 | composedTreeWalk(distributedNodes[i], callback, shadowRootAncestor); 671 | } 672 | return; 673 | } 674 | 675 | // If it is a element, descend into assigned nodes - these 676 | // are elements from outside the shadow root which are rendered inside the 677 | // shadow DOM. 678 | if (element.localName == 'slot') { 679 | const slot = /** @type {!HTMLSlotElement} */ (element); 680 | // Verify if ShadowDom v1 is supported. 681 | const distributedNodes = slot.assignedNodes ? 682 | slot.assignedNodes({flatten: true}) : []; 683 | for (let i = 0; i < distributedNodes.length; i++) { 684 | composedTreeWalk(distributedNodes[i], callback, shadowRootAncestor); 685 | } 686 | return; 687 | } 688 | } 689 | 690 | // If it is neither the parent of a ShadowRoot, a element, a 691 | // element, nor a element recurse normally. 692 | let child = node.firstChild; 693 | while (child != null) { 694 | composedTreeWalk(child, callback, shadowRootAncestor); 695 | child = child.nextSibling; 696 | } 697 | } 698 | 699 | /** 700 | * Adds a style element to the node containing the inert specific styles 701 | * @param {!Node} node 702 | */ 703 | function addInertStyle(node) { 704 | if (node.querySelector('style#inert-style, link#inert-style')) { 705 | return; 706 | } 707 | const style = document.createElement('style'); 708 | style.setAttribute('id', 'inert-style'); 709 | style.textContent = '\n'+ 710 | '[inert] {\n' + 711 | ' pointer-events: none;\n' + 712 | ' cursor: default;\n' + 713 | '}\n' + 714 | '\n' + 715 | '[inert], [inert] * {\n' + 716 | ' -webkit-user-select: none;\n' + 717 | ' -moz-user-select: none;\n' + 718 | ' -ms-user-select: none;\n' + 719 | ' user-select: none;\n' + 720 | '}\n'; 721 | node.appendChild(style); 722 | } 723 | 724 | if (!HTMLElement.prototype.hasOwnProperty('inert')) { 725 | /** @type {!InertManager} */ 726 | const inertManager = new InertManager(document); 727 | 728 | Object.defineProperty(HTMLElement.prototype, 'inert', { 729 | enumerable: true, 730 | /** @this {!HTMLElement} */ 731 | get: function() { 732 | return this.hasAttribute('inert'); 733 | }, 734 | /** @this {!HTMLElement} */ 735 | set: function(inert) { 736 | inertManager.setInert(this, inert); 737 | }, 738 | }); 739 | } 740 | })(); 741 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "parserOptions": { 6 | "sourceType": "script" 7 | }, 8 | "extends": "google", 9 | "rules": { 10 | "no-var": "off", 11 | "max-len": [2, { 12 | "code": 100, 13 | "tabWidth": 2, 14 | "ignoreUrls": true 15 | }] 16 | }, 17 | "plugins": [ 18 | "es5" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/aria-hidden.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | aria-hidden fixture 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 |
Click me
21 | 22 |
Click me
23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/fixtures/basic.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 |
Click me
9 |
10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/interactives.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 | 9 |
Click me
10 | 11 | Clickable Link 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 |
Some collapsed content goes here
22 | 23 |
24 | Collapsed Details 25 | Some detailed content goes here 26 |
27 | 28 |
Hello World
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/nested.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | nested fixture 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
Click me
19 |
20 | 21 |
Click me
22 |
23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/fixtures/tabindex.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | tabindex fixture 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 |
Click me
22 |
Click me
23 |
Click me
24 |
Click me
25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/specs/basic.spec.js: -------------------------------------------------------------------------------- 1 | describe('Basic', function() { 2 | before(function() { 3 | fixture.setBase('test/fixtures'); 4 | }); 5 | 6 | beforeEach(function(done) { 7 | fixture.load('basic.html'); 8 | // Because inert relies on MutationObservers, 9 | // wait till next microtask before running tests. 10 | setTimeout(function() { 11 | done(); 12 | }, 0); 13 | }); 14 | 15 | afterEach(function() { 16 | fixture.cleanup(); 17 | }); 18 | 19 | it('should have no effect on elements outside inert region', function() { 20 | var button = fixture.el.querySelector('#non-inert'); 21 | expect(isUnfocusable(button)).to.equal(false); 22 | }); 23 | 24 | it('should make implicitly focusable child not focusable', function() { 25 | var button = fixture.el.querySelector('[inert] button'); 26 | expect(isUnfocusable(button)).to.equal(true); 27 | }); 28 | 29 | it('should make explicitly focusable child not focusable', function() { 30 | var div = fixture.el.querySelector('#fake-button'); 31 | expect(div.hasAttribute('tabindex')).to.equal(false); 32 | expect(isUnfocusable(div)).to.equal(true); 33 | }); 34 | 35 | it('should remove attribute and un-inert content if set to false', function() { 36 | var inertContainer = fixture.el.querySelector('[inert]'); 37 | expect(inertContainer.hasAttribute('inert')).to.equal(true); 38 | expect(inertContainer.inert).to.equal(true); 39 | var button = inertContainer.querySelector('button'); 40 | expect(isUnfocusable(button)).to.equal(true); 41 | 42 | inertContainer.inert = false; 43 | expect(inertContainer.hasAttribute('inert')).to.equal(false); 44 | expect(inertContainer.inert).to.equal(false); 45 | expect(isUnfocusable(button)).to.equal(false); 46 | }); 47 | 48 | it('should be able to be reapplied multiple times', function() { 49 | var inertContainer = fixture.el.querySelector('[inert]'); 50 | var button = document.querySelector('[inert] button'); 51 | expect(isUnfocusable(button)).to.equal(true); 52 | 53 | inertContainer.inert = false; 54 | expect(isUnfocusable(button)).to.equal(false); 55 | 56 | inertContainer.inert = true; 57 | expect(isUnfocusable(button)).to.equal(true); 58 | 59 | inertContainer.inert = false; 60 | expect(isUnfocusable(button)).to.equal(false); 61 | 62 | inertContainer.inert = true; 63 | expect(isUnfocusable(button)).to.equal(true); 64 | }); 65 | 66 | it('should apply to dynamically added content', function(done) { 67 | var newButton = document.createElement('button'); 68 | newButton.textContent = 'Click me too'; 69 | var inertContainer = fixture.el.querySelector('[inert]'); 70 | inertContainer.appendChild(newButton); 71 | // Wait for the next microtask to allow mutation observers to react to the DOM change 72 | setTimeout(function() { 73 | expect(isUnfocusable(newButton)).to.equal(true); 74 | done(); 75 | }, 0); 76 | }); 77 | 78 | it('should be detected on dynamically added content', function(done) { 79 | var temp = document.createElement('div'); 80 | fixture.el.appendChild(temp); 81 | temp.outerHTML = '
'; 82 | var div = fixture.el.querySelector('#inert2'); 83 | // Wait for the next microtask to allow mutation observers to react to the DOM change 84 | setTimeout(function() { 85 | expect(div.inert).to.equal(true); 86 | var button = div.querySelector('button'); 87 | expect(isUnfocusable(button)).to.equal(true); 88 | done(); 89 | }, 0); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/specs/element.spec.js: -------------------------------------------------------------------------------- 1 | describe('HTMLElement.prototype', function() { 2 | it('should patch the HTMLElement prototype', function() { 3 | expect(HTMLElement.prototype.hasOwnProperty('inert')).to.be.ok; 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/specs/helpers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Quick and dirty logger to get info out of SauceLabs tests. 3 | */ 4 | var LogLevels = {INFO: 'info', ERROR: 'error', NONE: 'none'}; 5 | var LogLevel = LogLevels.NONE; // Set this to ERROR or NONE when not debugging. 6 | var LOG = {}; 7 | LOG.info = function() { 8 | if (LogLevel === LogLevels.INFO) { 9 | console.log.apply(null, arguments); // eslint-disable-line prefer-rest-params 10 | } 11 | }; 12 | 13 | /** 14 | * Check if an element is not focusable. 15 | * Note: This will be injected into the global scope by the test runner. 16 | * See the files array in karma.conf.js. 17 | * @param {HTMLElement} el 18 | * @return {Boolean} 19 | */ 20 | function isUnfocusable(el) { // eslint-disable-line no-unused-vars 21 | var oldActiveElement = document.activeElement; 22 | el.focus(); 23 | if (document.activeElement !== oldActiveElement) { 24 | LOG.info('document.activeElement !== oldActiveElement'); 25 | return false; 26 | } 27 | if (document.activeElement === el) { 28 | LOG.info('document.activeElement === el'); 29 | return false; 30 | } 31 | // Can't use tabIndex property here because Edge says a
has 32 | // a tabIndex of 0 by default, even though calling focus() on it does 33 | // not actually focus it. 34 | if (el.hasAttribute('tabindex') && el.getAttribute('tabindex') === '0') { 35 | LOG.info('el.getAttribute(tabindex) === 0'); 36 | return false; 37 | } 38 | return true; 39 | } 40 | -------------------------------------------------------------------------------- /test/specs/interactives.spec.js: -------------------------------------------------------------------------------- 1 | describe('Interactives', function() { 2 | before(function() { 3 | fixture.setBase('test/fixtures'); 4 | }); 5 | 6 | beforeEach(function(done) { 7 | fixture.load('interactives.html'); 8 | // Because inert relies on MutationObservers, 9 | // wait till next microtask before running tests. 10 | setTimeout(function() { 11 | done(); 12 | }, 0); 13 | }); 14 | 15 | afterEach(function() { 16 | fixture.cleanup(); 17 | }); 18 | 19 | it('should make button child not focusable', function() { 20 | var button = fixture.el.querySelector('[inert] button'); 21 | expect(isUnfocusable(button)).to.equal(true); 22 | }); 23 | 24 | it('should make tabindexed child not focusable', function() { 25 | var div = fixture.el.querySelector('#fake-button'); 26 | expect(div.hasAttribute('tabindex')).to.equal(false); 27 | expect(isUnfocusable(div)).to.equal(true); 28 | }); 29 | 30 | it('should make a[href] child not focusable', function() { 31 | var anchor = fixture.el.querySelector('[inert] a[href]'); 32 | expect(isUnfocusable(anchor)).to.equal(true); 33 | }); 34 | 35 | it('should make input child not focusable', function() { 36 | var input = fixture.el.querySelector('[inert] input'); 37 | expect(isUnfocusable(input)).to.equal(true); 38 | }); 39 | 40 | it('should make select child not focusable', function() { 41 | var select = fixture.el.querySelector('[inert] select'); 42 | expect(isUnfocusable(select)).to.equal(true); 43 | }); 44 | 45 | it('should make textarea child not focusable', function() { 46 | var textarea = fixture.el.querySelector('[inert] textarea'); 47 | expect(isUnfocusable(textarea)).to.equal(true); 48 | }); 49 | 50 | it('should make details child not focusable', function() { 51 | var details = fixture.el.querySelector('#details-no-summary'); 52 | 53 | expect(isUnfocusable(details)).to.equal(true); 54 | }); 55 | 56 | it('should make details with summary child not focusable', function() { 57 | var details = fixture.el.querySelector('#details-with-summary'); 58 | var summary = details.querySelector('summary'); 59 | 60 | expect(isUnfocusable(details)).to.equal(true); 61 | expect(isUnfocusable(summary)).to.equal(true); 62 | }); 63 | 64 | it('should make contenteditable child not focusable', function() { 65 | var editor = fixture.el.querySelector('#editable'); 66 | expect(isUnfocusable(editor)).to.equal(true); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/specs/nested.spec.js: -------------------------------------------------------------------------------- 1 | describe('Nested inert regions', function() { 2 | before(function() { 3 | fixture.setBase('test/fixtures'); 4 | }); 5 | 6 | beforeEach(function(done) { 7 | fixture.load('nested.html'); 8 | // Because inert relies on MutationObservers, 9 | // wait till next microtask before running tests. 10 | setTimeout(function() { 11 | done(); 12 | }, 0); 13 | }); 14 | 15 | afterEach(function() { 16 | fixture.cleanup(); 17 | }); 18 | 19 | it('should apply regardless of how many deep the nesting is', function() { 20 | var outerButton = fixture.el.querySelector('#outer-button'); 21 | expect(isUnfocusable(outerButton)).to.equal(true); 22 | var outerFakeButton = fixture.el.querySelector('#outer-fake-button'); 23 | expect(isUnfocusable(outerFakeButton)).to.equal(true); 24 | 25 | var innerButton = fixture.el.querySelector('#inner-button'); 26 | expect(isUnfocusable(innerButton)).to.equal(true); 27 | var innerFakeButton = fixture.el.querySelector('#inner-fake-button'); 28 | expect(isUnfocusable(innerFakeButton)).to.equal(true); 29 | }); 30 | 31 | it('should still apply if inner inert is removed', function() { 32 | fixture.el.querySelector('#inner').inert = false; 33 | 34 | var outerButton = fixture.el.querySelector('#outer-button'); 35 | expect(isUnfocusable(outerButton)).to.equal(true); 36 | var outerFakeButton = fixture.el.querySelector('#outer-fake-button'); 37 | expect(isUnfocusable(outerFakeButton)).to.equal(true); 38 | 39 | var innerButton = fixture.el.querySelector('#inner-button'); 40 | expect(isUnfocusable(innerButton)).to.equal(true); 41 | var innerFakeButton = fixture.el.querySelector('#inner-fake-button'); 42 | expect(isUnfocusable(innerFakeButton)).to.equal(true); 43 | }); 44 | 45 | it('should still apply to inner content if outer inert is removed', function() { 46 | fixture.el.querySelector('#outer').inert = false; 47 | 48 | var outerButton = fixture.el.querySelector('#outer-button'); 49 | expect(isUnfocusable(outerButton)).to.equal(false); 50 | var outerFakeButton = fixture.el.querySelector('#outer-fake-button'); 51 | expect(isUnfocusable(outerFakeButton)).to.equal(false); 52 | 53 | var innerButton = fixture.el.querySelector('#inner-button'); 54 | expect(isUnfocusable(innerButton)).to.equal(true); 55 | var innerFakeButton = fixture.el.querySelector('#inner-fake-button'); 56 | expect(isUnfocusable(innerFakeButton)).to.equal(true); 57 | }); 58 | 59 | it('should be detected on dynamically added content within an inert root', function(done) { 60 | var temp = document.createElement('div'); 61 | var outerContainer = fixture.el.querySelector('#outer'); 62 | outerContainer.appendChild(temp); 63 | expect(temp.parentElement).to.eql(outerContainer); 64 | temp.outerHTML = '
'; 65 | var div = outerContainer.querySelector('#inner2'); 66 | // Wait for the next microtask to allow mutation observers to react to the DOM change 67 | setTimeout(function() { 68 | expect(div.inert).to.equal(true); 69 | var button = div.querySelector('button'); 70 | expect(isUnfocusable(button)).to.equal(true); 71 | 72 | // un-inerting outer container doesn't mess up the new inner container 73 | outerContainer.inert = false; 74 | expect(div.inert).to.equal(true); 75 | expect(isUnfocusable(button)).to.equal(true); 76 | done(); 77 | }, 0); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/specs/reapply-aria-hidden.spec.js: -------------------------------------------------------------------------------- 1 | describe('Reapply existing aria-hidden', function() { 2 | before(function() { 3 | fixture.setBase('test/fixtures'); 4 | }); 5 | 6 | beforeEach(function(done) { 7 | fixture.load('aria-hidden.html'); 8 | // Because inert relies on MutationObservers, 9 | // wait till next microtask before running tests. 10 | setTimeout(function() { 11 | done(); 12 | }, 0); 13 | }); 14 | 15 | afterEach(function() { 16 | fixture.cleanup(); 17 | }); 18 | 19 | it('should reinstate pre-existing aria-hidden on setting inert=false', function() { 20 | var container = fixture.el.querySelector('#container'); 21 | var ariaHiddens = new Map(); 22 | Array.from(container.children).forEach(function(el) { 23 | if (el.hasAttribute('aria-hidden')) { 24 | ariaHiddens.set(el, el.getAttribute('aria-hidden')); 25 | } 26 | 27 | el.inert = true; 28 | el.inert = false; 29 | }); 30 | 31 | Array.from(container.children).forEach(function(el) { 32 | var ariaHidden = ariaHiddens.get(el); 33 | if (ariaHidden) { 34 | expect(el.hasAttribute('aria-hidden')).to.equal(true); 35 | expect(el.getAttribute('aria-hidden')).to.equal(ariaHiddens.get(el)); 36 | } else { 37 | expect(el.hasAttribute('aria-hidden')).to.equal(false); 38 | } 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/specs/reapply-tabindex.spec.js: -------------------------------------------------------------------------------- 1 | describe('Reapply existing tabindex', function() { 2 | before(function() { 3 | fixture.setBase('test/fixtures'); 4 | }); 5 | 6 | beforeEach(function(done) { 7 | fixture.load('tabindex.html'); 8 | // Because inert relies on MutationObservers, 9 | // wait till next microtask before running tests. 10 | setTimeout(function() { 11 | done(); 12 | }, 0); 13 | }); 14 | 15 | afterEach(function() { 16 | fixture.cleanup(); 17 | }); 18 | 19 | it('should reinstate pre-existing tabindex on setting inert=false', function() { 20 | var container = fixture.el.querySelector('#container'); 21 | var tabindexes = new Map(); 22 | var focusableElements = new Set(); 23 | Array.from(container.children).forEach(function(el) { 24 | if (el.hasAttribute('tabindex')) { 25 | tabindexes.set(el, el.getAttribute('tabindex')); 26 | } 27 | if (!isUnfocusable(el)) { 28 | focusableElements.add(el); 29 | } 30 | }); 31 | 32 | container.inert = true; 33 | focusableElements.forEach(function(focusableEl) { 34 | expect(isUnfocusable(focusableEl)).to.equal(true); 35 | }); 36 | 37 | container.inert = false; 38 | focusableElements.forEach(function(focusableEl) { 39 | expect(isUnfocusable(focusableEl)).to.equal(false); 40 | }); 41 | 42 | Array.from(container.children).forEach(function(el) { 43 | var tabindex = tabindexes.get(el); 44 | if (tabindex) { 45 | expect(el.hasAttribute('tabindex')).to.equal(true); 46 | expect(el.getAttribute('tabindex')).to.equal(tabindexes.get(el)); 47 | } else { 48 | expect(el.hasAttribute('tabindex')).to.equal(false); 49 | } 50 | }); 51 | }); 52 | 53 | it('should set tabindex correctly for elements added later in the inert root', function(done) { 54 | var divRoot = document.createElement('div'); 55 | document.body.appendChild(divRoot); 56 | divRoot.inert = true; 57 | var button = document.createElement('button'); 58 | divRoot.appendChild(button); 59 | 60 | // adding a timeout in order to enter the next event loop, due to the mutationObserver events 61 | setTimeout(function() { 62 | expect(button.tabIndex).to.equal(-1); 63 | 64 | divRoot.inert = false; 65 | expect(button.tabIndex).to.equal(0); 66 | done(); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/specs/shadow-dom-v0.spec.js: -------------------------------------------------------------------------------- 1 | describe('ShadowDOM v0', function() { 2 | if (!Element.prototype.createShadowRoot) { 3 | // ShadowDOM v0 is not supported by the browser. 4 | return; 5 | } 6 | 7 | before(function() { 8 | fixture.setBase('test/fixtures'); 9 | }); 10 | 11 | beforeEach(function(done) { 12 | fixture.load('basic.html'); 13 | // Because inert relies on MutationObservers, 14 | // wait till next microtask before running tests. 15 | setTimeout(function() { 16 | done(); 17 | }, 0); 18 | }); 19 | 20 | afterEach(function() { 21 | fixture.cleanup(); 22 | }); 23 | 24 | var host; 25 | 26 | beforeEach(function() { 27 | fixture.el.inert = false; 28 | host = document.createElement('div'); 29 | fixture.el.appendChild(host); 30 | host.createShadowRoot(); 31 | }); 32 | 33 | it('should apply inside shadow trees', function() { 34 | var shadowButton = document.createElement('button'); 35 | shadowButton.textContent = 'Shadow button'; 36 | host.shadowRoot.appendChild(shadowButton); 37 | shadowButton.focus(); 38 | fixture.el.inert = true; 39 | expect(isUnfocusable(shadowButton)).to.equal(true); 40 | }); 41 | 42 | it('should apply inert styles inside shadow trees', function() { 43 | var shadowButton = document.createElement('button'); 44 | shadowButton.textContent = 'Shadow button'; 45 | host.shadowRoot.appendChild(shadowButton); 46 | shadowButton.focus(); 47 | shadowButton.inert = true; 48 | expect(getComputedStyle(shadowButton).pointerEvents).to.equal('none'); 49 | }); 50 | 51 | it('should apply inside shadow trees distributed content', function() { 52 | host.shadowRoot.appendChild(document.createElement('content')); 53 | var distributedButton = document.createElement('button'); 54 | distributedButton.textContent = 'Distributed button'; 55 | host.appendChild(distributedButton); 56 | distributedButton.focus(); 57 | fixture.el.inert = true; 58 | expect(isUnfocusable(distributedButton)).to.equal(true); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/specs/shadow-dom-v1.spec.js: -------------------------------------------------------------------------------- 1 | describe('ShadowDOM v1', function() { 2 | if (!Element.prototype.attachShadow) { 3 | // ShadowDOM v1 is not supported by the browser. 4 | return; 5 | } 6 | 7 | before(function() { 8 | fixture.setBase('test/fixtures'); 9 | }); 10 | 11 | beforeEach(function(done) { 12 | fixture.load('basic.html'); 13 | // Because inert relies on MutationObservers, 14 | // wait till next microtask before running tests. 15 | setTimeout(function() { 16 | done(); 17 | }, 0); 18 | }); 19 | 20 | afterEach(function() { 21 | fixture.cleanup(); 22 | }); 23 | 24 | var host; 25 | 26 | beforeEach(function() { 27 | fixture.el.inert = false; 28 | host = document.createElement('div'); 29 | fixture.el.appendChild(host); 30 | host.attachShadow({ 31 | mode: 'open', 32 | }); 33 | }); 34 | 35 | it('should apply inside shadow trees', function() { 36 | var shadowButton = document.createElement('button'); 37 | shadowButton.textContent = 'Shadow button'; 38 | host.shadowRoot.appendChild(shadowButton); 39 | shadowButton.focus(); 40 | fixture.el.inert = true; 41 | expect(isUnfocusable(shadowButton)).to.equal(true); 42 | }); 43 | 44 | it('should apply inert styles inside shadow trees', function(done) { 45 | var shadowButton = document.createElement('button'); 46 | shadowButton.textContent = 'Shadow button'; 47 | host.shadowRoot.appendChild(shadowButton); 48 | shadowButton.focus(); 49 | shadowButton.inert = true; 50 | // Wait for the next microtask to allow mutation observers to react to the DOM change 51 | setTimeout(function() { 52 | expect(getComputedStyle(shadowButton).pointerEvents).to.equal('none'); 53 | done(); 54 | }, 0); 55 | }); 56 | 57 | it('should apply inert styles inside shadow trees that aren\'t focused', function(done) { 58 | var shadowButton = document.createElement('button'); 59 | shadowButton.textContent = 'Shadow button'; 60 | host.shadowRoot.appendChild(shadowButton); 61 | shadowButton.inert = true; 62 | // Wait for the next microtask to allow mutation observers to react to the DOM change 63 | setTimeout(function() { 64 | expect(getComputedStyle(shadowButton).pointerEvents).to.equal('none'); 65 | done(); 66 | }, 0); 67 | }); 68 | 69 | it('should apply inside shadow trees distributed content', function() { 70 | host.shadowRoot.appendChild(document.createElement('slot')); 71 | var distributedButton = document.createElement('button'); 72 | distributedButton.textContent = 'Distributed button'; 73 | host.appendChild(distributedButton); 74 | distributedButton.focus(); 75 | fixture.el.inert = true; 76 | expect(isUnfocusable(distributedButton)).to.equal(true); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /w3c.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": [80485] 3 | , "contacts": ["marcoscaceres"] 4 | , "repo-type": "tool" 5 | } 6 | --------------------------------------------------------------------------------