From 79bce837afed02ca9b1e71ee0dcf4f1b74367133 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Sat, 9 Mar 2019 22:41:00 +1100 Subject: [PATCH] feat: multi target lock --- _tests/smoke.spec.js | 82 +++++++++++++++++++++++++++++++++++++++ src/focusInside.js | 16 +++----- src/focusMerge.js | 27 ++++++++----- src/setFocus.js | 20 +++++----- src/tabHook.js | 2 +- src/utils/DOMutils.js | 9 +++-- src/utils/all-affected.js | 25 +++++++----- src/utils/array.js | 1 + 8 files changed, 137 insertions(+), 45 deletions(-) create mode 100644 _tests/smoke.spec.js diff --git a/_tests/smoke.spec.js b/_tests/smoke.spec.js new file mode 100644 index 0000000..88aea7e --- /dev/null +++ b/_tests/smoke.spec.js @@ -0,0 +1,82 @@ +import {expect} from 'chai'; + +import {focusInside, focusMerge} from '../src/'; + +describe('smoke', () => { + const createTest = () => { + document.body.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+
+
+ `; + }; + + describe('FocusInside', () => { + it('false - when there is no focus', () => { + createTest(); + expect(focusInside(document.body)).to.be.equal(true); + expect(focusInside(document.querySelector('#d1'))).to.be.equal(false); + expect(focusInside(document.querySelector('#d2'))).to.be.equal(false); + expect(focusInside(document.querySelector('#d3'))).to.be.equal(false); + expect(focusInside(document.querySelector('#d4'))).to.be.equal(false); + }); + + it('true - when focus in d1', () => { + createTest(); + document.querySelector('#d1 button').focus(); + expect(focusInside(document.body)).to.be.equal(true); + expect(focusInside(document.querySelector('#d1'))).to.be.equal(true); + expect(focusInside(document.querySelector('#d2'))).to.be.equal(false); + }); + + it('true - when focus on d4 (tabbable)', () => { + createTest(); + document.querySelector('#d4').focus(); + expect(focusInside(document.body)).to.be.equal(true); + expect(focusInside(document.querySelector('#d4'))).to.be.equal(true); + expect(focusInside(document.querySelector('#d1'))).to.be.equal(false); + }); + + it('multi-test', () => { + createTest(); + document.querySelector('#d1 button').focus(); + expect(focusInside(document.body)).to.be.equal(true); + expect(focusInside(document.querySelector('#d1'))).to.be.equal(true); + expect(focusInside([document.querySelector('#d1')])).to.be.equal(true); + expect(focusInside([document.querySelector('#d2')])).to.be.equal(false); + expect(focusInside([document.querySelector('#d1'), document.querySelector('#d2')])).to.be.equal(true); + expect(focusInside([document.querySelector('#d2'), document.querySelector('#d3')])).to.be.equal(false); + expect(focusInside([document.querySelector('#d3'), document.querySelector('#d1')])).to.be.equal(true); + }); + }); + + describe('FocusMerge', () => { + it('move focus', () => { + createTest(); + document.querySelector('#d4').focus(); + focusMerge(document.querySelector('#d1'), null).node.focus(); + expect(focusInside(document.querySelector('#d1'))).to.be.equal(true); + + focusMerge(document.querySelector('#d2'), null).node.focus(); + expect(focusInside(document.querySelector('#d2'))).to.be.equal(true); + + expect(focusMerge([document.querySelector('#d2'), document.querySelector('#d3')], null)).to.be.equal(undefined); + expect(focusInside(document.querySelector('#d2'))).to.be.equal(true); + + focusMerge([document.querySelector('#d3'), document.querySelector('#d4')], null).node.focus(); + expect(focusInside(document.querySelector('#d3'))).to.be.equal(true); + }); + }); + +}); \ No newline at end of file diff --git a/src/focusInside.js b/src/focusInside.js index 638ea96..ba79586 100644 --- a/src/focusInside.js +++ b/src/focusInside.js @@ -3,12 +3,7 @@ import { arrayFind, toArray } from './utils/array'; const focusInFrame = frame => frame === document.activeElement; -const focusInsideIframe = topNode => ( - getAllAffectedNodes(topNode).reduce( - (result, node) => result || !!arrayFind(toArray(node.querySelectorAll('iframe')), focusInFrame), - false, - ) -); +const focusInsideIframe = topNode => !!arrayFind(toArray(topNode.querySelectorAll('iframe')), focusInFrame); const focusInside = (topNode) => { const activeElement = document && document.activeElement; @@ -16,10 +11,11 @@ const focusInside = (topNode) => { if (!activeElement || (activeElement.dataset && activeElement.dataset.focusGuard)) { return false; } - return getAllAffectedNodes(topNode).reduce( - (result, node) => result || node.contains(activeElement) || focusInsideIframe(topNode), - false, - ); + return getAllAffectedNodes(topNode) + .reduce( + (result, node) => result || node.contains(activeElement) || focusInsideIframe(node), + false, + ); }; export default focusInside; diff --git a/src/focusMerge.js b/src/focusMerge.js index 38006a6..ea4e24e 100644 --- a/src/focusMerge.js +++ b/src/focusMerge.js @@ -1,6 +1,7 @@ import { getCommonParent, getTabbableNodes, getAllTabbableNodes, parentAutofocusables } from './utils/DOMutils'; import pickFirstFocus from './utils/firstFocus'; import getAllAffectedNodes from './utils/all-affected'; +import { asArray } from './utils/array'; const findAutoFocused = autoFocusables => node => ( !!node.autofocus || @@ -60,17 +61,23 @@ export const newFocus = (innerNodes, outerNodes, activeElement, lastNode, autoFo return undefined; }; -const getTopCommonParent = (activeElement, entry, entries) => { - let topCommon = entry; - entries.forEach((subEntry) => { - const common = getCommonParent(activeElement, subEntry); - if (common) { - if (common.contains(topCommon)) { - topCommon = common; - } else { - topCommon = getCommonParent(common, topCommon); +const getTopCommonParent = (baseActiveElement, leftEntry, rightEntries) => { + const activeElements = asArray(baseActiveElement); + const leftEntries = asArray(leftEntry); + const activeElement = activeElements[0]; + let topCommon = null; + leftEntries.forEach((entry) => { + topCommon = getCommonParent(topCommon || entry, entry); + rightEntries.forEach((subEntry) => { + const common = getCommonParent(activeElement, subEntry); + if (common) { + if (common.contains(topCommon)) { + topCommon = common; + } else { + topCommon = getCommonParent(common, topCommon); + } } - } + }); }); return topCommon; }; diff --git a/src/setFocus.js b/src/setFocus.js index 44b6503..75acc38 100644 --- a/src/setFocus.js +++ b/src/setFocus.js @@ -19,17 +19,15 @@ export default (topNode, lastNode) => { if (focusable) { if (guardCount > 2) { - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line no-console - console.error( - 'FocusLock: focus-fighting detected. Only one focus management system could be active. ' + - 'See https://github.com/theKashey/focus-lock/#focus-fighting', - ); - lockDisabled = true; - setTimeout(() => { - lockDisabled = false; - }, 1); - } + // eslint-disable-next-line no-console + console.error( + 'FocusLock: focus-fighting detected. Only one focus management system could be active. ' + + 'See https://github.com/theKashey/focus-lock/#focus-fighting', + ); + lockDisabled = true; + setTimeout(() => { + lockDisabled = false; + }, 1); return; } guardCount++; diff --git a/src/tabHook.js b/src/tabHook.js index 45d4f57..1d7cb2a 100644 --- a/src/tabHook.js +++ b/src/tabHook.js @@ -1,5 +1,5 @@ export default { - attach(node, enabled) { + attach() { }, detach() { diff --git a/src/utils/DOMutils.js b/src/utils/DOMutils.js index 5f92dc8..9d1dedb 100644 --- a/src/utils/DOMutils.js +++ b/src/utils/DOMutils.js @@ -51,10 +51,13 @@ export const filterFocusable = nodes => .filter(node => isVisible(node)) .filter(node => notHiddenInput(node)); -export const getTabbableNodes = topNodes => orderByTabIndex(filterFocusable(getFocusables(topNodes)), true); - -export const getAllTabbableNodes = topNodes => orderByTabIndex(filterFocusable(getFocusables(topNodes)), false); +export const getTabbableNodes = topNodes => ( + orderByTabIndex(filterFocusable(getFocusables(topNodes)), true) +); +export const getAllTabbableNodes = topNodes => ( + orderByTabIndex(filterFocusable(getFocusables(topNodes)), false) +); export const parentAutofocusables = topNode => filterFocusable(getParentAutofocusables(topNode)); diff --git a/src/utils/all-affected.js b/src/utils/all-affected.js index 4473060..160d1d3 100644 --- a/src/utils/all-affected.js +++ b/src/utils/all-affected.js @@ -1,12 +1,10 @@ import { FOCUS_DISABLED, FOCUS_GROUP } from '../constants'; -import { toArray } from './array'; +import { asArray, toArray } from './array'; const filterNested = (nodes) => { const l = nodes.length; - let i; - let j; - for (i = 0; i < l; i += 1) { - for (j = 0; j < l; j += 1) { + for (let i = 0; i < l; i += 1) { + for (let j = 0; j < l; j += 1) { if (i !== j) { if (nodes[i].contains(nodes[j])) { return filterNested(nodes.filter(x => x !== nodes[j])); @@ -20,11 +18,18 @@ const filterNested = (nodes) => { const getTopParent = node => (node.parentNode ? getTopParent(node.parentNode) : node); const getAllAffectedNodes = (node) => { - const group = node.getAttribute(FOCUS_GROUP); - if (group) { - return filterNested(toArray(getTopParent(node).querySelectorAll(`[${FOCUS_GROUP}="${group}"]:not([${FOCUS_DISABLED}="disabled"])`))); - } - return [node]; + const nodes = asArray(node); + return nodes.reduce((acc, currentNode) => { + const group = currentNode.getAttribute(FOCUS_GROUP); + acc.push( + ...group + ? filterNested(toArray( + getTopParent(currentNode).querySelectorAll(`[${FOCUS_GROUP}="${group}"]:not([${FOCUS_DISABLED}="disabled"])`), + )) + : [currentNode], + ); + return acc; + }, []); }; export default getAllAffectedNodes; diff --git a/src/utils/array.js b/src/utils/array.js index 89f62c1..4ff18f1 100644 --- a/src/utils/array.js +++ b/src/utils/array.js @@ -8,3 +8,4 @@ export const toArray = (a) => { export const arrayFind = (array, search) => array.filter(a => a === search)[0]; +export const asArray = a => (Array.isArray(a) ? a : [a]);