5 |
--------------------------------------------------------------------------------
/examples/WebComponents/focus-example.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | font-family: sans-serif;
4 | padding: 10px 10px;
5 | width: 100%;
6 | }
7 |
8 | label {
9 | display: block;
10 | font-size: 14px;
11 | }
12 |
13 | input {
14 | display: block;
15 | width: 93%;
16 | font-size: 18px;
17 | font-weight: bold;
18 | padding: 3px 5px;
19 | border-radius: 5px 0px 5px 0px;
20 | border: 2px solid black;
21 | outline: none;
22 | }
23 |
24 | input.highlight {
25 | background: #FFFFAA;
26 | }
27 |
28 | input:focus {
29 | background: #AAFFAA;
30 | }
31 |
--------------------------------------------------------------------------------
/src/.eslintrc.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = {
4 | "extends": "eslint:recommended",
5 | parserOptions: {
6 | ecmaVersion: 6
7 | },
8 | env: {
9 | es6: true,
10 | browser: true
11 | },
12 | rules: {
13 | "no-self-assign": [
14 | "off"
15 | ],
16 | indent: [
17 | "error",
18 | "tab",
19 | { "SwitchCase": 1 }
20 | ],
21 | semi: [
22 | "error",
23 | "always"
24 | ],
25 | "require-await": [
26 | "error"
27 | ],
28 | "no-constant-condition": [
29 | "off"
30 | ]
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/examples/WebComponents/focus-example.js:
--------------------------------------------------------------------------------
1 | // (c) 2018-present, The Awesome Engineering Company, https://awesomeneg.com
2 |
3 | import {ZephComponents,html,css,attribute,bind,bindAt,onAdd} from "./zeph.min.js";
4 |
5 | ZephComponents.define("focus-example",()=>{
6 | html("./focus-example.html");
7 | css("./focus-example.css");
8 |
9 | attribute("label","");
10 |
11 | bind("@label","label","$");
12 | bind("$","input","@value");
13 | bind("@value",".",".value");
14 | bindAt("input",".value",".","@value");
15 |
16 | onAdd((element,children)=>{
17 | let target = children.querySelector("input");
18 | focusManager.proxy(element,target);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/jsdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "conf": {
4 | "plugins": [],
5 | "recurseDepth": 10,
6 | "source": {
7 | "includePattern": ".+\\.js(doc|x)?$",
8 | "excludePattern": "(^|\\/|\\\\)_"
9 | },
10 | "sourceType": "module",
11 | "tags": {
12 | "allowUnknownTags": true,
13 | "dictionaries": [
14 | "jsdoc",
15 | "closure"
16 | ]
17 | },
18 | "templates": {
19 | "monospaceLinks": false,
20 | "cleverLinks": false,
21 | "default": {
22 | "outputSourceFiles": true
23 | }
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Awesome Engineering
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/GET_INVOLVED.md:
--------------------------------------------------------------------------------
1 | # Focus Traversal API
2 |
3 | We are always looking for people to get involved with the Focus Traversal API and proposal. Please consider helping us out in one of the following ways...
4 |
5 | #### Spread the word!
6 |
7 | [Tweet and Retweet](https://ctt.ac/H1IoF) about our proposal.
8 |
9 | Make sure to use the hashtag [#FocusTraversalAPI](https://twitter.com/search?q=%23FocusTraversalAPI&src=typd)!
10 |
11 | #### Comment on the Proposal.
12 |
13 | You can comment in either the [WICG W3C Proposal Submission discourse](https://discourse.wicg.io/t/proposal-focus-traversal-api/3427), or the [WHATWG Github Issue](https://github.com/whatwg/html/issues/4784). Even a thumbs up helps!
14 |
15 | Also, google has an incubator system for additions to chromium to which a ticket was submitted: https://bugs.chromium.org/p/chromium/issues/detail?id=997446
16 |
17 | #### Download the Polyfill and use it
18 |
19 | The more people [use the polyfill](./README.md#focus-traversal-api), the more issues we can find, and the more solid our proposal becomes.
20 |
21 | You can get started using it today with npm!
22 |
23 | ```
24 | npm install focus-traversal-api-polyfill
25 | ```
26 |
27 | #### Participate
28 |
29 | We need people to actively help us out, write test, make the proposal better. Take a look at the [issues](https://github.com/awesomeeng/FocusTraversalAPI/issues) for an idea of where to get started!
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "focus-traversal-api-polyfill",
3 | "version": "2.1.1",
4 | "author": "the awesome engineering company",
5 | "license": "MIT",
6 | "contributors": [
7 | "Glen R. Goodwin"
8 | ],
9 | "main": "./polyfill/web-focus-api-polyfill.js",
10 | "dependencies": {},
11 | "devDependencies": {
12 | "jsdom": "^15.2.0",
13 | "eslint": "5.16.0",
14 | "jsdoc-to-markdown": "^5.0.2",
15 | "minify": "4.1.1",
16 | "mocha": "^6.2.2"
17 | },
18 | "optionalDependencies": {},
19 | "scripts": {
20 | "lint": "eslint src",
21 | "docs": "jsdoc2md src/** --separators > docs/API.md",
22 | "test": "mocha test",
23 | "tests": "mocha test",
24 | "minify": "cat src/DOMTraversal.js src/focus-traversal-api-polyfill.js > focus-traversal-api-polyfill.full.js && minify focus-traversal-api-polyfill.full.js > focus-traversal-api-polyfill.min.js",
25 | "dev": "nodemon -w src --exec \"npm run minify && node /mnt/c/DEV/Awesome/Code/zephjs/src/cli/CLI.js serve\""
26 | },
27 | "description": "Focus Traversal API polyfill.",
28 | "directories": {
29 | "doc": "docs",
30 | "test": "test"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/awesomeeng/FocusTraversalAPI.git"
35 | },
36 | "keywords": [
37 | "focus",
38 | "focus traversal",
39 | "focus api",
40 | "focus traversal api",
41 | "traversal api",
42 | "browser focus",
43 | "browser focus api",
44 | "browser focus traversal api",
45 | "web focus",
46 | "web focus api",
47 | "web focus traversal api"
48 | ],
49 | "bugs": {
50 | "url": "https://github.com/awesomeeng/FocusTraversalAPI/issues"
51 | },
52 | "homepage": "https://github.com/awesomeeng/FocusTraversalAPI#readme"
53 | }
54 |
--------------------------------------------------------------------------------
/RELEASE_NOTES.md:
--------------------------------------------------------------------------------
1 | # AwesomeComponenets Release Notes
2 |
3 | #### **Version 2.1.1**
4 |
5 | - Fixes a bunch of error introduced accidentally with 2.1.0. Sorry.
6 |
7 | #### **Version 2.1.0**
8 |
9 | - Removes tabbable dependency.
10 | - trap() and untrap() can now take more than one element argument.
11 |
12 | #### **Version 2.0.4**
13 |
14 | - Fixes bug in `untrap()` that would cause an exception if called with no traps set.
15 |
16 |
17 | #### **Version 2.0.2**
18 |
19 | - Adds the `parent()` and `child()` methods.
20 |
21 | #### **Version 2.0.1**
22 |
23 | - Moved jsdom to devDependencies.
24 |
25 | #### **Version 2.0.0**
26 |
27 | - Adds `order()` method to programatically set the focus order of one or elements.
28 | - Adds `trap()` and `untrap()` to control trapping focus into a given container.
29 | - Adds `proxy()` and `unproxy()` to easily forward focus from one element to another.
30 | - Renamed `orderedElements()` to `list()`.
31 | - Fixes a bug in traversal that was preventing shadow elements from traversing backwards correctly.
32 | - DOMTraversal code broken out into a separate file and refactored. (This will move to a future project at some point.)
33 | - Lots of bug fixes around focus and shadowroot behaviors.
34 | - Revised next() and previous() behavior.
35 | - Removed dependency on Tabable.
36 |
37 | #### **Version 1.2.0**
38 |
39 | - Adds focusOption argument to focusManager.focus(), focusManager.forward() and focusManager.backward to align with element.focus.
40 |
41 | - Adds container constraint to focusManager.orderedElements().
42 |
43 | - Adds focusManager.first() and focusManager.last() for finding the first focusable or last focusable in a given container.
44 |
45 | #### **Version 1.1.0**
46 |
47 | - Adds support for focus traversal into and out of a Shadow DOM element.
48 |
49 | #### **Version 1.0.1**
50 |
51 | - Adds Minified version.
52 |
53 | #### **Version 1.0.0**
54 |
55 | - Initial release.
56 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies within all project spaces, and it also applies when
49 | an individual is representing the project or its community in public spaces.
50 | Examples of representing a project or community include using an official
51 | project e-mail address, posting via an official social media account, or acting
52 | as an appointed representative at an online or offline event. Representation of
53 | a project may be further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at contrib@awesomeeng.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
78 |
--------------------------------------------------------------------------------
/PROPOSAL.md:
--------------------------------------------------------------------------------
1 | # Focus Traversal API
2 |
3 | The current system for programmatically manipulating focus within a web page leaves a lot to be desired. A single element can request the focus with the `focus()` method but that is the extent of the programatic focus navigation options. Advancing the focus to the next focusable element or the previous focusable element involves a complex dance of DOM traversal and guess work as to what elements can recieve focus or not. Most solutions to manipulating focus are complicated; the very simple need to manipulate focus in a meaningful and accessalbe manner is significantly lacking.
4 |
5 | ## Proposed
6 |
7 | The creation of a unified Focus Traversal API that makes understanding, manipulating, and traversing the focus simple. This must include the ability to assign the focus, move the focus forward and backward, and understand both the next and previous focusable elements.
8 |
9 | A rough example of this API might be the following:
10 |
11 | `window.focusManager.currentlyFocused` - Contains the element currently holding the focus, if any.
12 |
13 | `window.focusManager.previouslyFocused` - Contains the element that held the focus prior to the current focus, if any.
14 |
15 | `window.focusManager.history` - An array of the last n historical focus holders.
16 |
17 | `window.focusManager.isFocusable(element)` - Returns true if the given element can receive the focus.
18 |
19 | `window.focusManager.hasFocus(element)` - Returns true if the given element currently has the focus. Functionally equivelent to `window.focusManager.currentlyFocused===element`.
20 |
21 | `window.focusManager.focus(element)` - Focus on the given element. Functionally the same as `element.focus()`. Returns void.
22 |
23 | `window.focusManager.forward()` - Move the focus to the next focusable element. Returns void.
24 |
25 | `window.focusManager.backward()` - Move the focus to the previous focusable element. Returns void.
26 |
27 | `window.focusManager.next(element)` - Returns the element that would revieve the focus if `window.focusManager.forward()` was called when the given element has the focus. If no element is given, the currently focused element is used.
28 |
29 | `window.focusManager.previous(element)` - Returns the element that would revieve the focus if `window.focusManager.backward()` was called when the given element has the focus. If no element is given, the currently focused element is used.
30 |
31 | `window.focusManager.list()` - Returns an array of all focusable elements in the order that focus traversal would occur.
32 |
33 | `window.focusManager.order(element,element,element,etc)` - Programatically set the traversal order of one or more elements. Given an array of elements (or multiple arguments) order them in the order they are given.
34 |
35 | `window.focusManager.trap()` - Trap focus within a given element, such that any focus event outside of the element's descendants will refocus the last focused element within the element's descendants. Traps calls stack, such that the latest trap always wins, but removing a trap will set th enext prior trap running.
36 |
37 | `window.focusManager.untrap()` - Removes a trap.
38 |
39 | `window.focusManager.proxy(source,target)` - When the given source element receives the focus, forward that focus immediately to the target element.
40 |
41 | `window.focusManager.unproxy(source,target)` - Removes a proxy.
42 |
43 | ## Illustrative User Issues
44 |
45 | - https://stackoverflow.com/questions/7208161/focus-next-element-in-tab-index
46 | - https://stackoverflow.com/questions/3639908/retrieving-previously-focused-element
47 |
48 | ## Example Polyfill
49 |
50 | An example implementation of the above as well as this document can be found at https://github.com/awesomeeng/FocusTraversalAPI.
51 |
52 | ## Similar Works
53 |
54 | https://github.com/davidtheclark/tabbable - A very popular library for getting a list of all the elements in a element that can recieve the focus.
55 |
--------------------------------------------------------------------------------
/examples/WebComponents/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | WebComponents - Examples - FocusTraversalAPI
7 |
8 |
9 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | Louise Belcher
65 |
66 | Brian
67 | Archer Sterling
68 |
90 |
152 |
153 |
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Focus Traversal API
2 |
3 | The Focus Traversal API is a proposed change to the W3C/wHATWG HTML Specification for making working with Browser UI Focus more useful and more powerful. The proposal has been brought to the [W3C WICG](https://discourse.wicg.io/t/proposal-focus-traversal-api/3427) and the [WHATWG](https://github.com/whatwg/html/issues/4784) for discussion and hopefully implementation and is currently in the discussion phase within both communities. If you would like more details about the proposal, an EXPLAINER is provided at: [EXPLAINER](./EXPLAINER.md)
4 |
5 | Additionally, a polyfill for the proposed additions can be found here to allow the community to try and demonstrate the usefulness of Focus Traversal and we encourage everyone to give it a try.
6 |
7 | Also, we are always looking for people to get involved with the Focus Traversal API and proposal. Please consider helping us by spreading the word, commenting on the proposal, or participating in this repo!
8 |
9 | Check out our [GETTING INVOLVED page](./GET_INVOLVED.md) for more details on how you can help!
10 |
11 | ## Focus Traversal API Features
12 |
13 | - Implements proposed Focus Traversal API standard.
14 | - Programatically move the focus forward or backwards.
15 | - Identify the next and previous items to receive focus.
16 | - Supports handling focus in Shadow DOM hidden components.
17 | - History of the last n focus holders.
18 | - Works on all modern browsers including Internet Explorer 9 or later.
19 |
20 | ## Usage
21 |
22 | Install from npm...
23 |
24 | ```
25 | npm install focus-traversal-api-polyfill
26 | ```
27 |
28 | Copy the polyfill into your own project...
29 |
30 | ```shell
31 | cp focus-traversal-api-polyfill/focus-traversal-api-polyfill.min.js .
32 | ```
33 |
34 | Include in your index.html...
35 |
36 | ```
37 |
38 | ```
39 |
40 | You may now use the focusManager as described in the section below.
41 |
42 | ## API Documentation
43 |
44 | > `window.focusManager.currentlyFocused` - Contains the element currently holding the focus, if any.
45 |
46 | > `window.focusManager.previouslyFocused` - Contains the element that held the focus prior to the current focus, if any.
47 |
48 | > `window.focusManager.history` - An array of the last n historical focus holders.
49 |
50 | > `window.focusManager.historyLimit` - A number indicating how may focus history events should be retained. Defaults to 50.
51 |
52 | > `window.focusManager.isFocusable(element)` - Returns true if the given element can receive focus.
53 |
54 | > `window.focusManager.hasFocusable(element)` - Returns true if the given element currently has the focus.
55 |
56 | > `window.focusManager.focus(element,focusOption)` - Focus on the given element. Functionally the same as `element.focus()`. Returns void.
57 |
58 | > `window.focusManager.forward(focusOption)` - Move the focus to the next focusable element. Returns void.
59 |
60 | > `window.focusManager.backward(focusOption)` - Move the focus to the previous focusable element. Returns void.
61 |
62 | > `window.focusManager.next(element)` - Returns the element that would receive the focus if `window.focusManager.forward()` was called when the given element has the focus. If no element is given, the currently focused element is used.
63 |
64 | > `window.focusManager.previous(element)` - Returns the element that would receive the focus if `window.focusManager.backward()` was called when the given element has the focus. If no element is given, the currently focused element is used.
65 |
66 | > `window.focusManager.list(container)` - Returns an array of all focusable elements in the order that focus traversal would occur. If container is provided and a valid HTMLElement, this would limit the results to only the children of the given container. If no container is provided, the current document is used as the container.
67 |
68 | > `window.focusManager.first(container)` - Returns the first element that would receive focus for the given container, or the current document if no container is provided.
69 |
70 | > `window.focusManager.last(container)` - Returns the first element that would receive focus for the given container, or the current document if no container is provided.
71 |
72 | > `window.focusManager.parent(element)` - returns the first focusable ancestor of the given element.
73 |
74 | > `window.focusManager.child(element)` - returns the first focusable descendant of the given element. The same as `first(element)`.
75 |
76 | > `window.focusManager.order(element,element,element,etc)` - Programatically set the traversal order of one or more elements. Given an array of elements (or multiple arguments) order them in the order they are given.
77 |
78 | > `window.focusManager.trap()` - Trap focus within a given element(s), such that any focus event outside and of the given element(s) descendants will refocus the last focused element within the element's descendants. Traps calls stack, such that the latest trap always wins, but removing a trap will set th enext prior trap running.
79 |
80 | > `window.focusManager.untrap()` - Removes a trap.
81 |
82 | > `window.focusManager.proxy(source,target)` - When the given source element receives the focus, forward that focus immediately to the target element.
83 |
84 | > `window.focusManager.unproxy(source,target)` - Removes a proxy.
85 |
86 | ## Issues
87 |
88 | If you would like to comment on the proposal, please [open an issue](https://github.com/awesomeeng/FocusTraversalAPI/issues).
89 |
90 | ## License
91 |
92 | The proposal and the polyfill library are available under the MIT License.
93 |
--------------------------------------------------------------------------------
/src/DOMTraversal.js:
--------------------------------------------------------------------------------
1 | (function (){
2 | var first = function first(element,delveShadow) {
3 | if (!element) throw new Error("Invalid element.");
4 | return element.firstElementChild || delveShadow && element.shadowRoot && element.shadowRoot.firstElementChild || null;
5 | };
6 | var down = first;
7 |
8 | var last = function last(element,delveShadow) {
9 | if (!element) throw new Error("Invalid element.");
10 | return delveShadow && element.shadowRoot && element.shadowRoot.lastElementChild || element.lastElementChild || null;
11 | };
12 | var downLast = last;
13 |
14 | var up = function up(element,delveShadow) {
15 | if (!element) throw new Error("Invalid element.");
16 | return element.parentElement || delveShadow && element.parentNode instanceof DocumentFragment && element.parentNode.host || null;
17 | };
18 |
19 | var top = function top(element,delveShadow) {
20 | if (!element) throw new Error("Invalid element.");
21 | while(true) {
22 | var e = up(element,delveShadow);
23 | if (!e) break;
24 | element = e;
25 | }
26 | return element;
27 | };
28 |
29 | var bottom = function bottom(element,delveShadow) {
30 | if (!element) throw new Error("Invalid element.");
31 | while (true) {
32 | var e = down(element,delveShadow);
33 | if (!e) break;
34 | element = e;
35 | }
36 | return element;
37 | };
38 |
39 | var bottomLast = function bottomLast(element,delveShadow) {
40 | if (!element) throw new Error("Invalid element.");
41 | while (true) {
42 | var e = downLast(element,delveShadow);
43 | if (!e) break;
44 | element = e;
45 | }
46 | return element;
47 | };
48 |
49 | var next = function next(element,delveShadow) {
50 | if (!element) throw new Error("Invalid element.");
51 |
52 | var c = element.firstElementChild || null;
53 | if (c) return c;
54 |
55 | var sc = delveShadow && element.shadowRoot && element.shadowRoot.firstElementChild || null;
56 | if (sc) return element.shadowRoot.firstElementChild;
57 |
58 | var sib = element.nextElementSibling || null;
59 | if (sib) return sib;
60 |
61 | var e = element;
62 | while (true) {
63 | e = up(e,delveShadow);
64 | if (!e || e===element) return null;
65 |
66 | sib = e.nextElementSibling;
67 | if (sib) return sib;
68 | }
69 | };
70 |
71 | var prev = function prev(element,delveShadow) {
72 | if (!element) throw new Error("Invalid element.");
73 |
74 | var sib = element.previousElementSibling;
75 | if (sib) return bottomLast(sib,delveShadow);
76 |
77 | var sc = delveShadow && !element.parentElement && element.parentNode && element.parentNode instanceof DocumentFragment && element.parentNode.host || null;
78 | if (sc && sc.lastElementChild) return bottom(sc.lastElementChild,delveShadow);
79 |
80 | var u = up(element,delveShadow);
81 | return u;
82 | };
83 |
84 | var descendants = function descendants(element,delveShadow) {
85 | if (!element) throw new Error("Invalid element.");
86 |
87 | var desc = [];
88 |
89 | var children = Array.prototype.slice.call(element.children);
90 | children.forEach(function(c){
91 | desc = desc.concat(c,descendants(c,delveShadow));
92 | });
93 |
94 | if (delveShadow && element.shadowRoot) {
95 | var shadows = Array.prototype.slice.call(element.shadowRoot.children);
96 | shadows.forEach(function(c){
97 | desc = desc.concat(c,descendants(c,delveShadow));
98 | });
99 | }
100 |
101 | return desc;
102 | };
103 |
104 | var ancestors = function ancestors(element,delveShadow) {
105 | if (!element) throw new Error("Invalid element.");
106 |
107 | var ansc = [];
108 | var e = element;
109 | while (e) {
110 | e = up(e,delveShadow);
111 | if (!e) break;
112 | ansc.push(e);
113 | }
114 | return ansc;
115 | };
116 |
117 | var forwardList = function forwardList(element,delveShadow) {
118 | if (!element) throw new Error("Invalid element.");
119 | var elements = [];
120 |
121 | var e = element;
122 | while (true) {
123 | if (!e) break;
124 | e = next(e,delveShadow);
125 | if (!e || e===element || !contains(element,e,delveShadow)) break;
126 | elements.push(e);
127 | }
128 | return elements;
129 | };
130 |
131 | var backwardList = function backwardList(element,delveShadow) {
132 | if (!element) throw new Error("Invalid element.");
133 | return forwardList(element,delveShadow).reverse();
134 | };
135 |
136 | var contains = function contains(container,element,delveShadow) {
137 | if (!container && !(container instanceof HTMLElement)) return false;
138 | if (!element && !(element instanceof HTMLElement)) return false;
139 |
140 | if (!delveShadow) return container.contains(element);
141 |
142 | var ancs = ancestors(element,true);
143 | return ancs.indexOf(container)>-1;
144 | };
145 |
146 | var dom = {
147 | first: first,
148 | last: last,
149 | up: up,
150 | down: first,
151 | downLast: last,
152 | next: next,
153 | forward: next,
154 | prev: prev,
155 | previous: prev,
156 | backward: prev,
157 | top: top,
158 | bottom: bottom,
159 | bottomLast: bottomLast,
160 | descendants: descendants,
161 | ancestors: ancestors,
162 | forwardList: forwardList,
163 | backwardList: backwardList,
164 | contains: contains
165 | };
166 |
167 | // can be used in node, mostly for testing
168 | // eslint-disable-next-line no-undef
169 | if (typeof module!=="undefined" && module && module.exports) {
170 | // eslint-disable-next-line no-undef
171 | module.exports = dom;
172 | }
173 |
174 | // creates window.DOMTraversal for usage in browsers.
175 | if (typeof window!=="undefined" && window) {
176 | window.DOMTraversal = window.DOMTraversal || dom;
177 | }
178 | })();
179 |
--------------------------------------------------------------------------------
/focus-traversal-api-polyfill.min.js:
--------------------------------------------------------------------------------
1 | !function(){var n=function(n,e){if(!n)throw new Error("Invalid element.");return n.firstElementChild||e&&n.shadowRoot&&n.shadowRoot.firstElementChild||null},e=n,t=function(n,e){if(!n)throw new Error("Invalid element.");return e&&n.shadowRoot&&n.shadowRoot.lastElementChild||n.lastElementChild||null},r=t,o=function(n,e){if(!n)throw new Error("Invalid element.");return n.parentElement||e&&n.parentNode instanceof DocumentFragment&&n.parentNode.host||null},i=function(n,t){if(!n)throw new Error("Invalid element.");for(;;){var r=e(n,t);if(!r)break;n=r}return n},u=function(n,e){if(!n)throw new Error("Invalid element.");for(;;){var t=r(n,e);if(!t)break;n=t}return n},a=function(n,e){if(!n)throw new Error("Invalid element.");var t=n.firstElementChild||null;if(t)return t;if(e&&n.shadowRoot&&n.shadowRoot.firstElementChild||null)return n.shadowRoot.firstElementChild;var r=n.nextElementSibling||null;if(r)return r;for(var i=n;;){if(!(i=o(i,e))||i===n)return null;if(r=i.nextElementSibling)return r}},l=function(n,e){if(!n)throw new Error("Invalid element.");var t=n.previousElementSibling;if(t)return u(t,e);var r=e&&!n.parentElement&&n.parentNode&&n.parentNode instanceof DocumentFragment&&n.parentNode.host||null;return r&&r.lastElementChild?i(r.lastElementChild,e):o(n,e)},f=function(n,e){if(!n)throw new Error("Invalid element.");for(var t=[],r=n;r&&(r=o(r,e));)t.push(r);return t},c=function(n,e){if(!n)throw new Error("Invalid element.");for(var t=[],r=n;r&&(r=a(r,e))&&r!==n&&s(n,r,e);)t.push(r);return t},s=function(n,e,t){return!!(n||n instanceof HTMLElement)&&(!!(e||e instanceof HTMLElement)&&(t?f(e,!0).indexOf(n)>-1:n.contains(e)))},d={first:n,last:t,up:o,down:n,downLast:t,next:a,forward:a,prev:l,previous:l,backward:l,top:function(n,e){if(!n)throw new Error("Invalid element.");for(;;){var t=o(n,e);if(!t)break;n=t}return n},bottom:i,bottomLast:u,descendants:function n(e,t){if(!e)throw new Error("Invalid element.");var r=[];Array.prototype.slice.call(e.children).forEach(function(e){r=r.concat(e,n(e,t))}),t&&e.shadowRoot&&Array.prototype.slice.call(e.shadowRoot.children).forEach(function(e){r=r.concat(e,n(e,t))});return r},ancestors:f,forwardList:c,backwardList:function(n,e){if(!n)throw new Error("Invalid element.");return c(n,e).reverse()},contains:s};"undefined"!=typeof module&&module&&module.exports&&(module.exports=d),"undefined"!=typeof window&&window&&(window.DOMTraversal=window.DOMTraversal||d)}(),function(){if("undefined"!=typeof window&&!window.focusManager){var n=window.DOMTraversal,e=function(n){if(!n)throw new Error("Missing array.");return n instanceof Array||(n=Array.prototype.slice.call(n)),n.reduce(function(n,e){return e instanceof Array?this.flatten(e).forEach(function(e){n.push(e)}):n.push(e),n},[])},t=[],r=50,o=null,i=document.activeElement||null,u=[],a=[],l=null,f=1e6,c=function(e){if(!e)return!1;if(!(e instanceof HTMLElement))return!1;for(var t=n.ancestors(e,!0).reverse(),r=0;r=0)return!0;if(e.hasAttribute("disabled",null))return!1;var l=e.tagName;if("BUTTON"===l||"INPUT"===l||"SELECT"===l||"TEXTAREA"===l)return!0;if("A"===l&&(e.hasAttribute("href")||e.hasAttribute("xlink:href")))return!0;if("AUDIO"===l&&e.hasAttribute("controls"))return!0;if("VIDEO"===l&&e.hasAttribute("controls"))return!0;var f=e.getAttribute("contenteditable");return null!==f&&!0===f},s=function(n,e,t){n&&n instanceof Element&&("boolean"==typeof e&&(t=e,e=null),l&&clearTimeout(l),l=null,!0===t?n.focus(e||{}):l=setTimeout(function(){n.focus(e||{})},0))},d=function(e){e=e||document.body;var t=n.bottom(e,!0);if(t)return c(t)?t:(t=h(t),n.contains(e,t,!0)?t:null)},v=d,h=function(e){if(e||(e=i),!(e&&e instanceof Element))return null;for(;e&&(!(e=n.forward(e,!0))||!c(e)););for(var t=0;t0&&(e=o[0])}i=e;var l=u[0]||null;l&&(l.last=e),t.unshift(e),r>-1&&(t=t.slice(0,r));for(var f=0;f0&&(t=r[0])}var i=e.relatedTarget;if(i){var a=u[0]||null;if(a&&a.last){var l=n.ancestors(i,!0);a.containers.reduce(function(n,e){return n||l.indexOf(e)>-1},!1)||s(a.last)}}o=t},!0),document.addEventListener("keyup",function(n){if(n.shiftKey&&9===n.keyCode)for(var e=i,t=0;t-1})}).length>0)){var t={containers:n,last:d(n[0]),blur:null};u.unshift(t),s(t.last)}},untrap:function(){if(!(u.length<1)){var n=Array.prototype.slice.call(arguments);if(!(n.length<1||(n=e(n)).some(function(n){return!(n instanceof HTMLElement)}))){var t=u.filter(function(e){return e.containers.every(function(e){return n.indexOf(e)>-1})}),r=t&&t.length>0&&u[0]&&u[0]===t[0];u=u.filter(function(e){return!e.containers.every(function(e){return n.indexOf(e)>-1})}),r&&u[0]&&s(u[0].last)}}},order:function(){var n=null;e(arguments).forEach(function(e){if("number"!=typeof e){if(e instanceof HTMLElement){var t=f++;null!==n&&(t=n++),e.setAttribute("tabindex",""+t)}}else n=e})},proxy:function(n,e){if(n&&n instanceof HTMLElement&&e&&e instanceof HTMLElement&&c(e)){w(n,e);var t=n.getAttribute("tabindex")||null;(null===(t=t&&parseInt(t)||null)||t<0)&&n.setAttribute("tabindex",0),a.push({source:n,target:e,sourceTabIndex:t}),i===n&&s(e)}},unproxy:w},Object.defineProperty(window.focusManager,"currentlyFocused",{get:function(){return i}}),Object.defineProperty(window.focusManager,"previouslyFocused",{get:function(){return o||null}}),Object.defineProperty(window.focusManager,"history",{get:function(){return[].concat(t)}}),Object.defineProperty(window.focusManager,"historyLimit",{get:function(){return r},set:function(n){n=parseInt(n),isNaN(n)||n<-1||(r=n)>-1&&(t=t.slice(0,r))}})}}();
2 |
--------------------------------------------------------------------------------
/TheCaseForFocus.md:
--------------------------------------------------------------------------------
1 | # The Case for a New Focus System
2 |
3 | Twenty years ago I wrote my first Web Application. Essentially a message board system it had no semblance to a modern web application, of course, but it contained many of the features we see on modern applications even now. Users could register, login, read messages, post messages, reply, etc. It was written in ColdFusion 1.5 and everything was backed to a Microsoft Access Database; ancient technology by any modern standards.
4 |
5 | The reason for this personal memory reflection is to illustrate a specific problem I had back then, a specific problem that we as web developers still face today: Focus.
6 |
7 | See, in my nascent Web Application there was a login form: standard username and password and submit button thing we have today. And I wanted the user to be able to type their username, press ENTER to move to the next field, type their password, and press ENTER to submit. This field advancement behavior mimicked they way the logged into their network accounts at the time. This meant turning to JavaScript: a language and concept just in its infancy.
8 |
9 | The need was to notice when someone pressed the ENTER key while in the currently focused field. When that occurred, I wanted to advance the focus to next field. Simple, right?
10 |
11 | Move the clock forward some twenty years. I am building a web application for my new company and, of course, it has a login form with a username, password, and submit button. And, of course, I want to advance the focus when someone presses ENTER. It should come as no surprise but twenty years later and I have the exact same problem to solve. Instead, the surprising bit is that twenty years later, it still has the same solution:
12 |
13 | ```JavaScript
14 | var usernameField = form.querySelector("input[type=text]");
15 | var passwordField = form.querySelector("input[type=password]");
16 | usernameField.addEventListener("keyup",function(event){
17 | if (event.code!=="Enter") return;
18 | passwordField.focus();
19 | });
20 | ```
21 |
22 | ## The Current Focus API
23 |
24 | The history of our current focus system is pretty simple...
25 |
26 | In January of 1998 we get the [first formal specification of focus](https://www.w3.org/TR/REC-DOM-Level-1/level-one-html.html#method-focus) in the Document Object Model (DOM) Level 1 Specification.
27 |
28 | * `element.focus()` and `element.blur()` work almost identically the same as they did twenty years ago. `element.focus()` will request the focus be given to a specific element. `element.blur()` will move the focus upwards in the DOM to its parent focusable container.
29 |
30 | Later we get focus events...
31 |
32 | * `focus` and `blur` events let one track where the current focus is and respond to changes in the focus. `focusin` and `focusout` are defined around this time as well, but we do not formally get them until much later.
33 |
34 | Still later we get a few more small focus additions.
35 |
36 | * `document.hasFocus()` will indicate if a given document has focus either itself or as one of its descendants.
37 |
38 | * `document.activeElement` will return the current element of the document that holds the focus, if any.
39 |
40 | Together these represent the state of the current focus system we have today. It's adequate, it gets the job done, but it could be, **it should be** so much more.
41 |
42 | ## The Problem with Focus Today
43 |
44 | There are two very specific problems with our current focus system...
45 |
46 | 1). **Determining if an element can receive the focus is extremely complicated.** There are well defined rules in the [WHATWG HTML Specification (section 6.4)](https://html.spec.whatwg.org/multipage/interaction.html#focus) but specifications can be hard to read and getting this right has been a problem for many who have tried. INPUT tags, Anchor tags, the `tabindex` attrbitues, ARIA attributes, etc., they all contribute to whether or not an element can receive focus; not to mention if an element SHOULD recieve focus.
47 |
48 | 2). **There is no way to move the focus forward or backward without knowing what element needs to receive the focus.** This is especially becoming more problematic in the age of Web Components. Say, for example, I wanted to write a Web Component for a username field that would advance the focus to the next focusable element when ENTER is pressed. From the components perspective it knows nothing about what is outside of it, what element receives the focus next. Advancing the focus is entirely impossible without external knowledge.
49 |
50 | Both of these problems make working with focus harder than it needs to be. A modern web developer should not have to go to difficult lengths to do simple, common things. We can do it better.
51 |
52 | ## Solving Modern Focus
53 |
54 | To address focus for modern web development, we need to answer both of these concerns. We need a means to determine what is focusable and a means to advance to, and fallback from, the actively focused element. Answering these two points is paramount. If we can add in some other "nice to haves", so much the better.
55 |
56 | First and foremost, we need a call to determine if an element is focusable or not. A call such as `element.isFocusable()` whould work ideally here. It should respond with a boolean true or false and take into account all of the various ins and outs of whether something can be focused and absolve the developers from having to interpret the specification themselves.
57 |
58 | Second, a means to move the focus forward or backward from some given element without knowing what is before or after it. This can take a form such as `focusManager.forward()` or `focusManager.backward()`. Additionally, while we are at it, a means to identify what is the next or previous focus of any given element should be easy to determine. Something like `focusManager.next(element)` or `focusManager.previous(element)` that will return the next or previous element that could receive the focus.
59 |
60 | A handful of other convience methods are also extremely easy to do once we have `element.isFocusable()`. A means to get all of the focusable elements of the container, for example, with `focusManager.list(element)`. Or even more simple would be to have a way to find what was last focused prior to the current focus such as `focusManager.previouslyFocused` or a history of focus changes such as `focusManager.history`.
61 |
62 | All of these new focus requirements are geared at making focus easier to use and easier to understand. This in turn lends itself to creating more consistency for the users, which means easier and more understandable user interfaces and happier customers for everyone.
63 |
64 | With these new APIs, moving focus goes from the code we showed you above to
65 |
66 | ```javascript
67 | form.querySelector("input[type=text]").addEventListener("keyup",function(event){
68 | if (event.code!=="Enter") return;
69 | focusManager.forward();
70 | });
71 | ```
72 |
73 | It's not a huge reduction in lines of code, but it is vastly superior in complexity of understanding. And that should be the goal of any new system.
74 |
75 | ## Making It Happen
76 |
77 | So, how do we make focus better, how do we make the things we discussed here a reality for all?
78 |
79 | To address the problems of focus once and for all requires a change to the HTML specification that is managed by the W3C. Unless you are one of the big three browser creators, the process to enact change within the standards is very difficult. Fortunately, the W3C knows that this can be difficult and it created the Web Incubator Community Group or WICG. This is an online community dedicated to discussing and promoting new ideas for the W3C to consider. The WICG was created specifically for "everyday citizens" to propose and discuss changes to the W3C standards.
80 |
81 | To that end the FocusTraversal API, which encapsualtes all of the focus related features offered in this article, was [proposed to the WICG](https://discourse.wicg.io/t/proposal-focus-traversal-api/3427) on March 13, 2019.
82 |
83 | But the WICG is a very tiny microcosm and not a lot of people know it exists, even within the W3C standards body. The WICG is the starting point for any [proposal](https://discourse.wicg.io/t/proposal-focus-traversal-api/3427), but to really enact change you need community support. And this is where you can help: the FocusTraversal API proposal will never be realized without a lot more support. It needs further discussion in WICG; it needs outside promotion on twitter, reddit, and the like; and it needs participation and issues on Github.
84 |
85 | We have gotten the ball rolling. We have written the [PROPOSAL](https://discourse.wicg.io/t/proposal-focus-traversal-api/3427) and a more detailed [EXPLAINER](https://github.com/awesomeeng/FocusTraversalAPI/blob/master/EXPLAINER.md). We have written a [POLYFILL](https://github.com/awesomeeng/FocusTraversalAPI) to demonstrate the ideas. Yet writing these thing is the easy part; spreading the word is the challenge. If you want to help, and we are begging for help here, we need more engagement. We need discussion about the [proposal](https://discourse.wicg.io/t/proposal-focus-traversal-api/3427) on the WICG Discourse board; we need [participation in the Github](https://github.com/awesomeeng/FocusTraversalAPI) with issues and pulls and comments; and we need help spreading the word on twitter and reddit and blogs and the like.
86 |
87 | So, if you are willing to help, and we hope you are, please get involved, please spread the word, and please help us make focus easier to use for everyone.
88 |
89 | ## More Information
90 |
91 | - PROPOSAL: [https://discourse.wicg.io/t/proposal-focus-traversal-api/3427](https://discourse.wicg.io/t/proposal-focus-traversal-api/3427)
92 |
93 | - EXPLAINER: [https://github.com/awesomeeng/FocusTraversalAPI/blob/master/EXPLAINER.md](https://github.com/awesomeeng/FocusTraversalAPI/blob/master/EXPLAINER.md)
94 |
95 | - POLYFILL: [https://github.com/awesomeeng/FocusTraversalAPI](https://github.com/awesomeeng/FocusTraversalAPI)
96 |
97 | - GITHUB: [https://github.com/awesomeeng/FocusTraversalAPI](https://github.com/awesomeeng/FocusTraversalAPI)
98 |
--------------------------------------------------------------------------------
/test/DOMTraversalTest.js:
--------------------------------------------------------------------------------
1 | // (c) 2018, The Awesome Engineering Company, https://awesomeneg.com
2 |
3 | /*
4 | Tests for .......
5 | */
6 |
7 | "use strict";
8 |
9 | const assert = require("assert");
10 | const DT = require("../src/DOMTraversal");
11 | const {JSDOM} = require("jsdom");
12 |
13 | let tree = new JSDOM(`
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | `);
53 | let window = tree.window;
54 | let document = window.document;
55 | let html = document.body.parentElement;
56 | let body = document.body;
57 | let head = document.head;
58 |
59 | let nodes = {
60 | a: body.querySelector(".a"),
61 | aa: body.querySelector(".aa"),
62 | aaa: body.querySelector(".aaa"),
63 | ab: body.querySelector(".ab"),
64 | b: body.querySelector(".b"),
65 | c: body.querySelector(".c"),
66 | ca: body.querySelector(".ca"),
67 | cb: body.querySelector(".cb"),
68 | cc: body.querySelector(".cc"),
69 | d: body.querySelector(".d"),
70 | da: body.querySelector(".da"),
71 | daa: body.querySelector(".daa"),
72 | dab: body.querySelector(".dab"),
73 | daba: body.querySelector(".daba"),
74 | dac: body.querySelector(".dac")
75 | };
76 |
77 | describe("DOMTraversal",function(){
78 | it("available",function(){
79 | assert(DT);
80 | });
81 |
82 | it("#down",function(){
83 | assert.equal(DT.down(body),nodes.a);
84 | assert.equal(DT.down(nodes.a),nodes.aa);
85 | assert.equal(DT.down(nodes.aa),nodes.aaa);
86 | assert.equal(DT.down(nodes.aaa),null);
87 | });
88 |
89 | it("#downLast",function(){
90 | assert.equal(DT.downLast(body),nodes.d);
91 | assert.equal(DT.downLast(nodes.a),nodes.ab);
92 | assert.equal(DT.downLast(nodes.aa),nodes.aaa);
93 | assert.equal(DT.downLast(nodes.aaa),null);
94 | assert.equal(DT.downLast(nodes.c),nodes.cc);
95 | });
96 |
97 | it("#up",function(){
98 | assert.equal(DT.up(nodes.a),body);
99 | assert.equal(DT.up(nodes.aa),nodes.a);
100 | assert.equal(DT.up(nodes.aaa),nodes.aa);
101 | });
102 |
103 | it("#first",function(){
104 | assert.equal(DT.first(body),nodes.a);
105 | assert.equal(DT.first(nodes.a),nodes.aa);
106 | assert.equal(DT.first(nodes.b),null);
107 | assert.equal(DT.first(nodes.c),nodes.ca);
108 | assert.equal(DT.first(nodes.d),nodes.da);
109 | });
110 |
111 | it("#last",function(){
112 | assert.equal(DT.last(body),nodes.d);
113 | assert.equal(DT.last(nodes.a),nodes.ab);
114 | assert.equal(DT.last(nodes.b),null);
115 | assert.equal(DT.last(nodes.c),nodes.cc);
116 | assert.equal(DT.last(nodes.d),nodes.da);
117 | });
118 |
119 | it("#top",function(){
120 | assert.equal(DT.top(body),html);
121 | assert.equal(DT.top(nodes.aaa),html);
122 | assert.equal(DT.top(nodes.aa),html);
123 | assert.equal(DT.top(nodes.cb),html);
124 | assert.equal(DT.top(nodes.dac),html);
125 | });
126 |
127 | it("#bottom",function(){
128 | assert.equal(DT.bottom(body),nodes.aaa);
129 | assert.equal(DT.bottom(nodes.a),nodes.aaa);
130 | assert.equal(DT.bottom(nodes.b),nodes.b);
131 | assert.equal(DT.bottom(nodes.c),nodes.ca);
132 | assert.equal(DT.bottom(nodes.d),nodes.daa);
133 | });
134 |
135 | it("#bottomLast",function(){
136 | assert.equal(DT.bottomLast(body),nodes.dac);
137 | assert.equal(DT.bottomLast(nodes.a),nodes.ab);
138 | assert.equal(DT.bottomLast(nodes.b),nodes.b);
139 | assert.equal(DT.bottomLast(nodes.c),nodes.cc);
140 | assert.equal(DT.bottomLast(nodes.d),nodes.dac);
141 | });
142 |
143 | it("#next",function(){
144 | assert.equal(DT.next(body),nodes.a);
145 | assert.equal(DT.next(nodes.a),nodes.aa);
146 | assert.equal(DT.next(nodes.aa),nodes.aaa);
147 | assert.equal(DT.next(nodes.aaa),nodes.ab);
148 | assert.equal(DT.next(nodes.ab),nodes.b);
149 | assert.equal(DT.next(nodes.b),nodes.c);
150 | assert.equal(DT.next(nodes.c),nodes.ca);
151 | assert.equal(DT.next(nodes.ca),nodes.cb);
152 | assert.equal(DT.next(nodes.cb),nodes.cc);
153 | assert.equal(DT.next(nodes.cc),nodes.d);
154 | assert.equal(DT.next(nodes.d),nodes.da);
155 | assert.equal(DT.next(nodes.da),nodes.daa);
156 | assert.equal(DT.next(nodes.daa),nodes.dab);
157 | assert.equal(DT.next(nodes.dab),nodes.daba);
158 | assert.equal(DT.next(nodes.daba),nodes.dac);
159 | assert.equal(DT.next(nodes.dac),null);
160 | });
161 |
162 | it("#forward",function(){
163 | assert.equal(DT.forward(body),nodes.a);
164 | assert.equal(DT.forward(nodes.a),nodes.aa);
165 | assert.equal(DT.forward(nodes.aa),nodes.aaa);
166 | assert.equal(DT.forward(nodes.aaa),nodes.ab);
167 | assert.equal(DT.forward(nodes.ab),nodes.b);
168 | assert.equal(DT.forward(nodes.b),nodes.c);
169 | assert.equal(DT.forward(nodes.c),nodes.ca);
170 | assert.equal(DT.forward(nodes.ca),nodes.cb);
171 | assert.equal(DT.forward(nodes.cb),nodes.cc);
172 | assert.equal(DT.forward(nodes.cc),nodes.d);
173 | assert.equal(DT.forward(nodes.d),nodes.da);
174 | assert.equal(DT.forward(nodes.da),nodes.daa);
175 | assert.equal(DT.forward(nodes.daa),nodes.dab);
176 | assert.equal(DT.forward(nodes.dab),nodes.daba);
177 | assert.equal(DT.forward(nodes.daba),nodes.dac);
178 | assert.equal(DT.forward(nodes.dac),null);
179 | });
180 |
181 | it("#previous",function(){
182 | assert.equal(DT.previous(nodes.dac),nodes.daba);
183 | assert.equal(DT.previous(nodes.daba),nodes.dab);
184 | assert.equal(DT.previous(nodes.dab),nodes.daa);
185 | assert.equal(DT.previous(nodes.daa),nodes.da);
186 | assert.equal(DT.previous(nodes.da),nodes.d);
187 | assert.equal(DT.previous(nodes.d),nodes.cc);
188 | assert.equal(DT.previous(nodes.cc),nodes.cb);
189 | assert.equal(DT.previous(nodes.cb),nodes.ca);
190 | assert.equal(DT.previous(nodes.ca),nodes.c);
191 | assert.equal(DT.previous(nodes.c),nodes.b);
192 | assert.equal(DT.previous(nodes.b),nodes.ab);
193 | assert.equal(DT.previous(nodes.ab),nodes.aaa);
194 | assert.equal(DT.previous(nodes.aaa),nodes.aa);
195 | assert.equal(DT.previous(nodes.aa),nodes.a);
196 | assert.equal(DT.previous(nodes.a),body);
197 | });
198 |
199 | it("#backward",function(){
200 | assert.equal(DT.backward(nodes.dac),nodes.daba);
201 | assert.equal(DT.backward(nodes.daba),nodes.dab);
202 | assert.equal(DT.backward(nodes.dab),nodes.daa);
203 | assert.equal(DT.backward(nodes.daa),nodes.da);
204 | assert.equal(DT.backward(nodes.da),nodes.d);
205 | assert.equal(DT.backward(nodes.d),nodes.cc);
206 | assert.equal(DT.backward(nodes.cc),nodes.cb);
207 | assert.equal(DT.backward(nodes.cb),nodes.ca);
208 | assert.equal(DT.backward(nodes.ca),nodes.c);
209 | assert.equal(DT.backward(nodes.c),nodes.b);
210 | assert.equal(DT.backward(nodes.b),nodes.ab);
211 | assert.equal(DT.backward(nodes.ab),nodes.aaa);
212 | assert.equal(DT.backward(nodes.aaa),nodes.aa);
213 | assert.equal(DT.backward(nodes.aa),nodes.a);
214 | assert.equal(DT.backward(nodes.a),body);
215 | });
216 |
217 | it("#descendants",function(){
218 | assert.deepStrictEqual(DT.descendants(body),[nodes.a, nodes.aa, nodes.aaa, nodes.ab, nodes.b, nodes.c, nodes.ca, nodes.cb, nodes.cc, nodes.d, nodes.da, nodes.daa, nodes.dab, nodes.daba, nodes.dac]);
219 | assert.deepStrictEqual(DT.descendants(nodes.a),[nodes.aa, nodes.aaa, nodes.ab]);
220 | });
221 |
222 | it("#ancestors",function(){
223 | assert.deepStrictEqual(DT.ancestors(body),[html]);
224 | assert.deepStrictEqual(DT.ancestors(nodes.a),[body,html]);
225 | assert.deepStrictEqual(DT.ancestors(nodes.aa),[nodes.a,body,html]);
226 | assert.deepStrictEqual(DT.ancestors(nodes.aaa),[nodes.aa,nodes.a,body,html]);
227 | assert.deepStrictEqual(DT.ancestors(nodes.b),[body,html]);
228 | assert.deepStrictEqual(DT.ancestors(nodes.c),[body,html]);
229 | assert.deepStrictEqual(DT.ancestors(nodes.ca),[nodes.c,body,html]);
230 | assert.deepStrictEqual(DT.ancestors(nodes.cb),[nodes.c,body,html]);
231 | assert.deepStrictEqual(DT.ancestors(nodes.cc),[nodes.c,body,html]);
232 | assert.deepStrictEqual(DT.ancestors(nodes.d),[body,html]);
233 | assert.deepStrictEqual(DT.ancestors(nodes.da),[nodes.d,body,html]);
234 | assert.deepStrictEqual(DT.ancestors(nodes.daa),[nodes.da,nodes.d,body,html]);
235 | assert.deepStrictEqual(DT.ancestors(nodes.dab),[nodes.da,nodes.d,body,html]);
236 | assert.deepStrictEqual(DT.ancestors(nodes.daba),[nodes.dab,nodes.da,nodes.d,body,html]);
237 | assert.deepStrictEqual(DT.ancestors(nodes.dac),[nodes.da,nodes.d,body,html]);
238 | });
239 |
240 | it("#forwardList",function(){
241 | assert.deepStrictEqual(DT.forwardList(body),[nodes.a,nodes.aa,nodes.aaa,nodes.ab,nodes.b,nodes.c,nodes.ca,nodes.cb,nodes.cc,nodes.d,nodes.da,nodes.daa,nodes.dab,nodes.daba,nodes.dac]);
242 | assert.deepStrictEqual(DT.forwardList(nodes.a),[nodes.aa,nodes.aaa,nodes.ab]);
243 | assert.deepStrictEqual(DT.forwardList(nodes.c),[nodes.ca,nodes.cb,nodes.cc]);
244 | assert.deepStrictEqual(DT.forwardList(nodes.d),[nodes.da,nodes.daa,nodes.dab,nodes.daba,nodes.dac]);
245 | });
246 |
247 | it("#backwardList",function(){
248 | assert.deepStrictEqual(DT.backwardList(body),[nodes.dac,nodes.daba,nodes.dab,nodes.daa,nodes.da,nodes.d,nodes.cc,nodes.cb,nodes.ca,nodes.c,nodes.b,nodes.ab,nodes.aaa,nodes.aa,nodes.a]);
249 | assert.deepStrictEqual(DT.backwardList(nodes.a),[nodes.ab,nodes.aaa,nodes.aa]);
250 | assert.deepStrictEqual(DT.backwardList(nodes.c),[nodes.cc,nodes.cb,nodes.ca]);
251 | assert.deepStrictEqual(DT.backwardList(nodes.d),[nodes.dac,nodes.daba,nodes.dab,nodes.daa,nodes.da]);
252 | });
253 |
254 |
255 |
256 |
257 | });
258 |
--------------------------------------------------------------------------------
/EXPLAINER.md:
--------------------------------------------------------------------------------
1 | # Focus Traversal API
2 |
3 | This is a proposed feature that would allow an author to better understand and manipulate the Focus system within a web page. This includes support for computing the next and previous Focus targets for any given element, moving the focus forward or backward programatically from an element without knowing the next focus target, and other focus related features.
4 |
5 | ## The Problem
6 |
7 | The current system for programmatically manipulating focus within a web page leaves a lot to be desired. A single element can request the focus with the `focus()` method but that is the extent of the programatic focus navigation options. Advancing the focus to the next focusable element or the previous focusable element involves a complex dance of DOM traversal and guess work as to what elements can receive focus or not. Most solutions to manipulating focus are complicated; the very simple need to manipulate focus in a meaningful and accessable manner is significantly lacking.
8 |
9 | #### Focusable
10 |
11 | A big part of the problem is how one determines if an element is "focusable" or not and can receive focus. There are very complex rules associated with this process as outlined in section 5.4.2 of the [W3C HTML Standard (as of 5.3 Working Draft, 18 October 2018)](https://www.w3.org/TR/2018/WD-html53-20181018/editing.html#data-model). These rules are fairly stable, but could change and expecting userland modules to keep in sync or even be in sync currently is unrealistic.
12 |
13 | #### Moving Focus Programatically
14 |
15 | In order to move focus from any given element to the next (or prior) focusable element is complicated. One would need to walk the DOM in a forward manner (down to lowest leaf of next sibling, then next sibling, then parent, etc). For each element, one would need to apply the rules for what is focusable. If the element is focusable, move the focus there. If not, move to the next element.
16 |
17 | This is not an easy process for users. Many userland modules and frameworks attempt to solve this problem, but as stated above there is a an unrealistic expectation of these modules to stay in concert with the focusable rules.
18 |
19 | ## The Focus Traversal API
20 |
21 | This proposal introduces the concepts of a Focus Traversal API to solve these problems. This API exists as an additional set of behaviours over and above what can be done with the existing focus system.
22 |
23 | ### The `window.focusManager` Object
24 |
25 | This proposal suggests the creating of a top level `window` property called `focusManager` which will be used to expose a series of properties and methods for working with Focus traversal.
26 |
27 | #### Focusable
28 |
29 | To address the complexity with determining if an element is focusable or not it is porposed to create `focusManager.isFocusable(element)` which will return true if the given element is able to receive the focus according to the Focus rules laid out in section 5.4.2 of the [W3C HTML Standard (as of 5.3 Working Draft, 18 October 2018)](https://www.w3.org/TR/2018/WD-html53-20181018/editing.html#data-model).
30 |
31 | #### Next/Previous Focus
32 |
33 | Moving the focus forward or backward is a key operation for developers and the Focus Traversal API seeks to ease this process greatly with the `focusManager.next(element)` and the `focusManager.previous(element)` methods. These functions will return the next (or previous) element that meets the `focusManager.isFocusable()` condition, or null if there are no next or previous elements that meet the requirement.
34 |
35 | #### Forward/Backward Focus Traversal
36 |
37 | A method to allow users to programatically move the focus forward or backward without having to manually compute the next or previous focus is highly desireable. `focusManager.forward()` takes the currently focused element and computes the next element that meets the `focusManager.isFocusable()` condition, and advances the focus to that element using `element.focus()`. `focusManager.backward()` is similar but it computes the pervious element that meets the `focusManager.isFocusable()` condition and moves the focus to that element using `element.focus()`.
38 |
39 | #### Other Features
40 |
41 | As part of this proposal some additional conveinience methods and properties are offered in an attempt to centralize and organize the focus system.
42 |
43 | `window.focusManager.currentlyFocused` - Contains the element currently holding the focus, if any.
44 |
45 | `window.focusManager.previouslyFocused` - Contains the element that held the focus prior to the current focus, if any.
46 |
47 | `window.focusManager.history` - An array of the last n historical focus holders. It is recommended this be capped at some number like 50 or 100 to prevent unnecessary memory leakage.
48 |
49 | `window.focusManager.hasFocus(element)` - Returns true if the given element currently has the focus. Functionally equivelent to `window.focusManager.currentlyFocused === element`.
50 |
51 | `window.focusManager.focus(element,focusOption)` - Focus on the given element. Functionally the same as `element.focus()`. Returns void.
52 |
53 | `window.focusManager.list(element)` - Returns an array of all focusable elements in the order that focus traversal would occur.
54 |
55 | `window.focusManager.order(element,element,element,etc)` - Programatically set the traversal order of one or more elements. Given an array of elements (or multiple arguments) order them in the order they are given.
56 |
57 | `window.focusManager.trap(element)` - Trap focus within a given element, such that any focus event outside of the element's descendants will refocus the last focused element within the element's descendants. Traps calls stack, such that the latest trap always wins, but removing a trap will set th enext prior trap running.
58 |
59 | `window.focusManager.untrap(element)` - Removes a trap.
60 |
61 | `window.focusManager.proxy(source,target)` - When the given source element receives the focus, forward that focus immediately to the target element.
62 |
63 | `window.focusManager.unproxy(source,target)` - Removes a proxy.
64 |
65 | ### Special Considerations
66 |
67 | Some special cases occur to which this specification must be mindful:
68 |
69 | - ShadowDOM: It is proposed that the Focus Management API also delve into ShadowDOM elements when computing the next or previous focus. If an element has an attached ShadowDOM, it must be traversed in accordance with its contents and any `` elements.
70 |
71 | - In especially large DOM trees computing the next focus, previous focus, or the `list()` list could be significantly slow. It is recommended that these functions be asyncrounous and return a Promise instead of syncronously blocking.
72 |
73 | ## Benefits
74 |
75 | The benefits of adopting this proposal include easing developer efforts in regard to Focus, more consistent behavior between web pages and applications, and increasing performance. No longer would developers be forced to rely on third party focus libraries nor forced to compute what is or is not focused manually thus reducing their exposure to errors and inconsistency.
76 |
77 | ## Concerns
78 |
79 | Largely, this proposal should not impact any existing systems as it is an addition to instead of a replacement or removal. Some consideration should be made to extract out the rule system for determining the focus-ability of an element into a common space if not already done so by implementors.
80 |
81 | Also, this proposal is currently unsure about how this would interact with an `