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 | [![Greenkeeper badge](https://badges.greenkeeper.io/theKashey/react-focus-lock.svg)](https://greenkeeper.io/)
|
16 | <hr/>
|
17 | </div>
|
18 |
|
19 | It is a trap! We got your focus and will not let him out!
|
20 |
|
21 | - Modal dialogs. You can not leave it with "Tab", ie do a "tab-out".
|
22 | - Focused tasks. It will aways brings you back, as you can "lock" user inside a component.
|
23 | - Any any other case, when you have to lock user _intention_ and _focus_, if that's what `a11y` is asking for.
|
24 |
|
25 | ### Trusted
|
26 | Trusted by
|
27 | [Atlassian AtlasKit](https://atlaskit.atlassian.com),
|
28 | [ReachUI](https://ui.reach.tech/),
|
29 | [SmoothUI](https://smooth-ui.smooth-code.com/),
|
30 | [Storybook](https://storybook.js.org/)
|
31 | and we will do our best to earn your trust too!
|
32 |
|
33 | # Features
|
34 | - no keyboard control, everything is done watching a __focus behavior__, not emulating it. Thus works always and everywhere.
|
35 | - 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).
|
36 | - _Scattered_ locks, or focus lock groups - you can setup different isolated locks, and _tab_ from one to another.
|
37 | - Controllable isolation level.
|
38 | - variable size bundle. Uses sidecar to trim UI part to 1.5kb.
|
39 |
|
40 | > 💡 __focus__ locks is only the first part, there are also __scroll lock__ and __text-to-speech__ lock
|
41 | you have to use to really "lock" the user.
|
42 | Try [react-focus-on](https://github.com/theKashey/react-focus-on) to archive everything above, assembled in the right order.
|
43 |
|
44 | # How to use
|
45 | Just wrap something with focus lock, and focus will be `moved inside` on mount.
|
46 | ```js
|
47 | import FocusLock from 'react-focus-lock';
|
48 |
|
49 | const JailForAFocus = ({onClose}) => (
|
50 | <FocusLock>
|
51 | You can not leave this form
|
52 | <button onClick={onClose} />
|
53 | </FocusLock>
|
54 | );
|
55 | ```
|
56 | Demo - https://codesandbox.io/s/5wmrwlvxv4.
|
57 |
|
58 | # WHY?
|
59 | From [MDN Article about accessible dialogs](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_dialog_role):
|
60 | - The dialog must be properly labeled
|
61 | - Keyboard __focus must be managed__ correctly
|
62 |
|
63 | This one is about managing the focus.
|
64 |
|
65 | I've got a good [article about focus management, dialogs and WAI-ARIA](https://medium.com/@antonkorzunov/its-a-focus-trap-699a04d66fb5).
|
66 |
|
67 | # API
|
68 | > FocusLock would work perfectly even with no props set.
|
69 |
|
70 | FocusLock has few props to tune behavior, all props are optional:
|
71 | - `[disabled`, to disable(enable) behavior without altering the tree.
|
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.
|
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 |
|
85 | ### Focusing in OSX (Safari/FireFox) is strange!
|
86 | By default `tabbing` in OSX `sees` only controls, but not links or anything else `tabbable`. This is system settings, and Safari/FireFox obey.
|
87 | Press Option+Tab in Safary to loop across all tabbables, or change the Safary 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.
|
88 |
|
89 | ## Set up
|
90 | ### Requirements
|
91 | - version 1x is React 15/16 compatible
|
92 | - version 2+ requires React 16.8+ (hooks)
|
93 | ### Import
|
94 | `react-focus-lock` exposed __3 entry points__: for the classical usage, and a _sidecar_ one.
|
95 | #### Default usage
|
96 | - 4kb, `import FocusLock from 'react-focus-lock` would give you component you are looking for.
|
97 |
|
98 | #### Separated usage
|
99 | Meanwhile - you dont need any focus related logic until it's needed.
|
100 | Thus - you may defer that logic till Lock activation and move all related code to a _sidecar_.
|
101 |
|
102 | - UI, __1.5kb__, `import FocusLockUI from 'react-focus-lock/UI` - a DOM part of a lock.
|
103 | - Sidecar, 3.5kb, `import Sidecar from 'react-focus-lock/sidecar` - which is the real focus lock.
|
104 |
|
105 | ```js
|
106 | import FocusLockUI from "react-focus-lock/UI";
|
107 | import {sidecar} from "use-sidecar";
|
108 |
|
109 | // prefetch sidecar. data would be loaded, but js would not be executed
|
110 | const FocusLockSidecar = sidecar(
|
111 | () => import(/* webpackPrefetch: true */ "react-focus-lock/sidecar")
|
112 | );
|
113 |
|
114 | <FocusLockUI
|
115 | disabled={this.state.disabled}
|
116 | sideCar={FocusLockSidecar}
|
117 | >
|
118 | {content}
|
119 | </FocusLockUI>
|
120 | ```
|
121 | That would split FocusLock into two pieces, reducing app size and improving the first load.
|
122 | The cost of focus-lock is just 1.5kb!
|
123 |
|
124 | > Saved 3.5kb?! 🤷♂️ 3.5kb here and 3.5kb here, and your 20mb bundle is ready.
|
125 |
|
126 | # Autofocus
|
127 | As long you cannot use `autoFocus` prop -
|
128 | cos "focusing" should be delayed to Trap activation, and autoFocus will effect immediately -
|
129 | Focus Lock provide a special API for it
|
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 gonna to be unmounted - focus will be returned after zero-timeout.
|
253 | - In case `returnFocus` did not set, and you are going to control focus change by your own - keep in mind
|
254 | >> React will first call Parent.componentWillUnmount, and next Child.componentWillUnmount
|
255 |
|
256 | Thus means - Trap will be still active, be the time you _may_ want move(return) focus on componentWillUnmount. Please deffer this action with a zero-timeout.
|
257 |
|
258 | # Not only for React
|
259 | Uses [focus-lock](https://github.com/theKashey/focus-lock/) under the hood. It does also provide support for Vue.js and Vanilla DOM solutions
|
260 |
|
261 | # Warning!
|
262 | Two different _focus-lock-managers_ or even different version of a single one, active
|
263 | simultaneously will FIGHT!
|
264 |
|
265 | __Focus-lock will surrender__, as long any other focus management library will not.
|
266 |
|
267 | ## Focus fighting
|
268 | You may wrap some render branch with `FreeFocusInside`, and react-focus-lock __will ignore__
|
269 | any focus inside marked node, thus landing a peace.
|
270 |
|
271 | ```js
|
272 | import { FreeFocusInside } from 'react-focus-lock';
|
273 |
|
274 | <FreeFocusInside>
|
275 | <div id="portal-for-modals">
|
276 | in this div i am going to portal my modals, dont fight with them please
|
277 | </div>
|
278 | </FreeFocusInside>
|
279 | ```
|
280 |
|
281 | Even the better is to `whiteList` FocusLock areas - for example "you should handle only React Stuff in React Root"
|
282 | ```js
|
283 | <FocusLock whiteList={node => document.getElementById('root').contains(node)}>
|
284 | ...
|
285 | </FocusLock>
|
286 | ```
|
287 |
|
288 | PS: __please use webpack or yarn resolution for force one version of react-focus-lock used__
|
289 |
|
290 | > webpack.conf
|
291 | ```js
|
292 | resolve: {
|
293 | alias: {
|
294 | 'react-focus-lock': path.resolve(path.join(__dirname, './node_modules/react-focus-lock'))
|
295 | ...
|
296 | ```
|
297 |
|
298 | # More
|
299 | To create a "right" modal dialog you have to:
|
300 | - manage a focus. Use this library
|
301 | - block document scroll. Use [react-scroll-locky](https://github.com/theKashey/react-scroll-locky).
|
302 | - hide everything else from screen readers. Use [aria-hidden](https://github.com/theKashey/aria-hidden)
|
303 |
|
304 | You may use [react-focus-on](https://github.com/theKashey/react-focus-on) to archive everything above, assembled in the right order.
|
305 |
|
306 | # Licence
|
307 | MIT
|
308 |
|
309 |
|