这是indexloc提供的服务,不要输入任何密码
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,4 @@ The script uses two heuristics to determine whether the keyboard is being used:
or `Shift + Tab`.
- focus moves into an element which requires keyboard interaction,
such as a text field
- _TODO: ideally, we also trigger keyboard modality
following a keyboard event which activates an element or causes a mutation;
this still needs to be implemented._
- We also trigger keyboard modality following a keyboard event which activates an element or causes a mutation. The following keys that typically result in a change of context or focus are handled: `Backspace`, `Enter`, `Esc`, `Space`, `PageUp`, `PageDown`, `End`, `Home`, `ArrowLeft`, `ArrowUp`, `ArrowRight`, `ArrowDown`, or `Delete`.
217 changes: 195 additions & 22 deletions dist/focus-ring.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* @return {ClassList}
*/

var index = function (el) {
var domClasslist = function (el) {
return new ClassList(el);
};

Expand Down Expand Up @@ -170,11 +170,47 @@ ClassList.prototype.toggle = function (token, force) {
return (typeof force == 'boolean' ? force : !hasToken);
};

// element-closest | CC0-1.0 | github.com/jonathantneal/closest

(function (ElementProto) {
if (typeof ElementProto.matches !== 'function') {
ElementProto.matches = ElementProto.msMatchesSelector || ElementProto.mozMatchesSelector || ElementProto.webkitMatchesSelector || function matches(selector) {
var element = this;
var elements = (element.document || element.ownerDocument).querySelectorAll(selector);
var index = 0;

while (elements[index] && elements[index] !== element) {
++index;
}

return Boolean(elements[index]);
};
}

if (typeof ElementProto.closest !== 'function') {
ElementProto.closest = function closest(selector) {
var element = this;

while (element && element.nodeType === 1) {
if (element.matches(selector)) {
return element;
}

element = element.parentNode;
}

return null;
};
}
})(window.Element.prototype);

/**
* https://github.com/WICG/focus-ring
*/
function init() {
var hadKeyboardEvent = false;
var elWithFocusRing;
var elementsWithFocusRing = document.getElementsByClassName('focus-ring');

var inputTypesWhitelist = {
'radio': true,
Expand All @@ -197,6 +233,121 @@ function init() {
'datetime-local': true,
};

// keys that often produce a change of context or focus
var navigationKeys = [
8 /* Backspace */,
9 /* Tab */,
13 /* Enter */,
27 /* Esc */,
32 /* Space */,
33 /* PageUp */,
34 /* PageDown */,
35 /* End */,
36 /* Home */,
37 /* ArrowLeft */,
38 /* ArrowUp */,
39 /* ArrowRight */,
40 /* ArrowDown */,
46/* Delete */,
];

var behavior = {
incrementable: {
inputType: {
'checkbox': true,
'radio': true,
'range': true,
},
role: {
'button': true,
'checkbox': true,
'columnheading': true,
'gridcell': true,
'menuitem': true,
'menuitemcheckbox': true,
'menuitemradio': true,
'option': true,
'radio': true,
'row': true,
'rowheading': true,
'slider': true,
'tab': true,
'treeitem': true,
},
},
selectable: {
inputType: {
'checkbox': true,
'radio': true,
},
role: {
'button': true,
'checkbox': true,
'columnheading': true,
'gridcell': true,
'menuitemcheckbox': true,
'menuitemradio': true,
'option': true,
'radio': true,
'row': true,
'rowheading': true,
'treeitem': true,
},
},
deletable: {
inputType: {
},
role: {
'option': true,
'row': true,
'tab': true,
'treeitem': true,
},
},
};

/**
* Computes whether keyboard event should be treated as initiating focus navigation.
* @param {Event} e
* @return {boolean}
*/
function handleEventAsNavigation(e) {
if (e.altKey || e.ctrlKey || e.metaKey)
return false;

var index = navigationKeys.indexOf(e.keyCode);
var tagName = e.target.tagName;
var inputType = tagName === 'INPUT' ? e.target.type : undefined;
var ariaRole = e.target.getAttribute('role');

// If key is not generally considered navigation, don't handle it as such.
if (index === -1)
return false;

// ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home/End, PageUp/PageDown
if (e.keyCode > 32 && e.keyCode < 41)
// Return true if target is an input or has a role and is whitelisted as 'incrementable'.
return (inputType && behavior.incrementable.inputType[inputType])
|| (ariaRole && behavior.incrementable.role[ariaRole]);

// Enter or Space
if (e.keyCode == 13 || e.keyCode == 32)
// Return true if target is an input or has a role and is whitelisted as 'selectable'.
return (inputType && behavior.selectable.inputType[inputType])
|| (ariaRole && behavior.selectable.role[ariaRole]);

// Esc key when target is a descendant of a dialog or menu.
if (e.keyCode == 27)
return event.target.closest('[role$="dialog"],[role="menu"]') !== null;

// Backspace or Delete
if (e.keyCode == 8 || e.keyCode == 46)
// Return true if target has a role and is whitelisted as 'deletable'.
return ariaRole && behavior.deletable.role[ariaRole];

return e.keyCode == 9;
}

/**
* Computes whether the given element should automatically trigger the
* `focus-ring` class being added, i.e. whether it should always match
Expand Down Expand Up @@ -226,9 +377,9 @@ function init() {
* @param {Element} el
*/
function addFocusRingClass(el) {
if (index(el).contains('focus-ring'))
if (domClasslist(el).contains('focus-ring'))
return;
index(el).add('focus-ring');
domClasslist(el).add('focus-ring');
el.setAttribute('data-focus-ring-added', '');
}

Expand All @@ -240,32 +391,52 @@ function init() {
function removeFocusRingClass(el) {
if (!el.hasAttribute('data-focus-ring-added'))
return;
index(el).remove('focus-ring');
domClasslist(el).remove('focus-ring');
el.removeAttribute('data-focus-ring-added');
}

/**
* On `keyup` add `focus-ring` class if the user pressed Tab and the event
* target is an element that will likely require interaction via the
* keyboard (e.g. a text box).
* The `keyup` event is used over the focus event because:
* 1. `focus` is a device-independent event, and `keyup` ensures the
* `focus-ring` class is only added when focus originates from
* keyboard navigation.
* 2. Unlike `focus`, keyup` will fire when the user navigates from the
* browser chrome into the document. (For more, see issue #15)
* On `keydown`, set `hadKeyboardEvent`, add `focus-ring` class if the
* key was Tab or another navigation key.
* @param {Event} e
*/
function onKeyUp(e) {
if (e.altKey || e.ctrlKey || e.metaKey)
function onKeyDown(e) {
if (!handleEventAsNavigation(e))
return;

if (e.keyCode != 9)
hadKeyboardEvent = true;
}

/**
* On `mousedown`, unset `hadKeyboardEvent`, remove `focus-ring` class from elements where not
* originally added by the author.
* @param {Event} e
*/
function onMouseDown(e) {
hadKeyboardEvent = false;
if (elementsWithFocusRing.length) {
for(var i = 0; i < elementsWithFocusRing.length; i++) {
if (!focusTriggersKeyboardModality(elementsWithFocusRing[i])) {
removeFocusRingClass(elementsWithFocusRing[i]);
}
}
}
}

/**
* On `focus`, add the `focus-ring` class to the target if:
* - the target received focus as a result of keyboard navigation
* - the event target is an element that will likely require interaction
* via the keyboard (e.g. a text box)
* @param {Event} e
*/
function onFocus(e) {
if (e.target == document)
return;

var target = e.target;
if (focusTriggersKeyboardModality(target)) {
addFocusRingClass(target);
if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {
addFocusRingClass(e.target);
hadKeyboardEvent = false;
}
}

Expand Down Expand Up @@ -304,20 +475,22 @@ function init() {
// document.activeElement === null
if (!document.activeElement)
return;
if (index(document.activeElement).contains('focus-ring')) {
if (domClasslist(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.
elWithFocusRing = document.activeElement;
}
}

document.addEventListener('keyup', onKeyUp, true);
document.addEventListener('keydown', onKeyDown, true);
document.addEventListener('mousedown', onMouseDown, true);
document.addEventListener('focus', onFocus, true);
document.addEventListener('blur', onBlur, true);
window.addEventListener('focus', onWindowFocus, true);
window.addEventListener('blur', onWindowBlur, true);

index(document.body).add('js-focus-ring');
domClasslist(document.body).add('js-focus-ring');
}

/**
Expand Down
Loading