Skip to content

Commit

Permalink
feat: introduce shards
Browse files Browse the repository at this point in the history
  • Loading branch information
theKashey committed Mar 10, 2019
1 parent 40018fd commit 7bc46f0
Show file tree
Hide file tree
Showing 15 changed files with 1,325 additions and 127 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
/dist/
.DS_Store
.cache
.cache
yarn-error.log
2 changes: 1 addition & 1 deletion .size-limit
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
{
path: "dist/es2015/index.js",
limit: "5 KB",
ignore: ["prop-types"]
ignore: ["prop-types", "\\@babel/runtime"]
}
]
2 changes: 1 addition & 1 deletion .storybook/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const path = require('path');

module.exports = (baseConfig, env, defaultConfig) => {

defaultConfig.resolve.alias['../src/index'] = path.resolve(__dirname, '../dist/es2015/index.js')
// defaultConfig.resolve.alias['../src/index'] = path.resolve(__dirname, '../dist/es2015/index.js')

return defaultConfig;
};
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ I've got a good [article about focus management, dialogs and WAI-ARIA](https://
- `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)
- `shards` an array of `ref` pointing to the nodes, which focus lock should consider and a part of it. This is another way focus scattering.
- `whiteList` you could _whitelist_ locations FocusLock should carry about. Everything outside it will ignore. For example - any modals.
- `as` if you need to change internal `div` element, to any other. Use ref forwarding to give FocusLock the node to work with.
- `lockProps` to pass any extra props (except className) to the internal wrapper.
Expand Down Expand Up @@ -124,6 +125,70 @@ Press Option+Tab in Safary to loop across all tabbables, or change the Safary se
</MoveFocusInside>
```

# Portals
Use focus scattering to handle portals

- using `groups`. Just create a few locks (only one could be active) with a same group name
```js
const PortaledElement = () => (
<FocusLock group="group42" disabled={true}>
// "discoverable" portaled content
</FocusLock>
);

<FocusLock group="group42">
// main content
</FocusLock>
```
- using `shards`. Just pass all the pieces to the "shards" prop.
```js
const PortaledElement = () => (
<div ref={ref}>
// "discoverable" portaled content
</div>
);

<FocusLock shards={[ref]}>
// main content
</FocusLock>
```
- without anything. FocusLock will not prevent focusing portaled element, but will not include them in to tab order
```js
const PortaledElement = () => (
<div>
// NON-"discoverable" portaled content
</div>
);

<FocusLock shards={[ref]}>
// main content
<PortaledElement />
</FocusLock>
```

### Guarding
As you may know - FocusLock is adding `Focus Guards` before and after lock to remove some side effects, like page scrolling.
But `shards` will not have such guards, and it might be not so cool to use them - for example if no `tabbable` would be
defined after shard - you will tab to the browser chrome.

You may wrap shard with `InFocusGuard` or just drop `InFocusGuard` here and there - that would solve the problem.
```js
import {InFocusGuard} from 'react-focus-lock';

<InFocusGuard>
<button>
</InFocusGuard>

//

<InFocusGuard />
<button>
<InFocusGuard />
```
InFocusGuards would be active(tabbable) only when tabble, it protecting, is focused.

### Automatic potral discovery

# Unmounting and focus management
- In case FocusLock has `returnFocus` enabled, and it's gonna to be unmounted - focus will be returned after zero-timeout.
- In case `returnFocus` did not set, and you are going to control focus change by your own - keep in mind
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@
"react-hot-loader": "^3.0.0-beta.7",
"react-test-renderer": "^16.2.0",
"sinon": "3.2.1",
"size-limit": "^0.21.0"
"size-limit": "^0.21.1"
},
"homepage": "https://github.com/theKashey/react-focus-lock#readme",
"dependencies": {
"@babel/runtime": "^7.0.0",
"focus-lock": "^0.5.2",
"focus-lock": "^0.6.0",
"prop-types": "^15.6.2",
"react-clientside-effect": "^1.2.0"
}
Expand Down
15 changes: 15 additions & 0 deletions react-focus-lock.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ declare module 'react-focus-lock' {
*/
whiteList?: (activeElement: HTMLElement) => boolean;

/**
* Shards forms a scattered lock, same as `group` does, but in more "low" and controlled way
*/
shards?: Array<React.RefObject<any>>;

children: React.ReactNode;
}

