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 | - `crossFrame=true` enables aggressive focus capturing within iframes
|
86 |
|
87 | ### Focusing in OSX (Safari/Firefox) is strange!
|
88 | By default `tabbing` in OSX `sees` only controls, but not links or anything else `tabbable`. This is system settings, and Safari/Firefox obey.
|
89 | 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.
|
90 |
|
91 | ## Set up
|
92 | ### Requirements
|
93 | - version 1x is React 15/16 compatible
|
94 | - version 2+ requires React 16.8+ (hooks)
|
95 | ### Import
|
96 | `react-focus-lock` exposed __3 entry points__: for the classical usage, and a _sidecar_ one.
|
97 | #### Default usage
|
98 | - 4kb, `import FocusLock from 'react-focus-lock` would give you component you are looking for.
|
99 |
|
100 | #### Separated usage
|
101 | Meanwhile - you dont need any focus related logic until it's needed.
|
102 | Thus - you may defer that logic till Lock activation and move all related code to a _sidecar_.
|
103 |
|
104 | - UI, __1.5kb__, `import FocusLockUI from 'react-focus-lock/UI` - a DOM part of a lock.
|
105 | - Sidecar, 3.5kb, `import Sidecar from 'react-focus-lock/sidecar` - which is the real focus lock.
|
106 |
|
107 | ```js
|
108 | import FocusLockUI from "react-focus-lock/UI";
|
109 | import {sidecar} from "use-sidecar";
|
110 |
|
111 | // prefetch sidecar. data would be loaded, but js would not be executed
|
112 | const FocusLockSidecar = sidecar(
|
113 | () => import(/* webpackPrefetch: true */ "react-focus-lock/sidecar")
|
114 | );
|
115 |
|
116 | <FocusLockUI
|
117 | disabled={this.state.disabled}
|
118 | sideCar={FocusLockSidecar}
|
119 | >
|
120 | {content}
|
121 | </FocusLockUI>
|
122 | ```
|
123 | That would split FocusLock into two pieces, reducing app size and improving the first load.
|
124 | The cost of focus-lock is just 1.5kb!
|
125 |
|
126 | > Saved 3.5kb?! 🤷♂️ 3.5kb here and 3.5kb here, and your 20mb bundle is ready.
|
127 |
|
128 | # Autofocus
|
129 | Use when you cannot use the native `autoFocus` prop - because you only want to autofocus once the Trap has been activated
|
130 |
|
131 | - prop `data-autofocus` on the element.
|
132 | - prop `data-autofocus-inside` on the element to focus on something inside.
|
133 | - `AutoFocusInside` component, as named export of this library.
|
134 | ```js
|
135 | import FocusLock, { AutoFocusInside } from 'react-focus-lock';
|
136 |
|
137 | <FocusLock>
|
138 | <button>Click</button>
|
139 | <AutoFocusInside>
|
140 | <button>will be focused</button>
|
141 | </AutoFocusInside>
|
142 | </FocusLock>
|
143 | // is the same as
|
144 |
|
145 | <FocusLock>
|
146 | <button>Click</button>
|
147 | <button data-autofocus>will be focused</button>
|
148 | </FocusLock>
|
149 | ```
|
150 |
|
151 | If there is more than one auto-focusable target - the first will be selected.
|
152 | If it is a part of radio group, and __rest of radio group element are also autofocusable__(just put them into AutoFocusInside) -
|
153 | checked one fill be selected.
|
154 |
|
155 | `AutoFocusInside` will work only on Lock activation, and does nothing, then used outside of the lock.
|
156 | You can use `MoveFocusInside` to move focus inside with or without lock.
|
157 |
|
158 | ```js
|
159 | import { MoveFocusInside } from 'react-focus-lock';
|
160 |
|
161 | <MoveFocusInside>
|
162 | <button>will be focused</button>
|
163 | </MoveFocusInside>
|
164 | ```
|
165 |
|
166 | # Portals
|
167 | Use focus scattering to handle portals
|
168 |
|
169 | - using `groups`. Just create a few locks (only one could be active) with a same group name
|
170 | ```js
|
171 | const PortaledElement = () => (
|
172 | <FocusLock group="group42" disabled={true}>
|
173 | // "discoverable" portaled content
|
174 | </FocusLock>
|
175 | );
|
176 |
|
177 | <FocusLock group="group42">
|
178 | // main content
|
179 | </FocusLock>
|
180 | ```
|
181 | - using `shards`. Just pass all the pieces to the "shards" prop.
|
182 | ```js
|
183 | const PortaledElement = () => (
|
184 | <div ref={ref}>
|
185 | // "discoverable" portaled content
|
186 | </div>
|
187 | );
|
188 |
|
189 | <FocusLock shards={[ref]}>
|
190 | // main content
|
191 | </FocusLock>
|
192 | ```
|
193 | - without anything. FocusLock will not prevent focusing portaled element, but will not include them in to tab order
|
194 | ```js
|
195 | const PortaledElement = () => (
|
196 | <div>
|
197 | // NON-"discoverable" portaled content
|
198 | </div>
|
199 | );
|
200 |
|
201 | <FocusLock shards={[ref]}>
|
202 | // main content
|
203 | <PortaledElement />
|
204 | </FocusLock>
|
205 | ```
|
206 |
|
207 | ### Using your own `Components`
|
208 | You may use `as` prop to change _what_ Focus-Lock will render around `children`.
|
209 | ```js
|
210 | <FocusLock as="section">
|
211 | <button>Click</button>
|
212 | <button data-autofocus>will be focused</button>
|
213 | </FocusLock>
|
214 |
|
215 | <FocusLock as={AnotherComponent} lockProps={{anyAnotherComponentProp: 4}}>
|
216 | <button>Click</button>
|
217 | <span>Hello there!</span>
|
218 | </FocusLock>
|
219 | ```
|
220 |
|
221 | ### Guarding
|
222 | As you may know - FocusLock is adding `Focus Guards` before and after lock to remove some side effects, like page scrolling.
|
223 | But `shards` will not have such guards, and it might be not so cool to use them - for example if no `tabbable` would be
|
224 | defined after shard - you will tab to the browser chrome.
|
225 |
|
226 | You may wrap shard with `InFocusGuard` or just drop `InFocusGuard` here and there - that would solve the problem.
|
227 | ```js
|
228 | import {InFocusGuard} from 'react-focus-lock';
|
229 |
|
230 | // wrap with
|
231 | <InFocusGuard>
|
232 | <button />
|
233 | </InFocusGuard>
|
234 |
|
235 | // place before and after
|
236 | <InFocusGuard />
|
237 | <button />
|
238 | <InFocusGuard />
|
239 | ```
|
240 | InFocusGuards would be active(tabbable) only when tabble, it protecting, is focused.
|
241 |
|
242 | #### Removing Tailing Guard
|
243 | If only your modal is the last tabble element on the body - you might remove the Tailing Guard,
|
244 | to allow user _tab_ into address bar.
|
245 | ```js
|
246 | <InFocusGuard/>
|
247 | <button />
|
248 | // there is no "tailing" guard :)
|
249 | ```
|
250 |
|
251 | # Unmounting and focus management
|
252 | - In case FocusLock has `returnFocus` enabled, and it's going to be unmounted - focus will be returned after zero-timeout.
|
253 | - In case `returnFocus` is set to `false`, and you are going to control focus change on your own - keep in mind
|
254 | >> React will first call Parent.componentWillUnmount, and next Child.componentWillUnmount
|
255 |
|
256 | 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.
|
257 |
|
258 | Similarly, if you are using the `disabled` prop to control FocusLock, you will need a zero-timeout to correctly restore focus.
|
259 |
|
260 | ```
|
261 | <FocusLock
|
262 | disabled={isFocusLockDisabled}
|
263 | onDeactivation={() => {
|
264 | // Without the zero-timeout, focus will likely remain on the button/control
|
265 | // you used to set isFocusLockDisabled = true
|
266 | window.setTimeout(() => myRef.current.focus(), 0);
|
267 | }
|
268 | >
|
269 | ```
|
270 |
|
271 | ## Return focus with no scroll
|
272 | > read more at the [issue #83](https://github.com/theKashey/react-focus-lock/issues/83) or
|
273 | [mdn article](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus).
|
274 |
|
275 | To return focus, but without _jumpy_ page scroll returning a focus you might specify a focus option
|
276 | ```js
|
277 | <FocusLock
|
278 | returnFocus={{ preventScroll: false }} // working not in all browsers
|
279 | >
|
280 | ```
|
281 | Not supported by Edge and Safari.
|
282 |
|
283 | # Not only for React
|
284 | Uses [focus-lock](https://github.com/theKashey/focus-lock/) under the hood. It does also provide support for Vue.js and Vanilla DOM solutions
|
285 |
|
286 | # Warning!
|
287 | Two different _focus-lock-managers_ or even different version of a single one, active
|
288 | simultaneously will FIGHT!
|
289 |
|
290 | __Focus-lock will surrender__, as long any other focus management library will not.
|
291 |
|
292 | ## Focus fighting
|
293 | You may wrap some render branch with `FreeFocusInside`, and react-focus-lock __will ignore__
|
294 | any focus inside marked node, thus landing a peace.
|
295 |
|
296 | ```js
|
297 | import { FreeFocusInside } from 'react-focus-lock';
|
298 |
|
299 | <FreeFocusInside>
|
300 | <div id="portal-for-modals">
|
301 | in this div i am going to portal my modals, dont fight with them please
|
302 | </div>
|
303 | </FreeFocusInside>
|
304 | ```
|
305 |
|
306 | Even the better is to `whiteList` FocusLock areas - for example "you should handle only React Stuff in React Root"
|
307 | ```js
|
308 | <FocusLock whiteList={node => document.getElementById('root').contains(node)}>
|
309 | ...
|
310 | </FocusLock>
|
311 | ```
|
312 |
|
313 | PS: __please use webpack or yarn resolution for force one version of react-focus-lock used__
|
314 |
|
315 | > webpack.conf
|
316 | ```js
|
317 | resolve: {
|
318 | alias: {
|
319 | 'react-focus-lock': path.resolve(path.join(__dirname, './node_modules/react-focus-lock'))
|
320 | ...
|
321 | ```
|
322 |
|
323 | # More
|
324 | To create a "right" modal dialog you have to:
|
325 | - manage a focus. Use this library
|
326 | - block document scroll. Use [react-scroll-locky](https://github.com/theKashey/react-scroll-locky).
|
327 | - hide everything else from screen readers. Use [aria-hidden](https://github.com/theKashey/aria-hidden)
|
328 |
|
329 | You may use [react-focus-on](https://github.com/theKashey/react-focus-on) to achieve everything above, assembled in the right order.
|
330 |
|
331 | # Licence
|
332 | MIT
|
333 |
|
334 |
|