diff --git a/.eslintrc b/.eslintrc index 1f6d407..57c3cbb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,6 +10,7 @@ "mocha": true, }, "rules": { + "no-cond-assign": "off", "jsx-a11y/href-no-hash": "off", // Override airbnb to disable devDependencies check until we figure out how to exclude tests files "import/no-extraneous-dependencies": [2, {"optionalDependencies": false}], @@ -24,4 +25,4 @@ // Turn off https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-named-as-default-member.md "import/no-named-as-default-member": 0, } -} +} \ No newline at end of file diff --git a/_tests/FocusLock.spec.js b/_tests/FocusLock.spec.js index aa0a1e9..c5eadae 100644 --- a/_tests/FocusLock.spec.js +++ b/_tests/FocusLock.spec.js @@ -791,6 +791,64 @@ describe('react-focus-lock', () => { }, 1); }); + it('Should handle focus groups - shard', (done) => { + const ref1 = React.createRef(); + const ref2 = React.createRef(); + const TestCase = () => ( +
+
+ text + + text +
+ + + + + + + +
+ text + + text +
+
+ ); + + const wrapper = mount(, mountPoint); + wrapper.update().find('#b2').getDOMNode().focus(); + // update wrapper to propagate ref2 + wrapper.setProps({}); + expect(document.activeElement.innerHTML).to.be.equal('button2'); + setTimeout(() => { + expect(document.activeElement.innerHTML).to.be.equal('button2'); + wrapper.find('#b3').simulate('focus'); + wrapper.find('#b3').getDOMNode().focus(); + wrapper.find('#b1').simulate('blur'); + expect(document.activeElement.innerHTML).to.be.equal('button3'); + setTimeout(() => { + expect(document.activeElement.innerHTML).to.be.equal('button3'); + wrapper.find('#b4').simulate('focus'); + wrapper.find('#b4').getDOMNode().focus(); + wrapper.find('#b1').simulate('blur'); + expect(document.activeElement.innerHTML).to.be.equal('button4'); + setTimeout(() => { + 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'); + wrapper.find('#b1').simulate('blur'); + setTimeout(() => { + // it should be 3 :( + expect(document.activeElement.innerHTML).to.be.equal('button3'); + done(); + }, 1); + }, 1); + }, 1); + }, 1); + }); + it('Should handle focus groups - disabled', (done) => { const wrapper = mount(
diff --git a/react-focus-lock.d.ts b/react-focus-lock.d.ts index ae5115c..d7f3c0a 100644 --- a/react-focus-lock.d.ts +++ b/react-focus-lock.d.ts @@ -70,7 +70,7 @@ declare module 'react-focus-lock' { /** * Shards forms a scattered lock, same as `group` does, but in more "low" and controlled way */ - shards?: Array>; + shards?: Array | HTMLElement>; children: React.ReactNode; } diff --git a/src/FocusGuard.js b/src/FocusGuard.js index d2cd1f5..c7255ed 100644 --- a/src/FocusGuard.js +++ b/src/FocusGuard.js @@ -11,16 +11,20 @@ export const hiddenGuard = { left: '1px', }; -const InFocusGuard = ({children}) => ( +const InFocusGuard = ({ children }) => ( -
+
{children} - {children &&
} + {children &&
} ); InFocusGuard.propTypes = { children: PropTypes.node, }; +InFocusGuard.defaultProps = { + children: null, +}; + export default InFocusGuard; diff --git a/src/Lock.js b/src/Lock.js index 0ae8d97..2441f7d 100644 --- a/src/Lock.js +++ b/src/Lock.js @@ -1,10 +1,10 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import {constants} from 'focus-lock'; -import FocusTrap, {onBlur, onFocus} from './Trap'; -import {hiddenGuard} from './FocusGuard'; +import { constants } from 'focus-lock'; +import FocusTrap, { onBlur, onFocus } from './Trap'; +import { hiddenGuard } from './FocusGuard'; -const RenderChildren = ({children}) =>
{children}
; +const RenderChildren = ({ children }) =>
{children}
; RenderChildren.propTypes = { children: PropTypes.node.isRequired, }; @@ -81,7 +81,7 @@ class FocusLock extends Component { as: Container = 'div', lockProps: containerProps = {}, } = this.props; - const {observed} = this.state; + const { observed } = this.state; if (process.env.NODE_ENV !== 'production') { if (typeof allowTextSelection !== 'undefined') { @@ -99,8 +99,8 @@ class FocusLock extends Component { return ( {!noFocusGuards && [ -
, // nearest focus guard -
, // first tabbed element guard +
, // nearest focus guard +
, // first tabbed element guard ]} { !noFocusGuards && -
+
} ); @@ -144,7 +144,7 @@ FocusLock.propTypes = { className: PropTypes.string, whiteList: PropTypes.func, - shards: PropTypes.arrayOf(PropTypes.shape({current: PropTypes.instanceOf(Element)})), + shards: PropTypes.arrayOf(PropTypes.shape({ current: PropTypes.instanceOf(Element) })), as: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]), lockProps: PropTypes.object, @@ -166,6 +166,7 @@ FocusLock.defaultProps = { shards: undefined, as: 'div', lockProps: {}, + onActivation: undefined, onDeactivation: undefined, }; diff --git a/src/Trap.js b/src/Trap.js index 11c6524..07120fd 100644 --- a/src/Trap.js +++ b/src/Trap.js @@ -1,8 +1,8 @@ import 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 moveFocusInside, { focusInside, focusIsHidden, getFocusabledIn } from 'focus-lock'; +import { deferAction } from './util'; const focusOnBody = () => ( document && document.activeElement === document.body @@ -22,7 +22,7 @@ const focusWhitelisted = activeElement => ( ); const recordPortal = (observerNode, portaledElement) => { - lastPortaledElement = {observerNode, portaledElement}; + lastPortaledElement = { observerNode, portaledElement }; }; const focusIsPortaledPair = element => ( @@ -47,14 +47,19 @@ function autoGuard(startIndex, end, step, allNodes) { } } +const extractRef = ref => ((ref && 'current' in ref) ? ref.current : ref); + const activateTrap = () => { let result = false; if (lastActiveTrap) { - const {observed, persistentFocus, autoFocus, shards} = lastActiveTrap; + const { observed, persistentFocus, autoFocus, shards } = lastActiveTrap; const workingNode = observed || (lastPortaledElement && lastPortaledElement.portaledElement); const activeElement = document && document.activeElement; if (workingNode) { - const workingArea = [workingNode, ...shards.map(({current}) => current)]; + const workingArea = [ + workingNode, + ...shards.map(extractRef).filter(Boolean), + ]; if (!activeElement || focusWhitelisted(activeElement)) { if (persistentFocus || !isFreeFocus() || (!lastActiveFocus && autoFocus)) { @@ -80,12 +85,12 @@ const activateTrap = () => { if (document) { const newActiveElement = document && document.activeElement; const allNodes = getFocusabledIn(workingArea); - const focusedItem = allNodes.find(({node}) => node === newActiveElement); + const focusedItem = allNodes.find(({ node }) => node === newActiveElement); if (focusedItem) { // 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')); const focusedIndex = allNodes.indexOf(focusedItem); autoGuard(focusedIndex, allNodes.length, +1, allNodes); @@ -120,7 +125,7 @@ export const onFocus = (event) => { const FocusWatcher = () => null; -const FocusTrap = ({children}) => ( +const FocusTrap = ({ children }) => (
{children}
@@ -140,10 +145,9 @@ const detachHandler = () => { document.removeEventListener('focusout', onBlur); }; - function reducePropsToState(propsList) { return propsList - .filter(({disabled}) => !disabled) + .filter(({ disabled }) => !disabled) .slice(-1)[0]; } diff --git a/stories/index.js b/stories/index.js index 2329c0f..47998ba 100644 --- a/stories/index.js +++ b/stories/index.js @@ -17,7 +17,7 @@ import {PortalCase, ShardPortalCase} from './Portal'; import {MUISelect, MUISelectWhite} from './MUI'; import Fight from './FocusFighting'; import {StyledComponent, StyledSection} from "./Custom"; -import {DisabledForm, DisabledFormWithTabIndex} from "./Disabled"; +import {AutoDisabledForm, DisabledForm, DisabledFormWithTabIndex} from "./Disabled"; const frameStyle = { width: '400px',