Expand All @@ -79,6 +84,10 @@ declare module 'react-focus-lock' {
className?: string;
}

interface InFocusGuardProps {
children: React.ReactNode;
}

/**
* Traps Focus inside a Lock
*/
Expand All @@ -102,4 +111,10 @@ declare module 'react-focus-lock' {
*/
export class FreeFocusInside extends React.Component<FreeFocusProps> {
}

/**
* Secures the focus around the node
*/
export class InFocusGuard extends React.Component<InFocusGuardProps> {
}
}
26 changes: 26 additions & 0 deletions src/FocusGuard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';

export const hiddenGuard = {
width: '1px',
height: '0px',
padding: 0,
overflow: 'hidden',
position: 'fixed',
top: '1px',
left: '1px',
};

const InFocusGuard = ({children}) => (
<React.Fragment>
<div key="guard-first" data-focus-guard data-focus-auto-guard style={hiddenGuard}/>
{children}
{children && <div key="guard-last" data-focus-guard data-focus-auto-guard style={hiddenGuard}/>}
</React.Fragment>
);
InFocusGuard.propTypes = {
children: PropTypes.node,
};


export default InFocusGuard;
35 changes: 18 additions & 17 deletions src/Lock.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
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 {constants} from 'focus-lock';
import FocusTrap, {onBlur, onFocus} from './Trap';
import {hiddenGuard} from './FocusGuard';

const RenderChildren = ({ children }) => <div>{children}</div>;
const RenderChildren = ({children}) => <div>{children}</div>;
RenderChildren.propTypes = {
children: PropTypes.node.isRequired,
};

const Fragment = React.Fragment ? React.Fragment : RenderChildren;

const hidden = {
width: '1px',
height: '0px',
padding: 0,
overflow: 'hidden',
position: 'fixed',
top: '1px',
left: '1px',
};
const emptyArray = [];

