arrow-key-navigation
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -11,33 +11,28 @@ 'use strict'; | ||
/* global document, addEventListener, removeEventListener, getSelection */ | ||
// This query is adapted from a11y-dialog | ||
// https://github.com/edenspiekermann/a11y-dialog/blob/cf4ed81/a11y-dialog.js#L6-L18 | ||
var focusablesQuery = 'a[href], area[href], input, select, textarea, ' + 'button, iframe, object, embed, [contenteditable], [tabindex], ' + 'video[controls], audio[controls], summary'; // TODO: email/number types are a special type, in that they return selectionStart/selectionEnd as null | ||
// TODO: email/number types are a special type, in that they return selectionStart/selectionEnd as null | ||
// As far as I can tell, there is no way to actually get the caret position from these inputs. So we | ||
// don't do the proper caret handling for those inputs, unfortunately. | ||
// https://html.spec.whatwg.org/multipage/input.html#do-not-apply | ||
var textInputTypes = ['text', 'search', 'url', 'password', 'tel']; | ||
var checkboxRadioInputTypes = ['checkbox', 'radio']; | ||
var focusTrapTest = undefined; | ||
var focusTrapTest = undefined; // This query is adapted from a11y-dialog | ||
// https://github.com/edenspiekermann/a11y-dialog/blob/cf4ed81/a11y-dialog.js#L6-L18 | ||
function getFocusableElements(activeElement) { | ||
// Respect focus trap inside of dialogs | ||
var dialogParent = getFocusTrapParent(activeElement); | ||
var root = dialogParent || document; | ||
var res = []; | ||
var elements = root.querySelectorAll(focusablesQuery); | ||
var len = elements.length; | ||
var focusablesQuery = 'a[href], area[href], input, select, textarea, ' + 'button, iframe, object, embed, [contenteditable], [tabindex], ' + 'video[controls], audio[controls], summary'; | ||
for (var i = 0; i < len; i++) { | ||
var element = elements[i]; | ||
function getActiveElement() { | ||
var activeElement = document.activeElement; | ||
if (element === activeElement || !element.disabled && !/^-/.test(element.getAttribute('tabindex') || '') && !element.hasAttribute('inert') && ( // see https://github.com/GoogleChrome/inert-polyfill | ||
element.offsetWidth > 0 || element.offsetHeight > 0)) { | ||
res.push(element); | ||
} | ||
while (activeElement.shadowRoot) { | ||
activeElement = activeElement.shadowRoot.activeElement; | ||
} | ||
return res; | ||
return activeElement; | ||
} | ||
function isFocusable(element) { | ||
return element.matches(focusablesQuery) && !element.disabled && !/^-/.test(element.getAttribute('tabindex') || '') && !element.hasAttribute('inert') && ( // see https://github.com/GoogleChrome/inert-polyfill | ||
element.offsetWidth > 0 || element.offsetHeight > 0); | ||
} | ||
function getFocusTrapParent(element) { | ||
@@ -59,3 +54,3 @@ if (!focusTrapTest) { | ||
function shouldIgnoreEvent(activeElement, key) { | ||
function shouldIgnoreEvent(activeElement, forwardDirection) { | ||
var tagName = activeElement.tagName; | ||
@@ -87,5 +82,5 @@ var isTextarea = tagName === 'TEXTAREA'; | ||
if (key === 'ArrowLeft' && selectionStart === selectionEnd && selectionStart === 0) { | ||
if (!forwardDirection && selectionStart === selectionEnd && selectionStart === 0) { | ||
return false; | ||
} else if (key === 'ArrowRight' && selectionStart === selectionEnd && selectionStart === len) { | ||
} else if (forwardDirection && selectionStart === selectionEnd && selectionStart === len) { | ||
return false; | ||
@@ -97,31 +92,94 @@ } | ||
function focusNextOrPrevious(event, key) { | ||
var activeElement = document.activeElement; | ||
function getNextCandidateNodeForShadowDomPolyfill(root, targetElement, forwardDirection, filter) { | ||
// When the shadydom polyfill is running, we can't use TreeWalker on ShadowRoots because | ||
// they aren't real Nodes. So we do this workaround where we run TreeWalker on the | ||
// children instead. | ||
var nodes = Array.prototype.slice.call(root.querySelectorAll('*')); | ||
var idx = nodes.indexOf(targetElement); | ||
if (shouldIgnoreEvent(activeElement, key)) { | ||
return; | ||
if (forwardDirection) { | ||
nodes = nodes.slice(idx + 1); | ||
} else { | ||
if (idx === -1) { | ||
idx = nodes.length; | ||
} | ||
nodes = nodes.slice(0, idx); | ||
nodes.reverse(); | ||
} | ||
var focusableElements = getFocusableElements(activeElement); | ||
for (var i = 0; i < nodes.length; i++) { | ||
var node = nodes[i]; | ||
if (!focusableElements.length) { | ||
return; | ||
if (node instanceof HTMLElement && filter.acceptNode(node) === NodeFilter.FILTER_ACCEPT) { | ||
return node; | ||
} | ||
} | ||
var index = focusableElements.indexOf(activeElement); | ||
var element; | ||
return undefined; | ||
} | ||
if (key === 'ArrowLeft') { | ||
element = focusableElements[index - 1] || focusableElements[0]; | ||
} else { | ||
// ArrowRight | ||
element = focusableElements[index + 1] || focusableElements[focusableElements.length - 1]; | ||
function getNextCandidateNode(root, targetElement, forwardDirection, filter) { | ||
var walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter); | ||
if (targetElement) { | ||
walker.currentNode = targetElement; | ||
} | ||
element.focus(); | ||
event.preventDefault(); | ||
if (forwardDirection) { | ||
return walker.nextNode(); | ||
} else if (targetElement) { | ||
return walker.previousNode(); | ||
} // iterating backwards through shadow root, use last child | ||
return walker.lastChild(); | ||
} | ||
function isShadowDomPolyfill() { | ||
return typeof ShadowRoot !== 'undefined' && ( // ShadowRoot.polyfill is just a hack for our unit tests | ||
'polyfill' in ShadowRoot || !ShadowRoot.toString().includes('[native code]')); | ||
} | ||
function getNextNode(root, targetElement, forwardDirection) { | ||
var filter = { | ||
acceptNode: function (node) { | ||
return node === targetElement || node.shadowRoot || isFocusable(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; | ||
} | ||
}; // TODO: remove this when we don't need to support the Shadow DOM polyfill | ||
var nextNode = isShadowDomPolyfill() && root instanceof ShadowRoot ? getNextCandidateNodeForShadowDomPolyfill(root, targetElement, forwardDirection, filter) : getNextCandidateNode(root, targetElement, forwardDirection, filter); | ||
if (nextNode && nextNode.shadowRoot) { | ||
// push into the shadow DOM | ||
return getNextNode(nextNode.shadowRoot, null, forwardDirection); | ||
} | ||
if (!nextNode && root.host) { | ||
// pop out of the shadow DOM | ||
return getNextNode(root.host.getRootNode(), root.host, forwardDirection); | ||
} | ||
return nextNode; | ||
} | ||
function focusNextOrPrevious(event, key) { | ||
var activeElement = getActiveElement(); | ||
var forwardDirection = key === 'ArrowRight'; | ||
if (shouldIgnoreEvent(activeElement, forwardDirection)) { | ||
return; | ||
} | ||
var root = getFocusTrapParent(activeElement) || activeElement.getRootNode(); | ||
var nextNode = getNextNode(root, activeElement, forwardDirection); | ||
if (nextNode && nextNode !== activeElement) { | ||
nextNode.focus(); | ||
event.preventDefault(); | ||
} | ||
} | ||
function handleEnter(event) { | ||
var activeElement = document.activeElement; | ||
var activeElement = getActiveElement(); | ||
@@ -128,0 +186,0 @@ if (activeElement.tagName === 'INPUT' && checkboxRadioInputTypes.indexOf(activeElement.getAttribute('type').toLowerCase()) !== -1) { |
@@ -6,7 +6,2 @@ /** | ||
/* global document, addEventListener, removeEventListener, getSelection */ | ||
// This query is adapted from a11y-dialog | ||
// https://github.com/edenspiekermann/a11y-dialog/blob/cf4ed81/a11y-dialog.js#L6-L18 | ||
var focusablesQuery = 'a[href], area[href], input, select, textarea, ' + | ||
'button, iframe, object, embed, [contenteditable], [tabindex], ' + | ||
'video[controls], audio[controls], summary'; | ||
// TODO: email/number types are a special type, in that they return selectionStart/selectionEnd as null | ||
@@ -19,20 +14,21 @@ // As far as I can tell, there is no way to actually get the caret position from these inputs. So we | ||
var focusTrapTest = undefined; | ||
function getFocusableElements(activeElement) { | ||
// Respect focus trap inside of dialogs | ||
var dialogParent = getFocusTrapParent(activeElement); | ||
var root = dialogParent || document; | ||
var res = []; | ||
var elements = root.querySelectorAll(focusablesQuery); | ||
var len = elements.length; | ||
for (var i = 0; i < len; i++) { | ||
var element = elements[i]; | ||
if (element === activeElement || (!element.disabled && | ||
!/^-/.test(element.getAttribute('tabindex') || '') && | ||
!element.hasAttribute('inert') && // see https://github.com/GoogleChrome/inert-polyfill | ||
(element.offsetWidth > 0 || element.offsetHeight > 0))) { | ||
res.push(element); | ||
} | ||
// This query is adapted from a11y-dialog | ||
// https://github.com/edenspiekermann/a11y-dialog/blob/cf4ed81/a11y-dialog.js#L6-L18 | ||
var focusablesQuery = 'a[href], area[href], input, select, textarea, ' + | ||
'button, iframe, object, embed, [contenteditable], [tabindex], ' + | ||
'video[controls], audio[controls], summary'; | ||
function getActiveElement() { | ||
var activeElement = document.activeElement; | ||
while (activeElement.shadowRoot) { | ||
activeElement = activeElement.shadowRoot.activeElement; | ||
} | ||
return res; | ||
return activeElement; | ||
} | ||
function isFocusable(element) { | ||
return element.matches(focusablesQuery) && | ||
!element.disabled && | ||
!/^-/.test(element.getAttribute('tabindex') || '') && | ||
!element.hasAttribute('inert') && // see https://github.com/GoogleChrome/inert-polyfill | ||
(element.offsetWidth > 0 || element.offsetHeight > 0); | ||
} | ||
function getFocusTrapParent(element) { | ||
@@ -50,3 +46,3 @@ if (!focusTrapTest) { | ||
} | ||
function shouldIgnoreEvent(activeElement, key) { | ||
function shouldIgnoreEvent(activeElement, forwardDirection) { | ||
var tagName = activeElement.tagName; | ||
@@ -76,6 +72,6 @@ var isTextarea = tagName === 'TEXTAREA'; | ||
// unless the cursor is at the beginning or the end | ||
if (key === 'ArrowLeft' && selectionStart === selectionEnd && selectionStart === 0) { | ||
if (!forwardDirection && selectionStart === selectionEnd && selectionStart === 0) { | ||
return false; | ||
} | ||
else if (key === 'ArrowRight' && selectionStart === selectionEnd && selectionStart === len) { | ||
else if (forwardDirection && selectionStart === selectionEnd && selectionStart === len) { | ||
return false; | ||
@@ -85,24 +81,80 @@ } | ||
} | ||
function focusNextOrPrevious(event, key) { | ||
var activeElement = document.activeElement; | ||
if (shouldIgnoreEvent(activeElement, key)) { | ||
return; | ||
function getNextCandidateNodeForShadowDomPolyfill(root, targetElement, forwardDirection, filter) { | ||
// When the shadydom polyfill is running, we can't use TreeWalker on ShadowRoots because | ||
// they aren't real Nodes. So we do this workaround where we run TreeWalker on the | ||
// children instead. | ||
var nodes = Array.prototype.slice.call(root.querySelectorAll('*')); | ||
var idx = nodes.indexOf(targetElement); | ||
if (forwardDirection) { | ||
nodes = nodes.slice(idx + 1); | ||
} | ||
var focusableElements = getFocusableElements(activeElement); | ||
if (!focusableElements.length) { | ||
return; | ||
else { | ||
if (idx === -1) { | ||
idx = nodes.length; | ||
} | ||
nodes = nodes.slice(0, idx); | ||
nodes.reverse(); | ||
} | ||
var index = focusableElements.indexOf(activeElement); | ||
var element; | ||
if (key === 'ArrowLeft') { | ||
element = focusableElements[index - 1] || focusableElements[0]; | ||
for (var i = 0; i < nodes.length; i++) { | ||
var node = nodes[i]; | ||
if (node instanceof HTMLElement && filter.acceptNode(node) === NodeFilter.FILTER_ACCEPT) { | ||
return node; | ||
} | ||
} | ||
else { // ArrowRight | ||
element = focusableElements[index + 1] || focusableElements[focusableElements.length - 1]; | ||
return undefined; | ||
} | ||
function getNextCandidateNode(root, targetElement, forwardDirection, filter) { | ||
var walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter); | ||
if (targetElement) { | ||
walker.currentNode = targetElement; | ||
} | ||
element.focus(); | ||
event.preventDefault(); | ||
if (forwardDirection) { | ||
return walker.nextNode(); | ||
} | ||
else if (targetElement) { | ||
return walker.previousNode(); | ||
} | ||
// iterating backwards through shadow root, use last child | ||
return walker.lastChild(); | ||
} | ||
function isShadowDomPolyfill() { | ||
return typeof ShadowRoot !== 'undefined' && | ||
// ShadowRoot.polyfill is just a hack for our unit tests | ||
('polyfill' in ShadowRoot || !ShadowRoot.toString().includes('[native code]')); | ||
} | ||
function getNextNode(root, targetElement, forwardDirection) { | ||
var filter = { | ||
acceptNode: function (node) { | ||
return (node === targetElement || node.shadowRoot || isFocusable(node)) | ||
? NodeFilter.FILTER_ACCEPT | ||
: NodeFilter.FILTER_SKIP; | ||
} | ||
}; | ||
// TODO: remove this when we don't need to support the Shadow DOM polyfill | ||
var nextNode = isShadowDomPolyfill() && root instanceof ShadowRoot | ||
? getNextCandidateNodeForShadowDomPolyfill(root, targetElement, forwardDirection, filter) | ||
: getNextCandidateNode(root, targetElement, forwardDirection, filter); | ||
if (nextNode && nextNode.shadowRoot) { // push into the shadow DOM | ||
return getNextNode(nextNode.shadowRoot, null, forwardDirection); | ||
} | ||
if (!nextNode && root.host) { // pop out of the shadow DOM | ||
return getNextNode(root.host.getRootNode(), root.host, forwardDirection); | ||
} | ||
return nextNode; | ||
} | ||
function focusNextOrPrevious(event, key) { | ||
var activeElement = getActiveElement(); | ||
var forwardDirection = key === 'ArrowRight'; | ||
if (shouldIgnoreEvent(activeElement, forwardDirection)) { | ||
return; | ||
} | ||
var root = getFocusTrapParent(activeElement) || activeElement.getRootNode(); | ||
var nextNode = getNextNode(root, activeElement, forwardDirection); | ||
if (nextNode && nextNode !== activeElement) { | ||
nextNode.focus(); | ||
event.preventDefault(); | ||
} | ||
} | ||
function handleEnter(event) { | ||
var activeElement = document.activeElement; | ||
var activeElement = getActiveElement(); | ||
if (activeElement.tagName === 'INPUT' && | ||
@@ -109,0 +161,0 @@ checkboxRadioInputTypes.indexOf(activeElement.getAttribute('type').toLowerCase()) !== -1) { |
@@ -13,33 +13,28 @@ (function (global, factory) { | ||
/* global document, addEventListener, removeEventListener, getSelection */ | ||
// This query is adapted from a11y-dialog | ||
// https://github.com/edenspiekermann/a11y-dialog/blob/cf4ed81/a11y-dialog.js#L6-L18 | ||
var focusablesQuery = 'a[href], area[href], input, select, textarea, ' + 'button, iframe, object, embed, [contenteditable], [tabindex], ' + 'video[controls], audio[controls], summary'; // TODO: email/number types are a special type, in that they return selectionStart/selectionEnd as null | ||
// TODO: email/number types are a special type, in that they return selectionStart/selectionEnd as null | ||
// As far as I can tell, there is no way to actually get the caret position from these inputs. So we | ||
// don't do the proper caret handling for those inputs, unfortunately. | ||
// https://html.spec.whatwg.org/multipage/input.html#do-not-apply | ||
var textInputTypes = ['text', 'search', 'url', 'password', 'tel']; | ||
var checkboxRadioInputTypes = ['checkbox', 'radio']; | ||
var focusTrapTest = undefined; | ||
var focusTrapTest = undefined; // This query is adapted from a11y-dialog | ||
// https://github.com/edenspiekermann/a11y-dialog/blob/cf4ed81/a11y-dialog.js#L6-L18 | ||
function getFocusableElements(activeElement) { | ||
// Respect focus trap inside of dialogs | ||
var dialogParent = getFocusTrapParent(activeElement); | ||
var root = dialogParent || document; | ||
var res = []; | ||
var elements = root.querySelectorAll(focusablesQuery); | ||
var len = elements.length; | ||
var focusablesQuery = 'a[href], area[href], input, select, textarea, ' + 'button, iframe, object, embed, [contenteditable], [tabindex], ' + 'video[controls], audio[controls], summary'; | ||
for (var i = 0; i < len; i++) { | ||
var element = elements[i]; | ||
function getActiveElement() { | ||
var activeElement = document.activeElement; | ||
if (element === activeElement || !element.disabled && !/^-/.test(element.getAttribute('tabindex') || '') && !element.hasAttribute('inert') && ( // see https://github.com/GoogleChrome/inert-polyfill | ||
element.offsetWidth > 0 || element.offsetHeight > 0)) { | ||
res.push(element); | ||
} | ||
while (activeElement.shadowRoot) { | ||
activeElement = activeElement.shadowRoot.activeElement; | ||
} | ||
return res; | ||
return activeElement; | ||
} | ||
function isFocusable(element) { | ||
return element.matches(focusablesQuery) && !element.disabled && !/^-/.test(element.getAttribute('tabindex') || '') && !element.hasAttribute('inert') && ( // see https://github.com/GoogleChrome/inert-polyfill | ||
element.offsetWidth > 0 || element.offsetHeight > 0); | ||
} | ||
function getFocusTrapParent(element) { | ||
@@ -61,3 +56,3 @@ if (!focusTrapTest) { | ||
function shouldIgnoreEvent(activeElement, key) { | ||
function shouldIgnoreEvent(activeElement, forwardDirection) { | ||
var tagName = activeElement.tagName; | ||
@@ -89,5 +84,5 @@ var isTextarea = tagName === 'TEXTAREA'; | ||
if (key === 'ArrowLeft' && selectionStart === selectionEnd && selectionStart === 0) { | ||
if (!forwardDirection && selectionStart === selectionEnd && selectionStart === 0) { | ||
return false; | ||
} else if (key === 'ArrowRight' && selectionStart === selectionEnd && selectionStart === len) { | ||
} else if (forwardDirection && selectionStart === selectionEnd && selectionStart === len) { | ||
return false; | ||
@@ -99,31 +94,94 @@ } | ||
function focusNextOrPrevious(event, key) { | ||
var activeElement = document.activeElement; | ||
function getNextCandidateNodeForShadowDomPolyfill(root, targetElement, forwardDirection, filter) { | ||
// When the shadydom polyfill is running, we can't use TreeWalker on ShadowRoots because | ||
// they aren't real Nodes. So we do this workaround where we run TreeWalker on the | ||
// children instead. | ||
var nodes = Array.prototype.slice.call(root.querySelectorAll('*')); | ||
var idx = nodes.indexOf(targetElement); | ||
if (shouldIgnoreEvent(activeElement, key)) { | ||
return; | ||
if (forwardDirection) { | ||
nodes = nodes.slice(idx + 1); | ||
} else { | ||
if (idx === -1) { | ||
idx = nodes.length; | ||
} | ||
nodes = nodes.slice(0, idx); | ||
nodes.reverse(); | ||
} | ||
var focusableElements = getFocusableElements(activeElement); | ||
for (var i = 0; i < nodes.length; i++) { | ||
var node = nodes[i]; | ||
if (!focusableElements.length) { | ||
return; | ||
if (node instanceof HTMLElement && filter.acceptNode(node) === NodeFilter.FILTER_ACCEPT) { | ||
return node; | ||
} | ||
} | ||
var index = focusableElements.indexOf(activeElement); | ||
var element; | ||
return undefined; | ||
} | ||
if (key === 'ArrowLeft') { | ||
element = focusableElements[index - 1] || focusableElements[0]; | ||
} else { | ||
// ArrowRight | ||
element = focusableElements[index + 1] || focusableElements[focusableElements.length - 1]; | ||
function getNextCandidateNode(root, targetElement, forwardDirection, filter) { | ||
var walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter); | ||
if (targetElement) { | ||
walker.currentNode = targetElement; | ||
} | ||
element.focus(); | ||
event.preventDefault(); | ||
if (forwardDirection) { | ||
return walker.nextNode(); | ||
} else if (targetElement) { | ||
return walker.previousNode(); | ||
} // iterating backwards through shadow root, use last child | ||
return walker.lastChild(); | ||
} | ||
function isShadowDomPolyfill() { | ||
return typeof ShadowRoot !== 'undefined' && ( // ShadowRoot.polyfill is just a hack for our unit tests | ||
'polyfill' in ShadowRoot || !ShadowRoot.toString().includes('[native code]')); | ||
} | ||
function getNextNode(root, targetElement, forwardDirection) { | ||
var filter = { | ||
acceptNode: function acceptNode(node) { | ||
return node === targetElement || node.shadowRoot || isFocusable(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; | ||
} | ||
}; // TODO: remove this when we don't need to support the Shadow DOM polyfill | ||
var nextNode = isShadowDomPolyfill() && root instanceof ShadowRoot ? getNextCandidateNodeForShadowDomPolyfill(root, targetElement, forwardDirection, filter) : getNextCandidateNode(root, targetElement, forwardDirection, filter); | ||
if (nextNode && nextNode.shadowRoot) { | ||
// push into the shadow DOM | ||
return getNextNode(nextNode.shadowRoot, null, forwardDirection); | ||
} | ||
if (!nextNode && root.host) { | ||
// pop out of the shadow DOM | ||
return getNextNode(root.host.getRootNode(), root.host, forwardDirection); | ||
} | ||
return nextNode; | ||
} | ||
function focusNextOrPrevious(event, key) { | ||
var activeElement = getActiveElement(); | ||
var forwardDirection = key === 'ArrowRight'; | ||
if (shouldIgnoreEvent(activeElement, forwardDirection)) { | ||
return; | ||
} | ||
var root = getFocusTrapParent(activeElement) || activeElement.getRootNode(); | ||
var nextNode = getNextNode(root, activeElement, forwardDirection); | ||
if (nextNode && nextNode !== activeElement) { | ||
nextNode.focus(); | ||
event.preventDefault(); | ||
} | ||
} | ||
function handleEnter(event) { | ||
var activeElement = document.activeElement; | ||
var activeElement = getActiveElement(); | ||
@@ -130,0 +188,0 @@ if (activeElement.tagName === 'INPUT' && checkboxRadioInputTypes.indexOf(activeElement.getAttribute('type').toLowerCase()) !== -1) { |
@@ -6,7 +6,2 @@ /** | ||
/* global document, addEventListener, removeEventListener, getSelection */ | ||
// This query is adapted from a11y-dialog | ||
// https://github.com/edenspiekermann/a11y-dialog/blob/cf4ed81/a11y-dialog.js#L6-L18 | ||
var focusablesQuery = 'a[href], area[href], input, select, textarea, ' + | ||
'button, iframe, object, embed, [contenteditable], [tabindex], ' + | ||
'video[controls], audio[controls], summary'; | ||
// TODO: email/number types are a special type, in that they return selectionStart/selectionEnd as null | ||
@@ -19,20 +14,21 @@ // As far as I can tell, there is no way to actually get the caret position from these inputs. So we | ||
var focusTrapTest = undefined; | ||
function getFocusableElements(activeElement) { | ||
// Respect focus trap inside of dialogs | ||
var dialogParent = getFocusTrapParent(activeElement); | ||
var root = dialogParent || document; | ||
var res = []; | ||
var elements = root.querySelectorAll(focusablesQuery); | ||
var len = elements.length; | ||
for (var i = 0; i < len; i++) { | ||
var element = elements[i]; | ||
if (element === activeElement || (!element.disabled && | ||
!/^-/.test(element.getAttribute('tabindex') || '') && | ||
!element.hasAttribute('inert') && // see https://github.com/GoogleChrome/inert-polyfill | ||
(element.offsetWidth > 0 || element.offsetHeight > 0))) { | ||
res.push(element); | ||
} | ||
// This query is adapted from a11y-dialog | ||
// https://github.com/edenspiekermann/a11y-dialog/blob/cf4ed81/a11y-dialog.js#L6-L18 | ||
var focusablesQuery = 'a[href], area[href], input, select, textarea, ' + | ||
'button, iframe, object, embed, [contenteditable], [tabindex], ' + | ||
'video[controls], audio[controls], summary'; | ||
function getActiveElement() { | ||
var activeElement = document.activeElement; | ||
while (activeElement.shadowRoot) { | ||
activeElement = activeElement.shadowRoot.activeElement; | ||
} | ||
return res; | ||
return activeElement; | ||
} | ||
function isFocusable(element) { | ||
return element.matches(focusablesQuery) && | ||
!element.disabled && | ||
!/^-/.test(element.getAttribute('tabindex') || '') && | ||
!element.hasAttribute('inert') && // see https://github.com/GoogleChrome/inert-polyfill | ||
(element.offsetWidth > 0 || element.offsetHeight > 0); | ||
} | ||
function getFocusTrapParent(element) { | ||
@@ -50,3 +46,3 @@ if (!focusTrapTest) { | ||
} | ||
function shouldIgnoreEvent(activeElement, key) { | ||
function shouldIgnoreEvent(activeElement, forwardDirection) { | ||
var tagName = activeElement.tagName; | ||
@@ -76,6 +72,6 @@ var isTextarea = tagName === 'TEXTAREA'; | ||
// unless the cursor is at the beginning or the end | ||
if (key === 'ArrowLeft' && selectionStart === selectionEnd && selectionStart === 0) { | ||
if (!forwardDirection && selectionStart === selectionEnd && selectionStart === 0) { | ||
return false; | ||
} | ||
else if (key === 'ArrowRight' && selectionStart === selectionEnd && selectionStart === len) { | ||
else if (forwardDirection && selectionStart === selectionEnd && selectionStart === len) { | ||
return false; | ||
@@ -85,24 +81,80 @@ } | ||
} | ||
function focusNextOrPrevious(event, key) { | ||
var activeElement = document.activeElement; | ||
if (shouldIgnoreEvent(activeElement, key)) { | ||
return; | ||
function getNextCandidateNodeForShadowDomPolyfill(root, targetElement, forwardDirection, filter) { | ||
// When the shadydom polyfill is running, we can't use TreeWalker on ShadowRoots because | ||
// they aren't real Nodes. So we do this workaround where we run TreeWalker on the | ||
// children instead. | ||
var nodes = Array.prototype.slice.call(root.querySelectorAll('*')); | ||
var idx = nodes.indexOf(targetElement); | ||
if (forwardDirection) { | ||
nodes = nodes.slice(idx + 1); | ||
} | ||
var focusableElements = getFocusableElements(activeElement); | ||
if (!focusableElements.length) { | ||
return; | ||
else { | ||
if (idx === -1) { | ||
idx = nodes.length; | ||
} | ||
nodes = nodes.slice(0, idx); | ||
nodes.reverse(); | ||
} | ||
var index = focusableElements.indexOf(activeElement); | ||
var element; | ||
if (key === 'ArrowLeft') { | ||
element = focusableElements[index - 1] || focusableElements[0]; | ||
for (var i = 0; i < nodes.length; i++) { | ||
var node = nodes[i]; | ||
if (node instanceof HTMLElement && filter.acceptNode(node) === NodeFilter.FILTER_ACCEPT) { | ||
return node; | ||
} | ||
} | ||
else { // ArrowRight | ||
element = focusableElements[index + 1] || focusableElements[focusableElements.length - 1]; | ||
return undefined; | ||
} | ||
function getNextCandidateNode(root, targetElement, forwardDirection, filter) { | ||
var walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter); | ||
if (targetElement) { | ||
walker.currentNode = targetElement; | ||
} | ||
element.focus(); | ||
event.preventDefault(); | ||
if (forwardDirection) { | ||
return walker.nextNode(); | ||
} | ||
else if (targetElement) { | ||
return walker.previousNode(); | ||
} | ||
// iterating backwards through shadow root, use last child | ||
return walker.lastChild(); | ||
} | ||
function isShadowDomPolyfill() { | ||
return typeof ShadowRoot !== 'undefined' && | ||
// ShadowRoot.polyfill is just a hack for our unit tests | ||
('polyfill' in ShadowRoot || !ShadowRoot.toString().includes('[native code]')); | ||
} | ||
function getNextNode(root, targetElement, forwardDirection) { | ||
var filter = { | ||
acceptNode: function (node) { | ||
return (node === targetElement || node.shadowRoot || isFocusable(node)) | ||
? NodeFilter.FILTER_ACCEPT | ||
: NodeFilter.FILTER_SKIP; | ||
} | ||
}; | ||
// TODO: remove this when we don't need to support the Shadow DOM polyfill | ||
var nextNode = isShadowDomPolyfill() && root instanceof ShadowRoot | ||
? getNextCandidateNodeForShadowDomPolyfill(root, targetElement, forwardDirection, filter) | ||
: getNextCandidateNode(root, targetElement, forwardDirection, filter); | ||
if (nextNode && nextNode.shadowRoot) { // push into the shadow DOM | ||
return getNextNode(nextNode.shadowRoot, null, forwardDirection); | ||
} | ||
if (!nextNode && root.host) { // pop out of the shadow DOM | ||
return getNextNode(root.host.getRootNode(), root.host, forwardDirection); | ||
} | ||
return nextNode; | ||
} | ||
function focusNextOrPrevious(event, key) { | ||
var activeElement = getActiveElement(); | ||
var forwardDirection = key === 'ArrowRight'; | ||
if (shouldIgnoreEvent(activeElement, forwardDirection)) { | ||
return; | ||
} | ||
var root = getFocusTrapParent(activeElement) || activeElement.getRootNode(); | ||
var nextNode = getNextNode(root, activeElement, forwardDirection); | ||
if (nextNode && nextNode !== activeElement) { | ||
nextNode.focus(); | ||
event.preventDefault(); | ||
} | ||
} | ||
function handleEnter(event) { | ||
var activeElement = document.activeElement; | ||
var activeElement = getActiveElement(); | ||
if (activeElement.tagName === 'INPUT' && | ||
@@ -109,0 +161,0 @@ checkboxRadioInputTypes.indexOf(activeElement.getAttribute('type').toLowerCase()) !== -1) { |
{ | ||
"name": "arrow-key-navigation", | ||
"description": "Add left/right key navigation to a KaiOS app or web app", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"license": "Apache-2.0", | ||
@@ -34,14 +34,26 @@ "files": [ | ||
"@pika/pack": "^0.5.0", | ||
"@pika/plugin-build-node": "^0.7.1", | ||
"@pika/plugin-build-umd": "^0.7.1", | ||
"@pika/plugin-build-web": "^0.7.1", | ||
"@pika/plugin-ts-standard-pkg": "^0.7.1", | ||
"@pika/plugin-build-node": "^0.9.2", | ||
"@pika/plugin-build-umd": "^0.9.2", | ||
"@pika/plugin-build-web": "^0.9.2", | ||
"@pika/plugin-ts-standard-pkg": "^0.9.2", | ||
"@rollup/plugin-commonjs": "^13.0.0", | ||
"@rollup/plugin-json": "^4.1.0", | ||
"@rollup/plugin-node-resolve": "^8.0.1", | ||
"assert": "^2.0.0", | ||
"jsdom": "^15.2.0", | ||
"jsdom": "^16.2.2", | ||
"jsdom-global": "^3.0.2", | ||
"mocha": "^6.2.2", | ||
"standard": "^14.3.1", | ||
"tslint": "^5.20.0", | ||
"tslint-config-standard": "^8.0.1", | ||
"typescript": "^3.6.4" | ||
"karma": "^5.1.0", | ||
"karma-chrome-launcher": "^3.1.0", | ||
"karma-coverage": "^2.0.2", | ||
"karma-mocha": "^2.0.1", | ||
"karma-rollup-preprocessor": "^7.0.5", | ||
"mocha": "^8.0.1", | ||
"nyc": "^15.1.0", | ||
"rollup": "^2.17.1", | ||
"rollup-plugin-istanbul": "^2.0.1", | ||
"rollup-plugin-node-polyfills": "^0.2.1", | ||
"standard": "^14.3.4", | ||
"tslint": "^6.1.2", | ||
"tslint-config-standard": "^9.0.0", | ||
"typescript": "^3.9.5" | ||
}, | ||
@@ -48,0 +60,0 @@ "source": "dist-src/index.js", |
@@ -14,3 +14,3 @@ arrow-key-navigation [![Build Status](https://travis-ci.org/nolanlawson/arrow-key-navigation.svg)](https://travis-ci.org/nolanlawson/arrow-key-navigation) | ||
It will also listen for the <kbd>Enter</kbd> key for certain special cases like checkbox/radio buttons. | ||
It will also listen for the <kbd>Enter</kbd> key for certain special cases like checkbox/radio buttons. `contenteditable` and Shadow DOM are also supported. | ||
@@ -78,2 +78,6 @@ ## Install | ||
### Code coverage | ||
npm run cover | ||
### Manual KaiOS app test | ||
@@ -80,0 +84,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
91847
807
86
25