wicg-focus-ring
Advanced tools
Comparing version 2.0.2 to 3.0.0
@@ -8,188 +8,22 @@ (function (global, factory) { | ||
/** | ||
* Module export | ||
* | ||
* @param {Element} el | ||
* @return {ClassList} | ||
*/ | ||
var index = function (el) { | ||
return new ClassList(el); | ||
}; | ||
/** | ||
* Initialize a new ClassList for the given element | ||
* | ||
* @param {Element} el DOM Element | ||
*/ | ||
function ClassList(el) { | ||
if (!el || el.nodeType !== 1) { | ||
throw new Error('A DOM Element reference is required'); | ||
} | ||
this.el = el; | ||
this.classList = el.classList; | ||
} | ||
/** | ||
* Check token validity | ||
* | ||
* @param token | ||
* @param [method] | ||
*/ | ||
function checkToken(token, method) { | ||
method = method || 'a method'; | ||
if (typeof token != 'string') { | ||
throw new TypeError( | ||
'Failed to execute \'' + method + '\' on \'ClassList\': ' + | ||
'the token provided (\'' + token + '\') is not a string.' | ||
); | ||
} | ||
if (token === "") { | ||
throw new SyntaxError( | ||
'Failed to execute \'' + method + '\' on \'ClassList\': ' + | ||
'the token provided must not be empty.' | ||
); | ||
} | ||
if (/\s/.test(token)) { | ||
throw new Error( | ||
'Failed to execute \'' + method + '\' on \'ClassList\': ' + | ||
'the token provided (\'' + token + '\') contains HTML space ' + | ||
'characters, which are not valid in tokens.' | ||
); | ||
} | ||
} | ||
/** | ||
* Return an array of the class names on the element. | ||
* | ||
* @return {Array} | ||
*/ | ||
ClassList.prototype.toArray = function () { | ||
var str = (this.el.getAttribute('class') || '').replace(/^\s+|\s+$/g, ''); | ||
var classes = str.split(/\s+/); | ||
if ('' === classes[0]) { classes.shift(); } | ||
return classes; | ||
}; | ||
/** | ||
* Add the given `token` to the class list if it's not already present. | ||
* | ||
* @param {String} token | ||
*/ | ||
ClassList.prototype.add = function (token) { | ||
var classes, index, updated; | ||
checkToken(token, 'add'); | ||
if (this.classList) { | ||
this.classList.add(token); | ||
} | ||
else { | ||
// fallback | ||
classes = this.toArray(); | ||
index = classes.indexOf(token); | ||
if (index === -1) { | ||
classes.push(token); | ||
this.el.setAttribute('class', classes.join(' ')); | ||
} | ||
} | ||
return; | ||
}; | ||
/** | ||
* Check if the given `token` is in the class list. | ||
* | ||
* @param {String} token | ||
* @return {Boolean} | ||
*/ | ||
ClassList.prototype.contains = function (token) { | ||
checkToken(token, 'contains'); | ||
return this.classList ? | ||
this.classList.contains(token) : | ||
this.toArray().indexOf(token) > -1; | ||
}; | ||
/** | ||
* Remove any class names that match the given `token`, when present. | ||
* | ||
* @param {String|RegExp} token | ||
*/ | ||
ClassList.prototype.remove = function (token) { | ||
var arr, classes, i, index, len; | ||
if ('[object RegExp]' == Object.prototype.toString.call(token)) { | ||
arr = this.toArray(); | ||
for (i = 0, len = arr.length; i < len; i++) { | ||
if (token.test(arr[i])) { | ||
this.remove(arr[i]); | ||
} | ||
} | ||
} | ||
else { | ||
checkToken(token, 'remove'); | ||
if (this.classList) { | ||
this.classList.remove(token); | ||
} | ||
else { | ||
// fallback | ||
classes = this.toArray(); | ||
index = classes.indexOf(token); | ||
if (index > -1) { | ||
classes.splice(index, 1); | ||
this.el.setAttribute('class', classes.join(' ')); | ||
} | ||
} | ||
} | ||
return; | ||
}; | ||
/** | ||
* Toggle the `token` in the class list. Optionally force state via `force`. | ||
* | ||
* Native `classList` is not used as some browsers that support `classList` do | ||
* not support `force`. Avoiding `classList` altogether keeps this function | ||
* simple. | ||
* | ||
* @param {String} token | ||
* @param {Boolean} [force] | ||
* @return {Boolean} | ||
*/ | ||
ClassList.prototype.toggle = function (token, force) { | ||
checkToken(token, 'toggle'); | ||
var hasToken = this.contains(token); | ||
var method = hasToken ? (force !== true && 'remove') : (force !== false && 'add'); | ||
if (method) { | ||
this[method](token); | ||
} | ||
return (typeof force == 'boolean' ? force : !hasToken); | ||
}; | ||
/** | ||
* https://github.com/WICG/focus-ring | ||
*/ | ||
function init() { | ||
var hadKeyboardEvent = false; | ||
var hadKeyboardEvent = true; | ||
var elWithFocusRing; | ||
var inputTypesWhitelist = { | ||
'text': true, | ||
'search': true, | ||
'url': true, | ||
'tel': true, | ||
'email': true, | ||
'password': true, | ||
'number': true, | ||
'date': true, | ||
'month': true, | ||
'week': true, | ||
'time': true, | ||
'datetime': true, | ||
'datetime-local': true, | ||
text: true, | ||
search: true, | ||
url: true, | ||
tel: true, | ||
email: true, | ||
password: true, | ||
number: true, | ||
date: true, | ||
month: true, | ||
week: true, | ||
time: true, | ||
datetime: true, | ||
'datetime-local': true | ||
}; | ||
@@ -208,10 +42,13 @@ | ||
if (tagName == 'INPUT' && inputTypesWhitelist[type] && !el.readonly) | ||
if (tagName == 'INPUT' && inputTypesWhitelist[type] && !el.readonly) { | ||
return true; | ||
} | ||
if (tagName == 'TEXTAREA' && !el.readonly) | ||
if (tagName == 'TEXTAREA' && !el.readonly) { | ||
return true; | ||
} | ||
if (el.contentEditable == 'true') | ||
if (el.contentEditable == 'true') { | ||
return true; | ||
} | ||
@@ -227,5 +64,6 @@ return false; | ||
function addFocusRingClass(el) { | ||
if (index(el).contains('focus-ring')) | ||
if (el.classList.contains('focus-ring')) { | ||
return; | ||
index(el).add('focus-ring'); | ||
} | ||
el.classList.add('focus-ring'); | ||
el.setAttribute('data-focus-ring-added', ''); | ||
@@ -240,5 +78,6 @@ } | ||
function removeFocusRingClass(el) { | ||
if (!el.hasAttribute('data-focus-ring-added')) | ||
if (!el.hasAttribute('data-focus-ring-added')) { | ||
return; | ||
index(el).remove('focus-ring'); | ||
} | ||
el.classList.remove('focus-ring'); | ||
el.removeAttribute('data-focus-ring-added'); | ||
@@ -249,11 +88,23 @@ } | ||
* On `keydown`, set `hadKeyboardEvent`, add `focus-ring` class if the | ||
* key was Tab. | ||
* key was Tab/Shift-Tab or Arrow Keys. | ||
* @param {Event} e | ||
*/ | ||
function onKeyDown(e) { | ||
if (e.altKey || e.ctrlKey || e.metaKey) | ||
const allowedKeys = [ | ||
9, // TAB | ||
37, // LEFT | ||
38, // UP | ||
39, // RIGHT | ||
40 // DOWN | ||
]; | ||
// Ignore keypresses if the user is holding down a modifier key. | ||
if (e.altKey || e.ctrlKey || e.metaKey) { | ||
return; | ||
} | ||
if (e.keyCode != 9) | ||
// Ignore keypresses which aren't related to keyboard navigation. | ||
if (allowedKeys.indexOf(e.keyCode) === -1) { | ||
return; | ||
} | ||
@@ -265,3 +116,3 @@ hadKeyboardEvent = true; | ||
* On `focus`, add the `focus-ring` class to the target if: | ||
* - the target received focus as a result of keyboard navigation | ||
* - the target received focus as a result of keyboard navigation, or | ||
* - the event target is an element that will likely require interaction | ||
@@ -272,4 +123,5 @@ * via the keyboard (e.g. a text box) | ||
function onFocus(e) { | ||
if (e.target == document) | ||
if (e.target == document) { | ||
return; | ||
} | ||
@@ -287,4 +139,5 @@ if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) { | ||
function onBlur(e) { | ||
if (e.target == document) | ||
if (e.target == document) { | ||
return; | ||
} | ||
@@ -299,8 +152,6 @@ removeFocusRingClass(e.target); | ||
function onWindowFocus() { | ||
// When removing the activeElement from DOM it's possible IE11 is in state | ||
// document.activeElement === null | ||
if (!document.activeElement) | ||
return; | ||
if (document.activeElement == elWithFocusRing) | ||
window.removeEventListener('focus', onWindowFocus, true); | ||
if (document.activeElement == elWithFocusRing) { | ||
addFocusRingClass(elWithFocusRing); | ||
} | ||
@@ -313,12 +164,15 @@ elWithFocusRing = null; | ||
* focus-ring class. | ||
* @param {Event} e | ||
*/ | ||
function onWindowBlur() { | ||
// When removing the activeElement from DOM it's possible IE11 is in state | ||
// document.activeElement === null | ||
if (!document.activeElement) | ||
function onWindowBlur(e) { | ||
if (e.target !== window) { | ||
return; | ||
if (index(document.activeElement).contains('focus-ring')) { | ||
// Keep a reference to the element to which the focus-ring class is applied | ||
// so the focus-ring class can be restored to it if the window regains | ||
// focus after being blurred. | ||
} | ||
window.addEventListener('focus', onWindowFocus, true); | ||
addInitialPointerMoveListeners(); | ||
if (document.activeElement.classList.contains('focus-ring')) { | ||
// Keep a reference to the element to which the focus-ring class is | ||
// applied so the focus-ring class can be restored to it if the window | ||
// regains focus after being blurred. | ||
elWithFocusRing = document.activeElement; | ||
@@ -328,2 +182,43 @@ } | ||
/** | ||
* Add a group of listeners to detect usage of any pointing devices. | ||
* These listeners will be added when the polyfill first loads, and anytime | ||
* the window is blurred, so that they are active when the window regains focus. | ||
*/ | ||
function addInitialPointerMoveListeners() { | ||
document.addEventListener('mousemove', onInitialPointerMove); | ||
document.addEventListener('mousedown', onInitialPointerMove); | ||
document.addEventListener('mouseup', onInitialPointerMove); | ||
document.addEventListener('pointermove', onInitialPointerMove); | ||
document.addEventListener('pointerdown', onInitialPointerMove); | ||
document.addEventListener('pointerup', onInitialPointerMove); | ||
document.addEventListener('touchmove', onInitialPointerMove); | ||
document.addEventListener('touchstart', onInitialPointerMove); | ||
document.addEventListener('touchend', onInitialPointerMove); | ||
} | ||
/** | ||
* When the polfyill first loads, assume the user is in keyboard modality. | ||
* If any event is received from a pointing device (e.g mouse, pointer, touch), turn off | ||
* keyboard modality. | ||
* This accounts for situations where focus enters the page from the URL bar. | ||
* @param {Event} e | ||
*/ | ||
function onInitialPointerMove(e) { | ||
// Work around a Safari quirk that fires a mousemove on <html> whenever the | ||
// window blurs, even if you're tabbing out of the page. ¯\_(ツ)_/¯ | ||
if (e.target.nodeName.toLowerCase() === 'html') return; | ||
hadKeyboardEvent = false; | ||
document.removeEventListener('mousemove', onInitialPointerMove); | ||
document.removeEventListener('mousedown', onInitialPointerMove); | ||
document.removeEventListener('mouseup', onInitialPointerMove); | ||
document.removeEventListener('pointermove', onInitialPointerMove); | ||
document.removeEventListener('pointerdown', onInitialPointerMove); | ||
document.removeEventListener('pointerup', onInitialPointerMove); | ||
document.removeEventListener('touchmove', onInitialPointerMove); | ||
document.removeEventListener('touchstart', onInitialPointerMove); | ||
document.removeEventListener('touchend', onInitialPointerMove); | ||
} | ||
document.addEventListener('keydown', onKeyDown, true); | ||
@@ -334,4 +229,5 @@ document.addEventListener('focus', onFocus, true); | ||
window.addEventListener('blur', onWindowBlur, true); | ||
addInitialPointerMoveListeners(); | ||
index(document.body).add('js-focus-ring'); | ||
document.body.classList.add('js-focus-ring'); | ||
} | ||
@@ -338,0 +234,0 @@ |
{ | ||
"name": "wicg-focus-ring", | ||
"version": "2.0.2", | ||
"version": "3.0.0", | ||
"description": "Polyfill for :focus-ring pseudo-selector", | ||
"scripts": { | ||
"lint": "eslint src/*.js", | ||
"test": "npm run lint", | ||
"build": "rollup -c", | ||
"dev": "rollup -c -w", | ||
"prepublish": "npm run test && npm run build" | ||
"precommit": "lint-staged", | ||
"prepublishOnly": "npm run test", | ||
"test:server": "http-server . -s", | ||
"test:selenium": "wait-on http://localhost:8080 && node ./test/index.js", | ||
"test": | ||
"npm run build && concurrently -k \"npm run test:server\" \"npm run test:selenium\"" | ||
}, | ||
"lint-staged": { | ||
"*.{js,json,css}": ["prettier --write", "git add"] | ||
}, | ||
"main": "dist/focus-ring.js", | ||
@@ -24,12 +29,24 @@ "repository": { | ||
"devDependencies": { | ||
"eslint": "^3.13.1", | ||
"eslint-config-google": "^0.7.1", | ||
"rollup": "^0.41.6", | ||
"rollup-plugin-commonjs": "^8.0.2", | ||
"rollup-plugin-node-resolve": "^3.0.0", | ||
"rollup-watch": "^3.2.2" | ||
"chromedriver": "2.33.2", | ||
"clear-module": "2.1.0", | ||
"concurrently": "3.5.0", | ||
"expect": "1.20.2", | ||
"geckodriver": "1.10.0", | ||
"glob-promise": "3.3.0", | ||
"http-server": "0.10.0", | ||
"husky": "0.14.3", | ||
"lint-staged": "5.0.0", | ||
"mocha": "4.0.1", | ||
"mz": "2.7.0", | ||
"prettier": "1.8.2", | ||
"rollup": "0.51.5", | ||
"rollup-plugin-commonjs": "8.2.6", | ||
"rollup-plugin-node-resolve": "3.0.0", | ||
"rollup-watch": "4.3.1", | ||
"selenium-assistant": "5.2.0", | ||
"wait-on": "2.0.2" | ||
}, | ||
"dependencies": { | ||
"dom-classlist": "^1.0.1" | ||
"greenkeeper": { | ||
"ignore": ["expect"] | ||
} | ||
} |
170
README.md
[![Build Status](https://travis-ci.org/WICG/focus-ring.svg?branch=master)](https://travis-ci.org/WICG/focus-ring) | ||
[![Greenkeeper badge](https://badges.greenkeeper.io/WICG/focus-ring.svg)](https://greenkeeper.io/) | ||
@@ -9,4 +10,10 @@ Based on the proposed CSS | ||
[Demo](https://wicg.github.io/focus-ring/demo) | ||
# Details | ||
- Read the [Explainer](explainer.md). | ||
- Read the [Spec](https://drafts.csswg.org/selectors-4/#the-focusring-pseudo). | ||
- Try the [Demo](https://wicg.github.io/focus-ring/demo). | ||
# Polyfill | ||
## Installation | ||
@@ -16,4 +23,21 @@ | ||
_We recommend only using versions of the polyfill that have been published to npm, rather than | ||
cloning the repo and using the source directly. This helps ensure the version you're using is stable | ||
and thoroughly tested._ | ||
_If you do want to build from source, make sure you clone the latest tag!_ | ||
## Usage | ||
### 1. Add the script to your page | ||
```html | ||
... | ||
<script src="/node_modules/wicg-focus-ring/dist/focus-ring.js"></script> | ||
</body> | ||
</html> | ||
``` | ||
### 2. Update your CSS | ||
We suggest that users | ||
@@ -24,5 +48,9 @@ selectively disable the default focus style | ||
```html | ||
```css | ||
/* | ||
This will hide the focus indicator if the element receives focus via the mouse, | ||
but it will still show up on keyboard focus. | ||
*/ | ||
.js-focus-ring :focus:not(.focus-ring) { | ||
outline-width: 0; | ||
outline: 0; | ||
} | ||
@@ -35,126 +63,8 @@ ``` | ||
## Rationale | ||
### How it works | ||
The status quo, `:focus`, is quite problematic: | ||
- Many developers disable the default focus ring in their CSS styles, | ||
others attempt to style it in concert with their design. | ||
The former often seems to be a result of finding the default focus ring | ||
both aesthetically unpleasant and confusing to users | ||
when applied after a mouse or touch event and introduces accessibility problems. | ||
The latter inevitably creates considerably more of the kind of problem that the former was trying to solve. | ||
- Some native elements in some browsers, | ||
notably `<button>` in Chrome, | ||
have a "magic" focus style which does _not_ apply | ||
unless focus was received via a keyboard interaction. | ||
To deal with this: | ||
- It seems evident that a visual indication of what has focus | ||
is only interesting to a user who is using the keyboard | ||
to interact with the page. | ||
A user using any kind of pointing device | ||
would only be interested in what is in focus | ||
if they were _just about_ to use the keyboard - | ||
otherwise, it is irrelevant and potentially confusing. | ||
- Thus, if we only show the focus ring when relevant, | ||
we can avoid user confusion | ||
and avoid creating incentives for developers to disable it. | ||
- A mechanism for exposing focus ring styles | ||
only when the keyboard is the user's current input modality | ||
gives us this opportunity. | ||
## API Proposal | ||
```css | ||
/* override UA stylesheet if necessary */ | ||
:focus { | ||
outline-width: 0; | ||
} | ||
/* establish desired focus ring appearance for appropriate input modalities */ | ||
:focus-ring { | ||
outline: 2px solid blue; | ||
} | ||
``` | ||
`:focus-ring` matches native elements that are | ||
1. focussed; and | ||
2. would display a focus ring if only UA styles applied | ||
Additionally, `:focus-ring` matches non-native elements as if they were | ||
native button elements. | ||
### Note: Styling non-native elements which should always match `focus-ring` | ||
This is not currently part of the spec, | ||
but a mechanism is needed to explain the ability of native text fields | ||
to match `:focus-ring` regardless of how focus arrived on the element. | ||
Consider an author creating a custom element, `custom-texty-element`, | ||
which they believe should show a focus ring on mouse click. | ||
By default, the default `:focus-ring` user agent style | ||
will not show a focus ring when this element receives focus via mouse click. | ||
However, if the author were to style the element via `:focus`, | ||
they could not recreate the browser's default `outline` style reliably: | ||
```css | ||
custom-texty-element:focus { | ||
outline: ???; | ||
} | ||
``` | ||
Either of the following two new primitives would allow the author to | ||
show the default focus ring on click for this element: | ||
1. Add a new keyword value to the outline shorthand that represents whatever the default UA `::focus-ring` is. Then authors can do: | ||
custom-texty-element:focus { | ||
outline: platform-default-focus-outline-style-foo; | ||
} | ||
2. Add a new CSS property that controls "keyboard modality" vs non-"keyboard modality" behavior, e.g. | ||
custom-texty-element { | ||
show-focus-ring-foo: always | keyboard-only; | ||
} | ||
_("`-foo`" placeholder indicates that these names are by no means final!)_ | ||
While either of these primitives would suffice, | ||
having both would provide more flexibility for authors. | ||
## Example heuristic | ||
The heuristic used to decide the current modality should not be defined | ||
normatively. An example heuristic is to update modality on each style recalc: | ||
if the most recent user interaction was via the keyboard; | ||
and the key pressed was either `Tab` or `Shift + Tab`; | ||
then the modality is keyboard. Otherwise, | ||
the modality is not keyboard. | ||
## Implementation Prototype | ||
The tiny | ||
[focus-ring.js](http://wicg.github.io/focus-ring/src/focus-ring.js) | ||
provides a prototype intended to achieve the goals we are proposing | ||
with technology that exists today | ||
in order for developers to be able to try it out, understand it and provide feedback. | ||
It sets a `.js-focus-ring` class on the body element | ||
to provide a way to disable focus styles only when the polyfill is loaded. | ||
It also sets a `.focus-ring` class on the active element | ||
if the script determines that the keyboard is being used. | ||
This attribute is removed on any `blur` event. | ||
This allows authors to write rules | ||
which show a focus style only when it would be relevant to the user. | ||
Note that the prototype does not match the proposed API - | ||
it is intended to give developers a feel for the model | ||
rather than to provide a high-fidelity polyfill. | ||
### How it works | ||
The script uses two heuristics to determine whether the keyboard is being used: | ||
- a `focus` event immediately following a `keydown` event where the key pressed was either `Tab` | ||
or `Shift + Tab`. | ||
- a `focus` event immediately following a `keydown` event where the key pressed was either `Tab`, | ||
`Shift + Tab`, or an arrow key. | ||
- focus moves into an element which requires keyboard interaction, | ||
@@ -165,1 +75,13 @@ such as a text field | ||
this still needs to be implemented._ | ||
### Dependencies | ||
The `:focus-ring` polyfill uses the | ||
[Element.classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) API which is | ||
not supported in IE 8-9. In accordance with the W3C's new [Polyfill | ||
guidance](https://www.w3.org/2001/tag/doc/polyfills/#don-t-serve-unnecessary-polyfills), the | ||
`:focus-ring` polyfill does not bundle other polyfills. If you need to support these older browsers | ||
you should add the [classList polyfill](https://github.com/eligrey/classList.js/) to your page | ||
before loading the `:focus-ring` polyfill. Using a service like | ||
[Polyfill.io](https://polyfill.io/v2/docs/) will handle feature detecting and loading the necessary | ||
polyfills for you. |
@@ -5,9 +5,8 @@ import resolve from 'rollup-plugin-node-resolve'; | ||
export default { | ||
entry: 'src/focus-ring.js', | ||
format: 'umd', | ||
dest: 'dist/focus-ring.js', | ||
plugins: [ | ||
resolve({ jsnext: true, main: true }), | ||
commonjs() | ||
] | ||
input: 'src/focus-ring.js', | ||
output: { | ||
file: 'dist/focus-ring.js', | ||
format: 'umd' | ||
}, | ||
plugins: [resolve({ jsnext: true, main: true }), commonjs()] | ||
}; |
@@ -1,3 +0,1 @@ | ||
import classList from 'dom-classlist'; | ||
/** | ||
@@ -7,19 +5,19 @@ * https://github.com/WICG/focus-ring | ||
function init() { | ||
var hadKeyboardEvent = false; | ||
var hadKeyboardEvent = true; | ||
var elWithFocusRing; | ||
var inputTypesWhitelist = { | ||
'text': true, | ||
'search': true, | ||
'url': true, | ||
'tel': true, | ||
'email': true, | ||
'password': true, | ||
'number': true, | ||
'date': true, | ||
'month': true, | ||
'week': true, | ||
'time': true, | ||
'datetime': true, | ||
'datetime-local': true, | ||
text: true, | ||
search: true, | ||
url: true, | ||
tel: true, | ||
email: true, | ||
password: true, | ||
number: true, | ||
date: true, | ||
month: true, | ||
week: true, | ||
time: true, | ||
datetime: true, | ||
'datetime-local': true | ||
}; | ||
@@ -38,10 +36,13 @@ | ||
if (tagName == 'INPUT' && inputTypesWhitelist[type] && !el.readonly) | ||
if (tagName == 'INPUT' && inputTypesWhitelist[type] && !el.readonly) { | ||
return true; | ||
} | ||
if (tagName == 'TEXTAREA' && !el.readonly) | ||
if (tagName == 'TEXTAREA' && !el.readonly) { | ||
return true; | ||
} | ||
if (el.contentEditable == 'true') | ||
if (el.contentEditable == 'true') { | ||
return true; | ||
} | ||
@@ -57,5 +58,6 @@ return false; | ||
function addFocusRingClass(el) { | ||
if (classList(el).contains('focus-ring')) | ||
if (el.classList.contains('focus-ring')) { | ||
return; | ||
classList(el).add('focus-ring'); | ||
} | ||
el.classList.add('focus-ring'); | ||
el.setAttribute('data-focus-ring-added', ''); | ||
@@ -70,5 +72,6 @@ } | ||
function removeFocusRingClass(el) { | ||
if (!el.hasAttribute('data-focus-ring-added')) | ||
if (!el.hasAttribute('data-focus-ring-added')) { | ||
return; | ||
classList(el).remove('focus-ring'); | ||
} | ||
el.classList.remove('focus-ring'); | ||
el.removeAttribute('data-focus-ring-added'); | ||
@@ -79,11 +82,23 @@ } | ||
* On `keydown`, set `hadKeyboardEvent`, add `focus-ring` class if the | ||
* key was Tab. | ||
* key was Tab/Shift-Tab or Arrow Keys. | ||
* @param {Event} e | ||
*/ | ||
function onKeyDown(e) { | ||
if (e.altKey || e.ctrlKey || e.metaKey) | ||
const allowedKeys = [ | ||
9, // TAB | ||
37, // LEFT | ||
38, // UP | ||
39, // RIGHT | ||
40 // DOWN | ||
]; | ||
// Ignore keypresses if the user is holding down a modifier key. | ||
if (e.altKey || e.ctrlKey || e.metaKey) { | ||
return; | ||
} | ||
if (e.keyCode != 9) | ||
// Ignore keypresses which aren't related to keyboard navigation. | ||
if (allowedKeys.indexOf(e.keyCode) === -1) { | ||
return; | ||
} | ||
@@ -95,3 +110,3 @@ hadKeyboardEvent = true; | ||
* On `focus`, add the `focus-ring` class to the target if: | ||
* - the target received focus as a result of keyboard navigation | ||
* - the target received focus as a result of keyboard navigation, or | ||
* - the event target is an element that will likely require interaction | ||
@@ -102,4 +117,5 @@ * via the keyboard (e.g. a text box) | ||
function onFocus(e) { | ||
if (e.target == document) | ||
if (e.target == document) { | ||
return; | ||
} | ||
@@ -117,4 +133,5 @@ if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) { | ||
function onBlur(e) { | ||
if (e.target == document) | ||
if (e.target == document) { | ||
return; | ||
} | ||
@@ -129,8 +146,6 @@ removeFocusRingClass(e.target); | ||
function onWindowFocus() { | ||
// When removing the activeElement from DOM it's possible IE11 is in state | ||
// document.activeElement === null | ||
if (!document.activeElement) | ||
return; | ||
if (document.activeElement == elWithFocusRing) | ||
window.removeEventListener('focus', onWindowFocus, true); | ||
if (document.activeElement == elWithFocusRing) { | ||
addFocusRingClass(elWithFocusRing); | ||
} | ||
@@ -143,12 +158,15 @@ elWithFocusRing = null; | ||
* focus-ring class. | ||
* @param {Event} e | ||
*/ | ||
function onWindowBlur() { | ||
// When removing the activeElement from DOM it's possible IE11 is in state | ||
// document.activeElement === null | ||
if (!document.activeElement) | ||
function onWindowBlur(e) { | ||
if (e.target !== window) { | ||
return; | ||
if (classList(document.activeElement).contains('focus-ring')) { | ||
// Keep a reference to the element to which the focus-ring class is applied | ||
// so the focus-ring class can be restored to it if the window regains | ||
// focus after being blurred. | ||
} | ||
window.addEventListener('focus', onWindowFocus, true); | ||
addInitialPointerMoveListeners(); | ||
if (document.activeElement.classList.contains('focus-ring')) { | ||
// Keep a reference to the element to which the focus-ring class is | ||
// applied so the focus-ring class can be restored to it if the window | ||
// regains focus after being blurred. | ||
elWithFocusRing = document.activeElement; | ||
@@ -158,2 +176,43 @@ } | ||
/** | ||
* Add a group of listeners to detect usage of any pointing devices. | ||
* These listeners will be added when the polyfill first loads, and anytime | ||
* the window is blurred, so that they are active when the window regains focus. | ||
*/ | ||
function addInitialPointerMoveListeners() { | ||
document.addEventListener('mousemove', onInitialPointerMove); | ||
document.addEventListener('mousedown', onInitialPointerMove); | ||
document.addEventListener('mouseup', onInitialPointerMove); | ||
document.addEventListener('pointermove', onInitialPointerMove); | ||
document.addEventListener('pointerdown', onInitialPointerMove); | ||
document.addEventListener('pointerup', onInitialPointerMove); | ||
document.addEventListener('touchmove', onInitialPointerMove); | ||
document.addEventListener('touchstart', onInitialPointerMove); | ||
document.addEventListener('touchend', onInitialPointerMove); | ||
} | ||
/** | ||
* When the polfyill first loads, assume the user is in keyboard modality. | ||
* If any event is received from a pointing device (e.g mouse, pointer, touch), turn off | ||
* keyboard modality. | ||
* This accounts for situations where focus enters the page from the URL bar. | ||
* @param {Event} e | ||
*/ | ||
function onInitialPointerMove(e) { | ||
// Work around a Safari quirk that fires a mousemove on <html> whenever the | ||
// window blurs, even if you're tabbing out of the page. ¯\_(ツ)_/¯ | ||
if (e.target.nodeName.toLowerCase() === 'html') return; | ||
hadKeyboardEvent = false; | ||
document.removeEventListener('mousemove', onInitialPointerMove); | ||
document.removeEventListener('mousedown', onInitialPointerMove); | ||
document.removeEventListener('mouseup', onInitialPointerMove); | ||
document.removeEventListener('pointermove', onInitialPointerMove); | ||
document.removeEventListener('pointerdown', onInitialPointerMove); | ||
document.removeEventListener('pointerup', onInitialPointerMove); | ||
document.removeEventListener('touchmove', onInitialPointerMove); | ||
document.removeEventListener('touchstart', onInitialPointerMove); | ||
document.removeEventListener('touchend', onInitialPointerMove); | ||
} | ||
document.addEventListener('keydown', onKeyDown, true); | ||
@@ -164,4 +223,5 @@ document.addEventListener('focus', onFocus, true); | ||
window.addEventListener('blur', onWindowBlur, true); | ||
addInitialPointerMoveListeners(); | ||
classList(document.body).add('js-focus-ring'); | ||
document.body.classList.add('js-focus-ring'); | ||
} | ||
@@ -168,0 +228,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
0
58
966
58608
18
2
83
1
- Removeddom-classlist@^1.0.1
- Removeddom-classlist@1.0.1(transitive)