Skip to content

Commit

Permalink
feat: multi target lock
Browse files Browse the repository at this point in the history
  • Loading branch information
theKashey committed Mar 9, 2019
1 parent a16789b commit 79bce83
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 45 deletions.
82 changes: 82 additions & 0 deletions _tests/smoke.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {expect} from 'chai';

import {focusInside, focusMerge} from '../src/';

describe('smoke', () => {
const createTest = () => {
document.body.innerHTML = `
<div id="d1">
<button>1</button>
<button>2</button>
</div>
<div id="d2">
<button>3</button>
<button>4</button>
</div>
<div id="d3">
<button>3</button>
<button>4</button>
</div>
<div id="d4" tabindex="1">
</div>
`;
};

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);
});
});

});
16 changes: 6 additions & 10 deletions src/focusInside.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,19 @@ 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;

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;
27 changes: 17 additions & 10 deletions src/focusMerge.js
Original file line number Diff line number Diff line change
@@ -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 ||
Expand Down Expand Up @@ -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;
};
Expand Down
20 changes: 9 additions & 11 deletions src/setFocus.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
Expand Down
2 changes: 1 addition & 1 deletion src/tabHook.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export default {
attach(node, enabled) {
attach() {
},

detach() {
Expand Down
9 changes: 6 additions & 3 deletions src/utils/DOMutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
25 changes: 15 additions & 10 deletions src/utils/all-affected.js
Original file line number Diff line number Diff line change
@@ -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]));
Expand All @@ -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;
1 change: 1 addition & 0 deletions src/utils/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

0 comments on commit 79bce83

Please sign in to comment.