Skip to content

Commit

Permalink
Impliment focus white lists. fixes theKashey#37, theKashey#36, theKas…
Browse files Browse the repository at this point in the history
  • Loading branch information
theKashey committed Sep 3, 2018
1 parent 6537e41 commit 68f3782
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 20 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -166,6 +167,13 @@ any focus inside marked node, thus landing a peace.
</FreeFocusInside>
```
Even the better is to `whiteList` FocusLock areas - for example "you should handle only React Stuff in React Root"
```js
<FocusLock whiteList={node => document.getElementById('root').contains(node)}>
...
</FocusLock>
```
# Licence
MIT
Expand Down
7 changes: 7 additions & 0 deletions react-focus-lock.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions src/Lock.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class FocusLock extends Component {
allowTextSelection,
group,
className,
whiteList,
} = this.props;
const { observed } = this.state;

Expand Down Expand Up @@ -99,6 +100,7 @@ class FocusLock extends Component {
disabled={disabled}
persistentFocus={persistentFocus}
autoFocus={autoFocus}
whiteList={whiteList}
onActivation={this.onActivation}
/>
{children}
Expand All @@ -121,6 +123,8 @@ FocusLock.propTypes = {

group: PropTypes.string,
className: PropTypes.string,

whiteList: PropTypes.func,
};

FocusLock.defaultProps = {
Expand All @@ -132,6 +136,7 @@ FocusLock.defaultProps = {
allowTextSelection: undefined,
group: undefined,
className: undefined,
whiteList: undefined,
};


Expand Down
48 changes: 30 additions & 18 deletions src/Trap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
};
Expand All @@ -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;
Expand Down
41 changes: 41 additions & 0 deletions stories/MUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,47 @@ export const MUISelect = () => (
<MenuItem value={4} primaryText="Weekends"/>
<MenuItem value={5} primaryText="Weekly"/>
</SelectField>

<SelectField floatingLabelText="Handle focus">
<MenuItem value={1} primaryText="Never"/>
<MenuItem value={2} primaryText="Every Night"/>
<MenuItem value={3} primaryText="Weeknights"/>
<MenuItem value={4} primaryText="Weekends"/>
<MenuItem value={5} primaryText="Weekly"/>
</SelectField>
</FocusLock>
<br/>
</MuiThemeProvider>
</div>
);

export const MUISelectWhite = () => (
<div>
<h2>With focus lock active</h2>
<h3>
will work, due to whitelisting
</h3>
<MuiThemeProvider>
<FocusLock
noFocusGuards
persistentFocus={true}
whiteList={node => document.getElementById('root').contains(node)}
>
<SelectField floatingLabelText="Frequency">
<MenuItem value={1} primaryText="Never"/>
<MenuItem value={2} primaryText="Every Night"/>
<MenuItem value={3} primaryText="Weeknights"/>
<MenuItem value={4} primaryText="Weekends"/>
<MenuItem value={5} primaryText="Weekly"/>
</SelectField>

<SelectField floatingLabelText="Handle focus">
<MenuItem value={1} primaryText="Never"/>
<MenuItem value={2} primaryText="Every Night"/>
<MenuItem value={3} primaryText="Weeknights"/>
<MenuItem value={4} primaryText="Weekends"/>
<MenuItem value={5} primaryText="Weekly"/>
</SelectField>
</FocusLock>
<br/>
</MuiThemeProvider>
Expand Down
5 changes: 3 additions & 2 deletions stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -52,7 +52,8 @@ storiesOf('Group', module)
.add('focus group', () => <Frame><GroupCase /></Frame>);

storiesOf('Material UI', module)
.add('Select', () => <Frame><MUISelect /></Frame>);
.add('Select', () => <Frame><MUISelect /></Frame>)
.add('Select White', () => <Frame><MUISelectWhite /></Frame>)

storiesOf('Focus fighting', module)
.add('fight', () => <Frame><Fight /></Frame>);
Expand Down

0 comments on commit 68f3782

Please sign in to comment.