From d3a0bc42a3002543397beafd93be6b5828ba38c5 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 3 Sep 2018 22:02:54 +1000 Subject: [PATCH] Impliment focus white lists. fixes #37, #36, #27 --- README.md | 8 ++++++++ react-focus-lock.d.ts | 7 +++++++ src/Lock.js | 5 +++++ src/Trap.js | 48 +++++++++++++++++++++++++++---------------- stories/MUI.js | 41 ++++++++++++++++++++++++++++++++++++ stories/index.js | 5 +++-- 6 files changed, 94 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a63387a..4be8b7f 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ I'v got a good [article about focus management, dialogs and WAI-ARIA](https://m - `autoFocus`, default true, enables or disables focusing into on Lock activation. If disabled Lock will blur an active focus. - `noFocusGuards` disabled _focus guards_ - virtual inputs which secure tab index. - `group` named focus group for focus scattering aka [combined lock targets](https://github.com/theKashey/vue-focus-lock/issues/2) + - `whiteList` you could _whitelist_ locations FocusLock should carry about. Everything outside it will ignore. For example - any modals. # Behavior 0. It will always keep focus inside Lock. @@ -166,6 +167,13 @@ any focus inside marked node, thus landing a peace. ``` +Even the better is to `whiteList` FocusLock areas - for example "you should handle only React Stuff in React Root" +```js + document.getElementById('root').contains(node)}> + ... + +``` + # Licence MIT diff --git a/react-focus-lock.d.ts b/react-focus-lock.d.ts index 34ec240..40e7cec 100644 --- a/react-focus-lock.d.ts +++ b/react-focus-lock.d.ts @@ -43,6 +43,13 @@ declare module 'react-focus-lock' { children: React.ReactNode; className?: string; + + /** + * Controls focus lock working areas. Lock will silently ignore all the events from `not allowed` areas + * @param activeElement + * @returns {Boolean} true if focus lock should handle activeElement, false if not + */ + whiteList?: (activeElement: HTMLElement) => boolean; } interface AutoFocusProps { diff --git a/src/Lock.js b/src/Lock.js index 29e7cf7..ef65c25 100644 --- a/src/Lock.js +++ b/src/Lock.js @@ -68,6 +68,7 @@ class FocusLock extends Component { allowTextSelection, group, className, + whiteList, } = this.props; const { observed } = this.state; @@ -99,6 +100,7 @@ class FocusLock extends Component { disabled={disabled} persistentFocus={persistentFocus} autoFocus={autoFocus} + whiteList={whiteList} onActivation={this.onActivation} /> {children} @@ -121,6 +123,8 @@ FocusLock.propTypes = { group: PropTypes.string, className: PropTypes.string, + + whiteList: PropTypes.func, }; FocusLock.defaultProps = { @@ -132,6 +136,7 @@ FocusLock.defaultProps = { allowTextSelection: undefined, group: undefined, className: undefined, + whiteList: undefined, }; diff --git a/src/Trap.js b/src/Trap.js index adf1a0e..291c609 100644 --- a/src/Trap.js +++ b/src/Trap.js @@ -4,13 +4,23 @@ import withSideEffect from 'react-clientside-effect'; import moveFocusInside, { focusInside, focusIsHidden } from 'focus-lock'; import { deferAction } from './util'; -const focusOnBody = () => document && (document.activeElement === document.body || focusIsHidden()); +const focusOnBody = () => ( + document && document.activeElement === document.body +); + +const isFreeFocus = () => focusOnBody() || focusIsHidden(); -let lastActiveTrap = 0; +let lastActiveTrap = null; let lastActiveFocus = null; let lastPortaledElement = null; +const defaultWhitelist = () => true; + +const focusWhitelisted = activeElement => ( + (lastActiveTrap.whiteList || defaultWhitelist)(activeElement) +); + const recordPortal = (observerNode, portaledElement) => { lastPortaledElement = { observerNode, portaledElement }; }; @@ -26,24 +36,26 @@ const activateTrap = () => { const workingNode = observed || (lastPortaledElement && lastPortaledElement.portaledElement); const activeElement = document && document.activeElement; - if (persistentFocus || !focusOnBody() || (!lastActiveFocus && autoFocus)) { - if ( - workingNode && - !( - focusInside(workingNode) || - focusIsPortaledPair(activeElement, workingNode) - ) - ) { - onActivation(); - if (document && !lastActiveFocus && activeElement && !autoFocus) { - activeElement.blur(); - document.body.focus(); - } else { - result = moveFocusInside(workingNode, lastActiveFocus); - lastPortaledElement = {}; + if (!activeElement || focusWhitelisted(activeElement)) { + if (persistentFocus || !isFreeFocus() || (!lastActiveFocus && autoFocus)) { + if ( + workingNode && + !( + focusInside(workingNode) || + focusIsPortaledPair(activeElement, workingNode) + ) + ) { + onActivation(); + if (document && !lastActiveFocus && activeElement && !autoFocus) { + activeElement.blur(); + document.body.focus(); + } else { + result = moveFocusInside(workingNode, lastActiveFocus); + lastPortaledElement = {}; + } } + lastActiveFocus = document && document.activeElement; } - lastActiveFocus = document && document.activeElement; } } return result; diff --git a/stories/MUI.js b/stories/MUI.js index b2573d3..3d87c1f 100644 --- a/stories/MUI.js +++ b/stories/MUI.js @@ -22,6 +22,47 @@ export const MUISelect = () => ( + + + + + + + + + +
+ + +); + +export const MUISelectWhite = () => ( +
+

With focus lock active

+

+ will work, due to whitelisting +

+ + document.getElementById('root').contains(node)} + > + + + + + + + + + + + + + + +
diff --git a/stories/index.js b/stories/index.js index ee48c1b..3181dec 100644 --- a/stories/index.js +++ b/stories/index.js @@ -14,7 +14,7 @@ import {TextSelectionEnabled, TextSelectionDisabled, TextSelectionTabIndexEnable import JumpCase from './Jump'; import GroupCase from './Group'; import PortalCase from './Portal'; -import {MUISelect} from './MUI'; +import {MUISelect, MUISelectWhite} from './MUI'; import Fight from './FocusFighting'; const frameStyle = { @@ -52,7 +52,8 @@ storiesOf('Group', module) .add('focus group', () => ); storiesOf('Material UI', module) - .add('Select', () => ); + .add('Select', () => ) + .add('Select White', () => ) storiesOf('Focus fighting', module) .add('fight', () => );