class FocusLock extends Component {
state = {
Expand Down Expand Up @@ -83,10 +77,11 @@ class FocusLock extends Component {
group,
className,
whiteList,
shards = emptyArray,
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') {
Expand All @@ -104,8 +99,8 @@ class FocusLock extends Component {
return (
<Fragment>
{!noFocusGuards && [
<div key="guard-first" data-focus-guard tabIndex={disabled ? -1 : 0} style={hidden} />, // nearest focus guard
<div key="guard-nearest" data-focus-guard tabIndex={disabled ? -1 : 1} style={hidden} />, // first tabbed element guard
<div key="guard-first" data-focus-guard tabIndex={disabled ? -1 : 0} style={hiddenGuard}/>, // nearest focus guard
<div key="guard-nearest" data-focus-guard tabIndex={disabled ? -1 : 1} style={hiddenGuard}/>, // first tabbed element guard
]}
<Container
ref={this.setObserveNode}
Expand All @@ -120,12 +115,16 @@ class FocusLock extends Component {
persistentFocus={persistentFocus}
autoFocus={autoFocus}
whiteList={whiteList}
shards={shards}
onActivation={this.onActivation}
onDeactivation={this.onDeactivation}
/>
{children}
</Container>
{!noFocusGuards && <div data-focus-guard tabIndex={disabled ? -1 : 0} style={hidden} />}
{
!noFocusGuards &&
<div data-focus-guard tabIndex={disabled ? -1 : 0} style={hiddenGuard}/>
}
</Fragment>
);
}
Expand All @@ -145,6 +144,7 @@ FocusLock.propTypes = {
className: PropTypes.string,

whiteList: PropTypes.func,
shards: PropTypes.arrayOf(PropTypes.shape({current: PropTypes.instanceOf(Element)})),

as: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
lockProps: PropTypes.object,
Expand All @@ -163,6 +163,7 @@ FocusLock.defaultProps = {
group: undefined,
className: undefined,
whiteList: undefined,
shards: undefined,
as: 'div',
lockProps: {},
onActivation: undefined,
Expand Down
83 changes: 60 additions & 23 deletions src/Trap.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import withSideEffect from 'react-clientside-effect';
import moveFocusInside, { focusInside, focusIsHidden } 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
Expand All @@ -22,38 +22,75 @@ const focusWhitelisted = activeElement => (
);

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

const focusIsPortaledPair = element => (
lastPortaledElement && lastPortaledElement.portaledElement === element
);

function autoGuard(startIndex, end, step, allNodes) {
let lastGuard = null;
let i = startIndex;
do {
const node = allNodes[i];
if (node.guard) {
lastGuard = node;
} else if (node.lockItem) {
lastGuard = null;
} else {
break;
}
} while ((i += step) !== end);
if (lastGuard) {
lastGuard.node.tabIndex = 0;
}
}

const activateTrap = () => {
let result = false;
if (lastActiveTrap) {
const { observed, persistentFocus, autoFocus } = lastActiveTrap;
const {observed, persistentFocus, autoFocus, shards} = lastActiveTrap;
const workingNode = observed || (lastPortaledElement && lastPortaledElement.portaledElement);
const activeElement = document && document.activeElement;

if (!activeElement || focusWhitelisted(activeElement)) {
if (persistentFocus || !isFreeFocus() || (!lastActiveFocus && autoFocus)) {
if (
workingNode &&
!(
focusInside(workingNode) ||
focusIsPortaledPair(activeElement, workingNode)
)
) {
if (document && !lastActiveFocus && activeElement && !autoFocus) {
activeElement.blur();
document.body.focus();
} else {
result = moveFocusInside(workingNode, lastActiveFocus);
lastPortaledElement = {};
if (workingNode) {
const workingArea = [workingNode, ...shards.map(({current}) => current)];

if (!activeElement || focusWhitelisted(activeElement)) {
if (persistentFocus || !isFreeFocus() || (!lastActiveFocus && autoFocus)) {
if (
workingNode &&
!(
focusInside(workingArea) ||
focusIsPortaledPair(activeElement, workingNode)
)
) {
if (document && !lastActiveFocus && activeElement && !autoFocus) {
activeElement.blur();
document.body.focus();
} else {
result = moveFocusInside(workingArea, lastActiveFocus);
lastPortaledElement = {};
}
}
lastActiveFocus = document && document.activeElement;
}
}

if (document) {
const newActiveElement = document && document.activeElement;
const allNodes = getFocusabledIn(workingArea);
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'));

const focusedIndex = allNodes.indexOf(focusedItem);
autoGuard(focusedIndex, allNodes.length, +1, allNodes);
autoGuard(focusedIndex, -1, -1, allNodes);
}
lastActiveFocus = document && document.activeElement;
}
}
}
Expand Down Expand Up @@ -83,7 +120,7 @@ export const onFocus = (event) => {

const FocusWatcher = () => null;

const FocusTrap = ({ children }) => (
const FocusTrap = ({children}) => (
<div onBlur={onBlur} onFocus={onFocus}>
{children}
</div>
Expand All @@ -106,7 +143,7 @@ const detachHandler = () => {

function reducePropsToState(propsList) {
return propsList
.filter(({ disabled }) => !disabled)
.filter(({disabled}) => !disabled)
.slice(-1)[0];
}

Expand Down
Loading

0 comments on commit 7bc46f0

Please sign in to comment.