It is a trap! We got your focus and will not let him out!
- Modal dialogs. You can not leave it with "Tab", ie do a "tab-out".
- Focused tasks. It will aways brings you back, as you can "lock" user inside a component.
- Any any other case, when you have to lock user intention and focus, if that's what
a11y
is asking for.
Trusted
Trusted by
Atlassian AtlasKit,
ReachUI,
SmoothUI,
Storybook
and we will do our best to earn your trust too!
Features
- no keyboard control, everything is done watching a focus behavior, not emulating it. Thus works always and everywhere.
- React Portals support. Even if some data is in outer space - it is still in lock.
- Scattered locks, or focus lock groups - you can setup different isolated locks, and tab from one to another.
- Controllable isolation level.
- variable size bundle. Uses sidecar to trim UI part to 1.5kb.
💡 focus locks is only the first part, there are also scroll lock and text-to-speech lock
you have to use to really "lock" the user.
Try react-focus-on to archive everything above, assembled in the right order.
How to use
Just wrap something with focus lock, and focus will be moved inside
on mount.
import FocusLock from 'react-focus-lock';
const JailForAFocus = ({onClose}) => (
<FocusLock>
You can not leave this form
<button onClick={onClose} />
</FocusLock>
);
Demo - https://codesandbox.io/s/5wmrwlvxv4.
WHY?
From MDN Article about accessible dialogs:
- The dialog must be properly labeled
- Keyboard focus must be managed correctly
This one is about managing the focus.
I've got a good article about focus management, dialogs and WAI-ARIA.
API
FocusLock would work perfectly even with no props set.
FocusLock has few props to tune behavior, all props are optional:
disabled
, to disable(enable) behavior without altering the tree.className
, to set the className
of the internal wrapper.returnFocus
, to return focus into initial position on unmount(not disable).
By default returnFocus
is disabled, so FocusLock will not restore original focus on deactivation.
This is expected behavior for Modals, but it is better to implement it by your self. See unmounting and focus management for details
persistentFocus=false
, requires any element to be focused. This also disables text selections inside, and outside focus lock.autoFocus=true
, enables or disables focusing into on Lock activation. If disabled Lock will blur an active focus.noFocusGuards=false
disabled focus guards - virtual inputs which secure tab index.group='''
named focus group for focus scattering aka combined lock targetsshards=[]
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=fn
you could whitelist locations FocusLock should carry about. Everything outside it will ignore. For example - any modals.as='div'
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.
Focusing in OSX (Safari/Firefox) is strange!
By default tabbing
in OSX sees
only controls, but not links or anything else tabbable
. This is system settings, and Safari/Firefox obey.
Press Option+Tab in Safari to loop across all tabbables, or change the Safari settings. There is no way to fix Firefox, unless change system settings (Control+F7). See this issue for more information.
Set up
Requirements
- version 1x is React 15/16 compatible
- version 2+ requires React 16.8+ (hooks)
Import
react-focus-lock
exposed 3 entry points: for the classical usage, and a sidecar one.
Default usage
- 4kb,
import FocusLock from 'react-focus-lock
would give you component you are looking for.
Separated usage
Meanwhile - you dont need any focus related logic until it's needed.
Thus - you may defer that logic till Lock activation and move all related code to a sidecar.
- UI, 1.5kb,
import FocusLockUI from 'react-focus-lock/UI
- a DOM part of a lock. - Sidecar, 3.5kb,
import Sidecar from 'react-focus-lock/sidecar
- which is the real focus lock.
import FocusLockUI from "react-focus-lock/UI";
import {sidecar} from "use-sidecar";
const FocusLockSidecar = sidecar(
() => import( "react-focus-lock/sidecar")
);
<FocusLockUI
disabled={this.state.disabled}
sideCar={FocusLockSidecar}
>
{content}
</FocusLockUI>
That would split FocusLock into two pieces, reducing app size and improving the first load.
The cost of focus-lock is just 1.5kb!
Saved 3.5kb?! 🤷♂️ 3.5kb here and 3.5kb here, and your 20mb bundle is ready.
Autofocus
Use when you cannot use the native autoFocus
prop - because you only want to autofocus once the Trap has been activated
- prop
data-autofocus
on the element. - prop
data-autofocus-inside
on the element to focus on something inside. AutoFocusInside
component, as named export of this library.
import FocusLock, { AutoFocusInside } from 'react-focus-lock';
<FocusLock>
<button>Click</button>
<AutoFocusInside>
<button>will be focused</button>
</AutoFocusInside>
</FocusLock>
<FocusLock>
<button>Click</button>
<button data-autofocus>will be focused</button>
</FocusLock>
If there is more than one auto-focusable target - the first will be selected.
If it is a part of radio group, and rest of radio group element are also autofocusable(just put them into AutoFocusInside) -
checked one fill be selected.
AutoFocusInside
will work only on Lock activation, and does nothing, then used outside of the lock.
You can use MoveFocusInside
to move focus inside with or without lock.
import { MoveFocusInside } from 'react-focus-lock';
<MoveFocusInside>
<button>will be focused</button>
</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
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.
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
const PortaledElement = () => (
<div>
// NON-"discoverable" portaled content
</div>
);
<FocusLock shards={[ref]}>
// main content
<PortaledElement />
</FocusLock>
Using your own Components
You may use as
prop to change what Focus-Lock will render around children
.
<FocusLock as="section">
<button>Click</button>
<button data-autofocus>will be focused</button>
</FocusLock>
<FocusLock as={AnotherComponent} lockProps={{anyAnotherComponentProp: 4}}>
<button>Click</button>
<span>Hello there!</span>
</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.
import {InFocusGuard} from 'react-focus-lock';
<InFocusGuard>
<button />
</InFocusGuard>
<InFocusGuard />
<button />
<InFocusGuard />
InFocusGuards would be active(tabbable) only when tabble, it protecting, is focused.
Removing Tailing Guard
If only your modal is the last tabble element on the body - you might remove the Tailing Guard,
to allow user tab into address bar.
<InFocusGuard/>
<button />
Unmounting and focus management
- In case FocusLock has
returnFocus
enabled, and it's going to be unmounted - focus will be returned after zero-timeout. - In case
returnFocus
is set to false
, and you are going to control focus change on your own - keep in mind
React will first call Parent.componentWillUnmount, and next Child.componentWillUnmount
This means - Trap will be still active by the time you may want move(return) focus on componentWillUnmount. Please deffer this action with a zero-timeout.
Similarly, if you are using the disabled
prop to control FocusLock, you will need a zero-timeout to correctly restore focus.
<FocusLock
disabled={isFocusLockDisabled}
onDeactivation={() => {
// Without the zero-timeout, focus will likely remain on the button/control
// you used to set isFocusLockDisabled = true
window.setTimeout(() => myRef.current.focus(), 0);
}
>
Return focus with no scroll
read more at the issue #83 or
mdn article.
To return focus, but without jumpy page scroll returning a focus you might specify a focus option
<FocusLock
returnFocus={{ preventScroll: false }}
>
Not supported by Edge and Safari.
Not only for React
Uses focus-lock under the hood. It does also provide support for Vue.js and Vanilla DOM solutions
Warning!
Two different focus-lock-managers or even different version of a single one, active
simultaneously will FIGHT!
Focus-lock will surrender, as long any other focus management library will not.
Focus fighting
You may wrap some render branch with FreeFocusInside
, and react-focus-lock will ignore
any focus inside marked node, thus landing a peace.
import { FreeFocusInside } from 'react-focus-lock';
<FreeFocusInside>
<div id="portal-for-modals">
in this div i am going to portal my modals, dont fight with them please
</div>
</FreeFocusInside>
Even the better is to whiteList
FocusLock areas - for example "you should handle only React Stuff in React Root"
<FocusLock whiteList={node => document.getElementById('root').contains(node)}>
...
</FocusLock>
PS: please use webpack or yarn resolution for force one version of react-focus-lock used
webpack.conf
resolve: {
alias: {
'react-focus-lock': path.resolve(path.join(__dirname, './node_modules/react-focus-lock'))
...
More
To create a "right" modal dialog you have to:
You may use react-focus-on to achieve everything above, assembled in the right order.
Licence
MIT