It is a trap! (for a focus)
npm install react-focus-lock




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.
- Including programatic focus management and smart return focus
moved inside on mount.js
import FocusLock from 'react-focus-lock'; const JailForAFocus = ({onClose}) => (
You can not leave this form
);
`
Demo - https://codesandbox.io/s/5wmrwlvxv4.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
> By default returnFocus is disabled, so FocusLock __will not__ restore original focus on deactivation.
> This was done mostly to avoid breaking changes. We __strong recommend enabling it__, to provide a better user experience.
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 targets
- 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=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.
- hasPositiveIndices=false to support a focus lock behavior when any elements tabIndex greater than 0.
- crossFrame=true enables aggressive focus capturing within iframesProgrammatic control
Focus lock exposes a few methods to control focus programmatically.
$3
- useFocusInside(nodeRef) - to move focus inside the given node
- useFocusScope():{autofocus, focusNext, focusPrev} - provides API to manage focus within the current lock
- useFocusState() - manages focus state of a given node
- useFocusController(nodeRef) - low level version of useFocusScope working without FocusLock $3
- - causes autofocus to look inside the component
- - wrapper around useFocusInside, forcibly moves focus inside on mount
- - hides internals from FocusLock allowing unmanaged focus#### Indirect API
Focus-lock behavior can be controlled via data-attributes. Declarative API above is working by setting them for you.
See corresponding section in focus-lock for details
$3
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
$3
- version 1x is React 15/16 compatible
- version 2+ requires React 16.8+ (hooks)
$3
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.`js
import FocusLockUI from "react-focus-lock/UI";
import {sidecar} from "use-sidecar";// prefetch sidecar. data would be loaded, but js would not be executed
const FocusLockSidecar = sidecar(
() => import(/ webpackPrefetch: true / "react-focus-lock/sidecar")
);
disabled={this.state.disabled}
sideCar={FocusLockSidecar}
>
{content}
`
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.
`js
import FocusLock, { AutoFocusInside } from 'react-focus-lock';
// is the same as
`
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.
`js
import { MoveFocusInside } from 'react-focus-lock';
`
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 = () => (
// "discoverable" portaled content
);
// main content
`
- using shards. Just pass all the pieces to the "shards" prop.
`js
const PortaledElement = () => (
// "discoverable" portaled content
);
// main content
`
- without anything. FocusLock will not prevent focusing portaled element, but will not include them in to tab order
`js
const PortaledElement = () => (
// NON-"discoverable" portaled content
);
// main content
`$3
You may use as prop to change _what_ Focus-Lock will render around children.
`js
Hello there!
` $3
Let's take a look at the Rowing Focus as an example.
`tsx
// Will set tabindex to -1 when is not focused
const FocusTrackingButton = ({ children }) => {
const { active, onFocus, ref } = useFocusState();
return (
);
};
const RowingFocusInternalTrap = () => {
const { autoFocus, focusNext, focusPrev } = useFocusScope();
// use useFocusController(divRef) if there is no FocusLock around useEffect(() => {
autoFocus();
}, []);
const onKey = (event) => {
if (event.key === 'ArrowDown') {
focusNext({ onlyTabbable: false });
}
if (event.key === 'ArrowUp') {
focusPrev({ onlyTabbable: false });
}
};
return (
onKeyDown={onKey}
// ref={divRef} for useFocusController
>
Button1
Button2
Button3
Button4
// FocusLock, even disabled one
const RowingFocusTrap = () => (
);
`
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';// wrap with
// place before and after
`
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.
`js
// there is no "tailing" guard :)
`
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.
`
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 to another node
In some cases the original node that was focused before the lock was activated is not the desired node to return focus to.
Some times this node might not exists at all.- first of all, FocusLock need a moment to record this node, please do not hide it onClick, but hide onBlur (Dropdown, looking at you)
- second, you may specify a callback as
returnFocus, letting you decide where to return focus to.
`tsx
returnFocus={(suggestedNode) => {
// somehow activeElement should not be changed
if(document.activeElement.hasAttributes('main-content')) {
// opt out from default behavior
return false;
}
if (someCondition(suggestedNode)) {
// proceed with the suggested node
return true;
}
// handle return focus manually
document.getElementById('the-button').focus();
// opt out from default behavior
return false;
}}
/>
``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
`js
returnFocus={{ preventScroll: false }} // working not in all browsers
>
`
Not supported by Edge and Safari.Focus fighting
Two different _focus-lock-managers_ or even different version of a single one, being active
simultaneously will FIGHT for the focus. This usually totally breaks user experience.__React-Focus-Lock will automatically surrender__, letting another library to take the lead.
$3
You may wrap some render branch with FreeFocusInside, and react-focus-lock __will ignore__
any focus inside marked node. So in case focus moves to _uncontrolled location_ focus-lock will not trigger letting another library to act without interference in that another location.`js
import { FreeFocusInside } from 'react-focus-lock';
in this div i am going to portal my modals, dont fight with them please
`
Another option for hybrid applications is to whiteList area where Focus-Lock should act, automatically allowing other managers in other areas.
The code below will scope Focus-Lock on inside the (react)root element, so anything jQuery can add to the body will be ignored.
`js
document.getElementById('root').contains(node)}>
...
`$3
React-Focus-Lock is expected to be a singleton.
__Use webpack or yarn resolution for force only one version of react-focus-lock used.> webpack.conf
`js
resolve: {
alias: {
'react-focus-lock': path.resolve(path.join(__dirname, './node_modules/react-focus-lock'))
...
``This one is about managing the focus.
I've got a good article about focus management, dialogs and WAI-ARIA.
You may use react-focus-on to achieve everything above, assembled in the right order.