1 | <div align="left">
|
2 | <h1 align="center">REACT FOCUS LOCK</h1>
|
3 | <img src="./assets/ackbar.png" alt="it-is-a-trap" width="200" height="200" align="right">
|
4 |
|
5 | - browser friendly focus lock<br/>
|
6 | - matching all your use cases<br/>
|
7 | - trusted by best UI frameworks<br/>
|
8 | - the thing Admiral Ackbar was talking about<br/>
|
9 | <br/>
|
10 |
|
11 | [![CircleCI status](https://img.shields.io/circleci/project/github/theKashey/react-focus-lock/master.svg?style=flat-square)](https://circleci.com/gh/theKashey/react-focus-lock/tree/master)
|
12 | [![npm](https://img.shields.io/npm/v/react-focus-lock.svg)](https://www.npmjs.com/package/react-focus-lock)
|
13 | [![bundle size](https://badgen.net/bundlephobia/minzip/react-focus-lock)](https://bundlephobia.com/result?p=react-focus-lock)
|
14 | [![downloads](https://badgen.net/npm/dm/react-focus-lock)](https://www.npmtrends.com/react-focus-lock)
|
15 | <hr/>
|
16 | </div>
|
17 |
|
18 | It is a trap! We got your focus and will not let him out!
|
19 |
|
20 | - Modal dialogs. You can not leave it with "Tab", ie do a "tab-out".
|
21 | - Focused tasks. It will aways brings you back, as you can "lock" user inside a component.
|
22 | - Any any other case, when you have to lock user _intention_ and _focus_, if that's what `a11y` is asking for.
|
23 |
|
24 | ### Trusted
|
25 | Trusted by
|
26 | [Atlassian AtlasKit](https://atlaskit.atlassian.com),
|
27 | [ReachUI](https://ui.reach.tech/),
|
28 | [SmoothUI](https://smooth-ui.smooth-code.com/),
|
29 | [Storybook](https://storybook.js.org/)
|
30 | and we will do our best to earn your trust too!
|
31 |
|
32 | # Features
|
33 | - no keyboard control, everything is done watching a __focus behavior__, not emulating it. Thus works always and everywhere.
|
34 | - React __Portals__ support. Even if some data is in outer space - it is [still in lock](https://github.com/theKashey/react-focus-lock/issues/19).
|
35 | - _Scattered_ locks, or focus lock groups - you can setup different isolated locks, and _tab_ from one to another.
|
36 | - Controllable isolation level.
|
37 | - variable size bundle. Uses sidecar to trim UI part to 1.5kb.
|
38 |
|
39 | > 💡 __focus__ locks is only the first part, there are also __scroll lock__ and __text-to-speech__ lock
|
40 | you have to use to really "lock" the user.
|
41 | Try [react-focus-on](https://github.com/theKashey/react-focus-on) to archive everything above, assembled in the right order.
|
42 |
|
43 | # How to use
|
44 | Just wrap something with focus lock, and focus will be `moved inside` on mount.
|
45 | ```js
|
46 | import FocusLock from 'react-focus-lock';
|
47 |
|
48 | const JailForAFocus = ({onClose}) => (
|
49 | <FocusLock>
|
50 | You can not leave this form
|
51 | <button onClick={onClose} />
|
52 | </FocusLock>
|
53 | );
|
54 | ```
|
55 | Demo - https://codesandbox.io/s/5wmrwlvxv4.
|
56 |
|
57 | # WHY?
|
58 | From [MDN Article about accessible dialogs](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_dialog_role):
|
59 | - The dialog must be properly labeled
|
60 | - Keyboard __focus must be managed__ correctly
|
61 |
|
62 | This one is about managing the focus.
|
63 |
|
64 | I've got a good [article about focus management, dialogs and WAI-ARIA](https://medium.com/@antonkorzunov/its-a-focus-trap-699a04d66fb5).
|
65 |
|
66 | # API
|
67 | > FocusLock would work perfectly even with no props set.
|
68 |
|
69 | FocusLock has few props to tune behavior, all props are optional:
|
70 | - `disabled`, to disable(enable) behavior without altering the tree.
|
71 | - `className`, to set the `className` of the internal wrapper.
|
72 | - `returnFocus`, to return focus into initial position on unmount(not disable).
|
73 | > By default `returnFocus` is disabled, so FocusLock will __not__ restore original focus on deactivation.
|
74 |
|
75 | This is expected behavior for Modals, but it is better to implement it by your self. See [unmounting and focus management](https://github.com/theKashey/react-focus-lock#unmounting-and-focus-management) for details
|
76 | - `persistentFocus=false`, requires any element to be focused. This also disables text selections inside, and __outside__ focus lock.
|
77 | - `autoFocus=true`, enables or disables focusing into on Lock activation. If disabled Lock will blur an active focus.
|
78 | - `noFocusGuards=false` disabled _focus guards_ - virtual inputs which secure tab index.
|
79 | - `group='''` named focus group for focus scattering aka [combined lock targets](https://github.com/theKashey/vue-focus-lock/issues/2)
|
80 | - `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.
|
81 | - `whiteList=fn` you could _whitelist_ locations FocusLock should carry about. Everything outside it will ignore. For example - any modals.
|
82 | - `as='div'` if you need to change internal `div` element, to any other. Use ref forwarding to give FocusLock the node to work with.
|
83 | - `lockProps={}` to pass any extra props (except className) to the internal wrapper.
|
84 | - `hasPositiveIndices=false` to support a focus lock behavior when any elements tabIndex greater than 0.
|
85 |
|
86 | ### Focusing in OSX (Safari/Firefox) is strange!
|
87 | By default `tabbing` in OSX `sees` only controls, but not links or anything else `tabbable`. This is system settings, and Safari/Firefox obey.
|
88 | 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](https://github.com/theKashey/react-focus-lock/issues/24) for more information.
|
89 |
|
90 | ## Set up
|
91 | ### Requirements
|
92 | - version 1x is React 15/16 compatible
|
93 | - version 2+ requires React 16.8+ (hooks)
|
94 | ### Import
|
95 | `react-focus-lock` exposed __3 entry points__: for the classical usage, and a _sidecar_ one.
|
96 | #### Default usage
|
97 | - 4kb, `import FocusLock from 'react-focus-lock` would give you component you are looking for.
|
98 |
|
99 | #### Separated usage
|
100 | Meanwhile - you dont need any focus related logic until it's needed.
|
101 | Thus - you may defer that logic till Lock activation and move all related code to a _sidecar_.
|
102 |
|
103 | - UI, __1.5kb__, `import FocusLockUI from 'react-focus-lock/UI` - a DOM part of a lock.
|
104 | - Sidecar, 3.5kb, `import Sidecar from 'react-focus-lock/sidecar` - which is the real focus lock.
|
105 |
|
106 | ```js
|
107 | import FocusLockUI from "react-focus-lock/UI";
|
108 | import {sidecar} from "use-sidecar";
|
109 |
|
110 | // prefetch sidecar. data would be loaded, but js would not be executed
|
111 | const FocusLockSidecar = sidecar(
|
112 | () => import(/* webpackPrefetch: true */ "react-focus-lock/sidecar")
|
113 | );
|
114 |
|
115 | <FocusLockUI
|
116 | disabled={this.state.disabled}
|
117 | sideCar={FocusLockSidecar}
|
118 | >
|
119 | {content}
|
120 | </FocusLockUI>
|
121 | ```
|
122 | That would split FocusLock into two pieces, reducing app size and improving the first load.
|
123 | The cost of focus-lock is just 1.5kb!
|
124 |
|
125 | > Saved 3.5kb?! 🤷♂️ 3.5kb here and 3.5kb here, and your 20mb bundle is ready.
|
126 |
|
127 | # Autofocus
|
128 | Use when you cannot use the native `autoFocus` prop - because you only want to autofocus once the Trap has been activated
|
129 |
|
130 | - prop `data-autofocus` on the element.
|
131 | - prop `data-autofocus-inside` on the element to focus on something inside.
|
132 | - `AutoFocusInside` component, as named export of this library.
|
133 | ```js
|
134 | import FocusLock, { AutoFocusInside } from 'react-focus-lock';
|
135 |
|
136 | <FocusLock>
|
137 | <button>Click</button>
|
138 | <AutoFocusInside>
|
139 | <button>will be focused</button>
|
140 | </AutoFocusInside>
|
141 | </FocusLock>
|
142 | // is the same as
|
143 |
|
144 | <FocusLock>
|
145 | <button>Click</button>
|
146 | <button data-autofocus>will be focused</button>
|
147 | </FocusLock>
|
148 | ```
|
149 |
|
150 | If there is more than one auto-focusable target - the first will be selected.
|
151 | If it is a part of radio group, and __rest of radio group element are also autofocusable__(just put them into AutoFocusInside) -
|
152 | checked one fill be selected.
|
153 |
|
154 | `AutoFocusInside` will work only on Lock activation, and does nothing, then used outside of the lock.
|
155 | You can use `MoveFocusInside` to move focus inside with or without lock.
|
156 |
|
157 | ```js
|
158 | import { MoveFocusInside } from 'react-focus-lock';
|
159 |
|
160 | <MoveFocusInside>
|
161 | <button>will be focused</button>
|
162 | </MoveFocusInside>
|
163 | ```
|
164 |
|
165 | # Portals
|
166 | Use focus scattering to handle portals
|
167 |
|
168 | - using `groups`. Just create a few locks (only one could be active) with a same group name
|
169 | ```js
|
170 | const PortaledElement = () => (
|
171 | <FocusLock group="group42" disabled={true}>
|
172 | // "discoverable" portaled content
|
173 | </FocusLock>
|
174 | );
|
175 |
|
176 | <FocusLock group="group42">
|
177 | // main content
|
178 | </FocusLock>
|
179 | ```
|
180 | - using `shards`. Just pass all the pieces to the "shards" prop.
|
181 | ```js
|
182 | const PortaledElement = () => (
|
183 | <div ref={ref}>
|
184 | // "discoverable" portaled content
|
185 | </div>
|
186 | );
|
187 |
|
188 | <FocusLock shards={[ref]}>
|
189 | // main content
|
190 | </FocusLock>
|
191 | ```
|
192 | - without anything. FocusLock will not prevent focusing portaled element, but will not include them in to tab order
|
193 | ```js
|
194 | const PortaledElement = () => (
|
195 | <div>
|
196 | // NON-"discoverable" portaled content
|
197 | </div>
|
198 | );
|
199 |
|
200 | <FocusLock shards={[ref]}>
|
201 | // main content
|
202 | <PortaledElement />
|
203 | </FocusLock>
|
204 | ```
|
205 |
|
206 | ### Using your own `Components`
|
207 | You may use `as` prop to change _what_ Focus-Lock will render around `children`.
|
208 | ```js
|
209 | <FocusLock as="section">
|
210 | <button>Click</button>
|
211 | <button data-autofocus>will be focused</button>
|
212 | </FocusLock>
|
213 |
|
214 | <FocusLock as={AnotherComponent} lockProps={{anyAnotherComponentProp: 4}}>
|
215 | <button>Click</button>
|
216 | <span>Hello there!</span>
|
217 | </FocusLock>
|
218 | ```
|
219 |
|
220 | ### Guarding
|
221 | As you may know - FocusLock is adding `Focus Guards` before and after lock to remove some side effects, like page scrolling.
|
222 | But `shards` will not have such guards, and it might be not so cool to use them - for example if no `tabbable` would be
|
223 | defined after shard - you will tab to the browser chrome.
|
224 |
|
225 | You may wrap shard with `InFocusGuard` or just drop `InFocusGuard` here and there - that would solve the problem.
|
226 | ```js
|
227 | import {InFocusGuard} from 'react-focus-lock';
|
228 |
|
229 | // wrap with
|
230 | <InFocusGuard>
|
231 | <button />
|
232 | </InFocusGuard>
|
233 |
|
234 | // place before and after
|
235 | <InFocusGuard />
|
236 | <button />
|
237 | <InFocusGuard />
|
238 | ```
|
239 | InFocusGuards would be active(tabbable) only when tabble, it protecting, is focused.
|
240 |
|
241 | #### Removing Tailing Guard
|
242 | If only your modal is the last tabble element on the body - you might remove the Tailing Guard,
|
243 | to allow user _tab_ into address bar.
|
244 | ```js
|
245 | <InFocusGuard/>
|
246 | <button />
|
247 | // there is no "tailing" guard :)
|
248 | ```
|
249 |
|
250 | # Unmounting and focus management
|
251 | - In case FocusLock has `returnFocus` enabled, and it's going to be unmounted - focus will be returned after zero-timeout.
|
252 | - In case `returnFocus` is set to `false`, and you are going to control focus change on your own - keep in mind
|
253 | >> React will first call Parent.componentWillUnmount, and next Child.componentWillUnmount
|
254 |
|
255 | 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.
|
256 |
|
257 | Similarly, if you are using the `disabled` prop to control FocusLock, you will need a zero-timeout to correctly restore focus.
|
258 |
|
259 | ```
|
260 | <FocusLock
|
261 | disabled={isFocusLockDisabled}
|
262 | onDeactivation={() => {
|
263 | // Without the zero-timeout, focus will likely remain on the button/control
|
264 | // you used to set isFocusLockDisabled = true
|
265 | window.setTimeout(() => myRef.current.focus(), 0);
|
266 | }
|
267 | >
|
268 | ```
|
269 |
|
270 | ## Return focus with no scroll
|
271 | > read more at the [issue #83](https://github.com/theKashey/react-focus-lock/issues/83) or
|
272 | [mdn article](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus).
|
273 |
|
274 | To return focus, but without _jumpy_ page scroll returning a focus you might specify a focus option
|
275 | ```js
|
276 | <FocusLock
|
277 | returnFocus={{ preventScroll: false }} // working not in all browsers
|
278 | >
|
279 | ```
|
280 | Not supported by Edge and Safari.
|
281 |
|
282 | # Not only for React
|
283 | Uses [focus-lock](https://github.com/theKashey/focus-lock/) under the hood. It does also provide support for Vue.js and Vanilla DOM solutions
|
284 |
|
285 | # Warning!
|
286 | Two different _focus-lock-managers_ or even different version of a single one, active
|
287 | simultaneously will FIGHT!
|
288 |
|
289 | __Focus-lock will surrender__, as long any other focus management library will not.
|
290 |
|
291 | ## Focus fighting
|
292 | You may wrap some render branch with `FreeFocusInside`, and react-focus-lock __will ignore__
|
293 | any focus inside marked node, thus landing a peace.
|
294 |
|
295 | ```js
|
296 | import { FreeFocusInside } from 'react-focus-lock';
|
297 |
|
298 | <FreeFocusInside>
|
299 | <div id="portal-for-modals">
|
300 | in this div i am going to portal my modals, dont fight with them please
|
301 | </div>
|
302 | </FreeFocusInside>
|
303 | ```
|
304 |
|
305 | Even the better is to `whiteList` FocusLock areas - for example "you should handle only React Stuff in React Root"
|
306 | ```js
|
307 | <FocusLock whiteList={node => document.getElementById('root').contains(node)}>
|
308 | ...
|
309 | </FocusLock>
|
310 | ```
|
311 |
|
312 | PS: __please use webpack or yarn resolution for force one version of react-focus-lock used__
|
313 |
|
314 | > webpack.conf
|
315 | ```js
|
316 | resolve: {
|
317 | alias: {
|
318 | 'react-focus-lock': path.resolve(path.join(__dirname, './node_modules/react-focus-lock'))
|
319 | ...
|
320 | ```
|
321 |
|
322 | # More
|
323 | To create a "right" modal dialog you have to:
|
324 | - manage a focus. Use this library
|
325 | - block document scroll. Use [react-scroll-locky](https://github.com/theKashey/react-scroll-locky).
|
326 | - hide everything else from screen readers. Use [aria-hidden](https://github.com/theKashey/aria-hidden)
|
327 |
|
328 | You may use [react-focus-on](https://github.com/theKashey/react-focus-on) to achieve everything above, assembled in the right order.
|
329 |
|
330 | # Licence
|
331 | MIT
|
332 |
|
333 |
|