theKashey / react-focus-lock

It is a trap! A lock for a Focus. 🔓
MIT License
1.25k stars 65 forks source link
a11y focus focus-lock modal-dialogs react

REACT FOCUS LOCK

it-is-a-trap - browser friendly focus lock
- matching all your use cases
- trusted by best UI frameworks
- the thing Admiral Ackbar was talking about

[![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) [![npm](https://img.shields.io/npm/v/react-focus-lock.svg)](https://www.npmjs.com/package/react-focus-lock) [![bundle size](https://badgen.net/bundlephobia/minzip/react-focus-lock)](https://bundlephobia.com/result?p=react-focus-lock) [![downloads](https://badgen.net/npm/dm/react-focus-lock)](https://www.npmtrends.com/react-focus-lock)

It is a trap! We got your focus and will not let him out!

Trusted

Trusted by Atlassian AtlasKit, ReachUI, SmoothUI, Storybook and we will do our best to earn your trust too!

Features

💡 focus locks is part of a bigger whole, consider 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.

API

FocusLock would work perfectly even with no props set.

FocusLock has few props to tune behavior, all props are optional:

Programmatic control

Focus lock exposes a few methods to control focus programmatically.

Imperative API

Declarative API

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

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

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.

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")
);

<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

 import { MoveFocusInside } from 'react-focus-lock';

 <MoveFocusInside>
  <button>will be focused</button>
 </MoveFocusInside>

Portals

Use focus scattering to handle portals

// main content
- using `shards`. Just pass all the pieces to the "shards" prop. 
```js
const PortaledElement = () => (
   <div ref={ref}>
     // "discoverable" portaled content
   </div>  
);

<FocusLock shards={[ref]}>
  // main content
</FocusLock>
// main content

### Using your own `Components`
You may use `as` prop to change _what_ Focus-Lock will render around `children`.
```js
<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>

Programmatic Control

Let's take a look at the Rowing Focus as an example.

// Will set tabindex to -1 when is not focused
const FocusTrackingButton = ({ children }) => {
    const { active, onFocus, ref } = useFocusState();
    return (
        <button tabIndex={active ? undefined : -1} onFocus={onFocus} ref={ref}>
            {children}
        </button>
    );
};
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 (
        <div
            onKeyDown={onKey}
            // ref={divRef} for  useFocusController
        >
            <FocusButton>Button1</FocusButton>
            <FocusButton>Button2</FocusButton>
            <FocusButton>Button3</FocusButton>
            <FocusButton>Button4</FocusButton>
        </div>
    );
};

// FocusLock, even disabled one
const RowingFocusTrap = () => (
    <FocusLock disabled>
        <RowingFocusInternalTrap />
    </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';

// wrap with
<InFocusGuard>
  <button />
</InFocusGuard>

// place before and after
<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 />  
// there is no "tailing" guard :)

Unmounting and focus management

<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 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.

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 }} // 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.

Resolving focus fighting

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.

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>

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.

<FocusLock whiteList={node => document.getElementById('root').contains(node)}>
 ...
</FocusLock>

Two Focus-Locks

React-Focus-Lock is expected to be a singlentone. __Use webpack or yarn resolution for force only one version of react-focus-lock used.

webpack.conf

resolve: {    
alias: {
'react-focus-lock': path.resolve(path.join(__dirname, './node_modules/react-focus-lock'))
...

WHY?

From MDN Article about accessible dialogs:

This one is about managing the focus.

I've got a good article about focus management, dialogs and WAI-ARIA.

Not only for React

Uses focus-lock under the hood. It does also provide support for Vue.js and Vanilla DOM solutions

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