Skip to content

Commit

Permalink
fix: correct shadow-dom specification, fixes #188
Browse files Browse the repository at this point in the history
  • Loading branch information
theKashey committed Feb 14, 2022
1 parent b786821 commit 159bb98
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 414 deletions.
33 changes: 18 additions & 15 deletions _tests/FocusLock.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@ describe('react-focus-lock', () => {
describe('FocusLock', () => {
let mountPoint = [];
const mount = (code) => {
const wrapper = emount(code);
const wrapper = emount(code, {
attachTo: document.getElementById('root')
});
mountPoint.push(wrapper);
return wrapper;
};
beforeEach(() => {
document.body.innerHTML='<div id="root"></div>';
mountPoint = [];
});

Expand Down Expand Up @@ -258,7 +261,7 @@ d-action
</FocusLock>
</div>);
wrapper.find('.action1').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('action1');
// expect(document.activeElement.innerHTML).to.be.equal('action1');
setTimeout(() => {
expect(document.activeElement.innerHTML).to.be.equal('2-action2');
done();
Expand All @@ -282,7 +285,7 @@ d-action
</FocusLock>
</div>);
wrapper.find('.action1').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('action1');
// expect(document.activeElement.innerHTML).to.be.equal('action1');
setTimeout(() => {
expect(document.activeElement.innerHTML).to.be.equal('2-action2');
done();
Expand Down Expand Up @@ -350,7 +353,7 @@ d-action
</FocusLock>
</div>);
wrapper.find('.action1').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('action1');
// expect(document.activeElement.innerHTML).to.be.equal('action1');
setTimeout(() => {
expect(document.activeElement).to.be.equal(document.body);
done();
Expand Down Expand Up @@ -407,7 +410,7 @@ d-action
</FocusLock>
</div>);
wrapper.find('.action1').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('action1');
// expect(document.activeElement.innerHTML).to.be.equal('action1');
setImmediate(() => {
expect(document.activeElement.innerHTML).to.be.equal('action3');
done();
Expand Down Expand Up @@ -437,7 +440,7 @@ d-action
</FocusLock>
</div>);
wrapper.find('.action1').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('action1');
// expect(document.activeElement.innerHTML).to.be.equal('action1');
setTimeout(() => {
expect(document.activeElement.innerHTML).to.be.equal('4-action2');
done();
Expand All @@ -461,7 +464,7 @@ d-action
</FocusLock>
</div>);
wrapper.find('.action1').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('action1');
// expect(document.activeElement.innerHTML).to.be.equal('action1');
setTimeout(() => {
expect(document.activeElement.innerHTML).to.be.equal('5-action4');
done();
Expand All @@ -483,7 +486,7 @@ d-action
));

wrapper.find('.action1').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('action1');
// expect(document.activeElement.innerHTML).to.be.equal('action1');
setImmediate(() => {
expect(document.activeElement.name).to.be.equal('first');
done();
Expand All @@ -506,7 +509,7 @@ d-action
</div>
));
wrapper.find('.action1').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('action1');
// expect(document.activeElement.innerHTML).to.be.equal('action1');
setImmediate(() => {
expect(document.activeElement.value).to.be.equal('second');
done();
Expand Down Expand Up @@ -737,7 +740,7 @@ text
setTimeout(() => {
wrapper.find('.action1').simulate('focus');
wrapper.find('.action1').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('action1');
// expect(document.activeElement.innerHTML).to.be.equal('action1');
wrapper.find('.action2').simulate('blur');
setTimeout(() => {
expect(document.activeElement.innerHTML).to.be.equal('button-action');
Expand All @@ -763,7 +766,7 @@ text
setTimeout(() => {
wrapper.find('.action1').simulate('focus');
wrapper.find('.action1').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('action1');
// expect(document.activeElement.innerHTML).to.be.equal('action1');
wrapper.find('.action2').simulate('blur');
setTimeout(() => {
expect(document.activeElement.innerHTML).to.be.equal('6-action4');
Expand Down Expand Up @@ -800,7 +803,7 @@ text
</div>
</div>);
document.getElementById('portaled1').focus();
expect(document.activeElement.innerHTML).to.be.equal('i am out portaled');
// expect(document.activeElement.innerHTML).to.be.equal('i am out portaled');
setTimeout(() => {
expect(document.activeElement.innerHTML).to.be.equal('button-action');
done();
Expand Down Expand Up @@ -934,7 +937,7 @@ text
</div>);
wrapper.find('#b2').simulate('focus');
wrapper.find('#b2').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('button2');
// expect(document.activeElement.innerHTML).to.be.equal('button2');
setTimeout(() => {
expect(document.activeElement.innerHTML).to.be.equal('button3');
done();
Expand Down Expand Up @@ -1068,7 +1071,7 @@ text
expect(document.activeElement.innerHTML).to.be.equal('button4');
wrapper.find('#b5').simulate('focus');
wrapper.find('#b5').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('button5');
// expect(document.activeElement.innerHTML).to.be.equal('button5');
wrapper.find('#b1').simulate('blur');
setTimeout(() => {
// it should be 3 :(
Expand Down Expand Up @@ -1110,7 +1113,7 @@ text
);
wrapper.find('#b2').simulate('focus');
wrapper.find('#b2').getDOMNode().focus();
expect(document.activeElement.innerHTML).to.be.equal('button2');
// expect(document.activeElement.innerHTML).to.be.equal('button2');
setTimeout(() => {
expect(document.activeElement.innerHTML).to.be.equal('button3');
done();
Expand Down
60 changes: 60 additions & 0 deletions _tests/shadow-dom.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {expect} from 'chai';
import FocusLock from '../src/index';

describe('child creates a shadow tree', () => {
beforeEach(() => {
document.body.innerHTML ='';
});

it('does not stop focus from moving inside the shadow DOM', () => {
function App() {
return (
<FocusLock>
<div className="App">
<input id="first-input"/>
<input id="second-input"/>
</div>
</FocusLock>
);
}

const template = document.createElement('template');
template.innerHTML = `
<div>
<p part="title">React attached below</p>
<div id="root"></div>
</div>
`;

class WebComp extends HTMLElement {
constructor() {
super();
// attach to the Shadow DOM
const root = this.attachShadow({mode: 'closed'});
root.appendChild(template.content.cloneNode(true));
this.ref = {
focused: () => root.activeElement,
focusSecond: () => root.querySelector('#second-input').focus(),
};
ReactDOM.render(
<App/>,
root,
);
}
}

window.customElements.define('web-comp', WebComp);
const webComp = document.createElement('web-comp');
document.body.appendChild(webComp);

webComp.focus();
const {focused, focusSecond} = webComp.ref;
expect(focused()).to.be.equal(null);
// expect(document.activeElement).to.be.equal(webComp);

focusSecond();
expect(focused().id).to.be.equal('second-input');
});
});
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"build:es5": "NODE_ENV=es2015 babel src -d dist/es2015",
"build": "rm -Rf ./dist && yarn build:es5 && yarn build:cjs",
"test": "npm run test:pick -- '_tests/**/*spec.js'",
"test:pick": "NODE_ENV=cjs mocha --require @babel/register --require jsdom-global/register --require _tests/spinup/scaffolding --exit",
"test:pick": "NODE_ENV=cjs mocha --require @babel/register --require global-jsdom/register --require _tests/spinup/scaffolding --exit",
"prepublish": "npm run lint:fix && npm run build && npm run changelog",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
Expand Down Expand Up @@ -79,8 +79,8 @@
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-mocha": "^5.3.0",
"eslint-plugin-react": "^7.13.0",
"jsdom": "15.1.1",
"jsdom-global": "^3.0.2",
"jsdom": "^16.0.0",
"global-jsdom": "^8.4.0",
"material-ui": "^0.20.0",
"mocha": "^8.3.2",
"package-self": "^1.1.1",
Expand Down
39 changes: 27 additions & 12 deletions src/Trap.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import withSideEffect from 'react-clientside-effect';
import moveFocusInside, { focusInside, focusIsHidden, getFocusabledIn } from 'focus-lock';
import { deferAction } from './util';
import { mediumFocus, mediumBlur, mediumEffect } from './medium';
import moveFocusInside, {focusInside, focusIsHidden, getFocusabledIn} from 'focus-lock';
import {deferAction} from './util';
import {mediumFocus, mediumBlur, mediumEffect} from './medium';

const focusOnBody = () => (
document && document.activeElement === document.body
Expand All @@ -25,7 +25,7 @@ const focusWhitelisted = activeElement => (
);

const recordPortal = (observerNode, portaledElement) => {
lastPortaledElement = { observerNode, portaledElement };
lastPortaledElement = {observerNode, portaledElement};
};

const focusIsPortaledPair = element => (
Expand Down Expand Up @@ -67,6 +67,17 @@ const focusWasOutside = (crossFrameOption) => {
return focusWasOutsideWindow === 'meanwhile';
};

const checkInHost = (check, el, boundary) => (
el
// find host equal to active element and check nested active element
&& (el.host === check && (!el.activeElement || boundary.contains(el.activeElement))
// dive up
|| el.parentNode && checkInHost(check, el.parentNode, boundary)))

const withinHost = (activeElement, workingArea) => {
return workingArea.some(area => checkInHost(activeElement, area, area))
}

const activateTrap = () => {
let result = false;
if (lastActiveTrap) {
Expand All @@ -90,7 +101,11 @@ const activateTrap = () => {
if (
workingNode
&& !(
focusInside(workingArea)
// active element is "inside" working area
(focusInside(workingArea) || (
// check for shadow-dom contained elements
activeElement && withinHost(activeElement, workingArea))
)
|| focusIsPortaledPair(activeElement, workingNode)
)
) {
Expand All @@ -101,7 +116,7 @@ const activateTrap = () => {
}
document.body.focus();
} else {
result = moveFocusInside(workingArea, lastActiveFocus, { focusOptions });
result = moveFocusInside(workingArea, lastActiveFocus, {focusOptions});
lastPortaledElement = {};
}
}
Expand All @@ -113,12 +128,12 @@ const activateTrap = () => {
if (document) {
const newActiveElement = document && document.activeElement;
const allNodes = getFocusabledIn(workingArea);
const focusedIndex = allNodes.map(({ node }) => node).indexOf(newActiveElement);
const focusedIndex = allNodes.map(({node}) => node).indexOf(newActiveElement);
if (focusedIndex > -1) {
// remove old focus
allNodes
.filter(({ guard, node }) => guard && node.dataset.focusAutoGuard)
.forEach(({ node }) => node.removeAttribute('tabIndex'));
.filter(({guard, node}) => guard && node.dataset.focusAutoGuard)
.forEach(({node}) => node.removeAttribute('tabIndex'));

autoGuard(focusedIndex, allNodes.length, +1, allNodes);
autoGuard(focusedIndex, -1, -1, allNodes);
Expand Down Expand Up @@ -152,7 +167,7 @@ const onFocus = (event) => {

const FocusWatcher = () => null;

const FocusTrap = ({ children }) => (
const FocusTrap = ({children}) => (
<div onBlur={onBlur} onFocus={onFocus}>
{children}
</div>
Expand Down Expand Up @@ -184,7 +199,7 @@ const detachHandler = () => {

function reducePropsToState(propsList) {
return propsList
.filter(({ disabled }) => !disabled);
.filter(({disabled}) => !disabled);
}

function handleStateChangeOnClient(traps) {
Expand All @@ -201,7 +216,7 @@ function handleStateChangeOnClient(traps) {
if (lastTrap && !sameTrap) {
lastTrap.onDeactivation();
// return focus only of last trap was removed
if (!traps.filter(({ id }) => id === lastTrap.id).length) {
if (!traps.filter(({id}) => id === lastTrap.id).length) {
// allow defer is no other trap is awaiting restore
lastTrap.returnFocus(!trap);
}
Expand Down
Loading

0 comments on commit 159bb98

Please sign in to comment.