Giter Club home page Giter Club logo

react-focus-lock's Introduction

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 npm bundle size downloads


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

Trusted

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

Features

  • no keyboard control, everything is done watching a focus behavior, not emulating it. Focus-Locks works for all cases including positive tab indexes.
  • React Portals support. Even if some data is in outer space - it is still in lock.
  • Scattered locks, or focus lock groups - you can setup different isolated locks, and tab from one to another.
  • Controllable isolation level.
  • variable size bundle. Uses sidecar to trim UI part down to 1.5kb.

πŸ’‘ 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:

  • 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 iframes

Programmatic control

Focus lock exposes a few methods to control focus programmatically.

Imperative API

  • 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

Declarative API

  • <AutoFocusInside/> - causes autofocus to look inside the component
  • <MoveFocusInside/> - wrapper around useFocusInside, forcibly moves focus inside on mount
  • <FreeFocusInside/> - 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

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

  • version 1x is React 15/16 compatible
  • version 2+ requires React 16.8+ (hooks)

Import

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

  • 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.
 import FocusLock, { AutoFocusInside } from 'react-focus-lock';
 
 <FocusLock>
   <button>Click</button>
   <AutoFocusInside>
    <button>will be focused</button>
   </AutoFocusInside>
 </FocusLock>
 // is the same as
 
 <FocusLock>
   <button>Click</button>
    <button data-autofocus>will be focused</button>
 </FocusLock>

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.

 import { MoveFocusInside } from 'react-focus-lock';
    
 <MoveFocusInside>
  <button>will be focused</button>
 </MoveFocusInside>

Portals

Use focus scattering to handle portals

  • using groups. Just create a few locks (only one could be active) with a same group name
const PortaledElement = () => (
   <FocusLock group="group42" disabled={true}>
     // "discoverable" portaled content
   </FocusLock>  
);

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

<FocusLock shards={[ref]}>
  // main content
</FocusLock>
  • without anything. FocusLock will not prevent focusing portaled element, but will not include them in to tab order
const PortaledElement = () => (
   <div>
     // NON-"discoverable" portaled content
   </div>  
);

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

Using your own Components

You may use as prop to change what Focus-Lock will render around children.

<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

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

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

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

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

  • The dialog must be properly labeled
  • Keyboard focus must be managed correctly

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

react-focus-lock's People

Contributors

abhimanyu-singh-uber avatar ai avatar aldo-roman avatar alex-e-leon avatar arronhunt avatar benoitgrelard avatar cheungj avatar crshmk avatar ezhlobo avatar greenkeeper[bot] avatar ianwang avatar jefski14 avatar johnhok avatar jossmac avatar juliacbrown avatar leemoonpig avatar leyanlo avatar lpadier avatar mejackreed avatar mrm007 avatar ndelangen avatar thekashey avatar toolchild avatar trysound avatar wojtekmaj avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-focus-lock's Issues

PropTypes version

Right now we have prop-types: "15.5.10" in dependencies. As result, I have 2 PropTypes duplicates in my node_modules. Should we replace it to >=15.5.10.

Missing document check

Lock.js

onActivation = () => {
    this.originalFocusedElement = this.originalFocusedElement || document.activeElement;
};

The existence of the document object is not tested in onActivation, this can cause Jest unit tests to fail. If setTimeout is not included in a unit test, this code is executed after Jest has already cleaned up the document object.

Disable focus programmatically

Hi,

I'm trying to disable the focus on an element programmatically, by changing an option variable.

Current lineup:

<FocusLock disabled={disabledFocus}>
  <div
    tabIndex="1"
    role="dialog"
    aria-labelledby={heading}
    aria-describedby={subHeading}
    className={css(styles.modal)}
    style={{ ...modalStyles[state] }}
    state={state}
    onKeyDown={this.handleKeyClose}
  >
    ...
  </div>
</Focus>

I toggle this state in a subcomponent of my modal via redux. I've also checked the React devtools, and in fact, the disabled will be set to true/false regarding the current state of disabledFocus but somehow the focus is still on the <div />.

How can I disable the focus once another component inside of my modal opens?

ReferenceError: Element is not defined

I started getting this error in version 1.18.2:

ReferenceError: Element is not defined
    at Object.<anonymous> (/Users/spencerelliott/Dev/path/to/common/temp/node_modules/.registry.npmjs.org/react-focus-lock/1.18.2/[email protected]/node_modules/react-focus-lock/dist/cjs/Lock.js:195:44)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:22:18)
    at Object.<anonymous> (/Users/spencerelliott/Dev/path/to/common/temp/node_modules/.registry.npmjs.org/react-focus-lock/1.18.2/[email protected]/node_modules/react-focus-lock/dist/cjs/index.js:34:36)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:22:18)

I think it was introduced in this commit: 7bc46f0#diff-757809db6137e7d28cf62c382a00aeadR147

Running linter and tests on CI

BTW, what do you think about setting up CircleCI or Travis? They both should be free for open source projects.

It will make this repository more friendly for others and help you to avoid bad code in master.

I can push CircleCI config if you are ok with it. Or let me know what do you think about it.

FocusLock + iOS Safari

@stereobooster reports in https://dev.to/stereobooster/fullscreen-mobile-modal-how-hard-can-it-be-7mj that RFL does not work properly on mobile Safari - https://twitter.com/stereobooster.
To be honest - I never tried to press the buttons, pointed in the article, to navigate thought the page :)

Reach Dialog uses aria-hidden prop on
Array.prototype.forEach.call(document.querySelectorAll("body > *"), node => {, but this is not a case for this library, as long lock could be anywhere on the page.

Let's debug...

Skips <video/>

I'm using this library for locking focus inside my custom slideshow component.
The slideshow can play videos.
What I noticed is that when hitting Tab key it doesn't tab to the <video/> element (always skips it), but it does tab to the <video/> element when hitting Shift + Tab (doesn't skip the video).

Also, I noticed that when I add whiteList={node => { console.log(node); return false; }} it stops working (lets the focus outside and doesn't trap it).

<FocusLock
  returnFocus
  autoFocus={false}>
  ...
</FocusLock>

PersistentFocus + FreeFocusInside

Hi! I have to use PersistentFocus on my search input to provide constant focus, but when I combine it with material-ui components I get focus-fighting log error which I tried to remove (without success) by wrapping those elements with FreeFocusInside tag. When I remove PersistentFocus logs do not appear but I do not have my functionality. Is it a correct behaviour or am I doing something wrong?

Here I prepared minimal example to show you my case:
https://codesandbox.io/s/wizardly-sutherland-lv1jj
PS. In order to modify that code on sandbox you need to remove input through browser inspector as react-focus-lock steals focus from codesandbox ;)

Tabindex issue

Hi,
Accessibility tools generally freaks when tabindex is greater than 0 and it seems this library is using tabindex 1 in place.
I'm not sure how important is that to have that implementation, but is it possible to run with tabindex 0 only?

Multiple FocusLock Elements Conflict

Hi, I currently have multiple <FocusLock /> elements that when rendered on top of each other throw a stack overflow error when they attempt to set focus on their respective elements.

What is the workaround that I can use for this? I referred to your README regarding the group prop, but the documentation is vague regarding its use and what it actually does.

Maximum call stack size exceeded Exception

I'm getting the exception below when I have a focus trapped dialog and a user clicks a button to open another dialog that is not focus trapped.

Uncaught RangeError: Maximum call stack size exceeded.
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)

and

Uncaught Error: An error was thrown inside one of your components, but React doesn't know what it was. This is likely due to browser flakiness. React does its best to preserve the "Pause on exceptions" behavior of the DevTools, which requires some DEV-mode only tricks. It's possible that these don't work in your browser. Try triggering the error in production mode, or switching to a modern browser. If you suspect that this is actually an issue with React, please file an issue.
    at Object.invokeGuardedCallbackDev (react-dom.development.js:143)
    at Object.invokeGuardedCallback (react-dom.development.js:187)
    at Object.invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:201)
    at executeDispatch (react-dom.development.js:461)
    at executeDispatchesInOrder (react-dom.development.js:480)
    at executeDispatchesAndRelease (react-dom.development.js:581)
    at executeDispatchesAndReleaseTopLevel (react-dom.development.js:592)
    at forEachAccumulated (react-dom.development.js:562)
    at runEventsInBatch (react-dom.development.js:723)
    at runExtractedEventsInBatch (react-dom.development.js:732)
    at handleTopLevel (react-dom.development.js:4477)
    at batchedUpdates$1 (react-dom.development.js:16660)
    at batchedUpdates (react-dom.development.js:2131)
    at dispatchEvent (react-dom.development.js:4556)
    at interactiveUpdates$1 (react-dom.development.js:16715)
    at interactiveUpdates (react-dom.development.js:2150)
    at dispatchInteractiveEvent (react-dom.development.js:4533)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)
    at HTMLDocument.onTrap (Trap.js:74)
    at HTMLDocument.Modal._this.enforceFocus (Modal.js:191)
    at focusOn (setFocus.js:15)
    at webpackJsonp../node_modules/focus-lock/dist/setFocus.js.exports.default (setFocus.js:25)
    at activateTrap (Trap.js:63)

Adding Guard with Portals so that Focus returns to first focusable element after last.

Im using react-portal for placing a Modal component into the body of my HTML inside of a react app. On top of this I am using react-focus-lock in hopes that I can lock focus to the Modal while it is mounted, and have tabbing through focusable elements loop over them, returning to the first focusable modal element once the final focusable element has been tabbed from.

This is according to the W3C modal accessibility spec:

If focus is on the last tabbable element inside the dialog, moves focus to the first tabbable element inside the dialog.

My code is structured as follows (simplified for example):

There is a ModalContainer that uses react-focus-lock to wrap the Modal in a focus lock, passing the Modal a ref to use on itself via the "Childred as Function" react pattern:

import React from "react";
import FocusLock from "react-focus-lock";

class ModalContainer extends React.Component {
  constructor(props) {
    super(props);

    this.modalRef = React.createRef();
  }

  render() {
    const { children, render = children } = this.props;

    return (
      <FocusLock shards={[this.modalRef]}>
        {
          render({
            modalRef: this.modalRef
          })
        }
      </FocusLock>
    );
  }
}

export default ModalContainer;

The Modal component imports this ModalContainer so that it may wrap itself inside of it and obtain a reference to the ref react-focus-lock wants for its shards property:

import React from "react";
import { Portal } from "react-portal";

import ModalContainer from "../containers/ModalContainer";
import Backdrop from "../views/Backdrop";
import ModalView from "../views/ModalView";

class Modal extends React.Component {
  render() {
    return (
      <ModalContainer>
        {
          ({
            modalRef
          }) => {

            return (
              <Portal>
                <Backdrop>
                  <ModalView ref={modalRef}>
                    <div><a href="http://google.com">first</a></div>
                    <div><a href="http://google.com">second</a></div>
                  </ModalView>
                </Backdrop>
              </Portal>
            );
          }
        }
      </ModalContainer>
    );
  }
}

export default Modal;

This all works to lock focus to the Modal and not to other focusable elements on the page, as expected. However, once the final focusable element (the link with text "second") is tabbed away from, focus moves on to the tabs in my browser window. I can "backwards tab" back to the modal, but what I want is to keep tabbing within the modal.

As per react-focus-lock's documentation, "guards" may be added to arrive at this desired goal:

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.

However, I have been unable to figure out how to actually do this. Ive placed InFocusGuard components in several parts of my modal setup without any noticeable results.

If InFocusGuard works as described, can you tell me where to place InFocusGuard in my setup?

FocusLock white list

v1.12.0 introduced FreeFocusInside component, which could white-list any focus management library inside, but that's not enough.

As long there is no way to wrap some modals with this component - react-focus-lock should not interfere with any elements outside of it's root component.

  • add an option, to ignore focus events outside current react root
  • add an option to ignore any elements by a selector.

react-focus-lock with material UI drop-downs

If drop-down elements are warped with the FocusLock, items in the drop-down are cannot access and select from the keyboard. (using arrow keys and enter key press ).
Note: It works with mouse click events.

It has warning about mandatory "key" property

It shows a warning in the console that "key" is important for each item in arrays. And it refers to Lock.js file.

PS. What do you think about using jest? Jest has everything that we have right now out of the box (except integration with enzyme). And it catches

[Question] Controlling which element get focus

I have set of buttons in a custom modal popup. According to current implementation what happen is first button in the group get the focus. What I want to do is to set focus for the last item in the button group. How can I achieve this behavior? Sorry if this was documented in readme I didn't see that. @theKashey

Focus Lock + PopperJS Issues

Issue Description

When combining PopperJS w/ Focus Lock there are various overflow issues.

Steps to reproduce

  • Set html, body to 100% height w/ overflow auto
  • Create a popperjs element
  • Link the popper element to something that is positioned at the bottom of a page.
  • Inside the popper element, put a focus lock element

Thats quite a complex combination, I'll try to create a repo case. In the meantime, here is a video that might help. https://www.screencast.com/t/VIH2TlRNep

Prop type error with React.forwardRef

I got a prop type error when rendering FocusLock as a component where the component was created by React.forwardRef, having something like:

<FocusLock as={Component} />

I would suggest creating a more specific propType, rather than checking for a string or function (Line 136 in Lock.js: as: PropTypes.oneOfType([PropTypes.string, PropTypes.func])).

You can check whether a prop is a valid component using react-is. See also: facebook/react#12453

Here's what I'm doing until this gets fixed:

import { isValidElementType } from 'react-is';
import FocusLock from 'react-focus-lock';

const propTypeFactory = function propTypeFactory(check, isRequired = false) {
  return function propType(props, propName, componentName) {
    const prop = props[propName];
    if (prop != null) {
      if (!check(prop)) {
        return new Error(`Invalid prop "${propName}" supplied to "${componentName}". Validation failed.`);
      }
    }
    else if (isRequired) {
      // Prop is required but wasn't specified. Throw an error.
      return new Error(`Required prop "${propName}" supplied to "${componentName}" is missing.`);
    }
  };
};

const createPropType = function createPropType(check) {
  const propType = propTypeFactory(check);
  propType.isRequired = propTypeFactory(check, true);
  return propType;
};

const componentPropType = createPropType(isValidElementType);

FocusLock.propTypes = {
  ...FocusLock.propTypes,
  as: componentPropType,
};

Support for options in .focus()

Hey @theKashey, I am looking at adding support for focus options when calling .focus() in returnFocus.

See here: https://github.com/theKashey/react-focus-lock/blob/master/src/Lock.js#L60

The reason for that is that currently when the focus gets returned, if the page has been scrolled down and the element supposed to receive the focus back is not in view anymore, the browser will scroll to reveal that element being focused.

I understand that setting the returning the focus is absolutely important for accessibility and I wouldn't change that. However, it can be a bit disorientating for users to be scrolled to a difference place for what seems to be no apparent reason (to them).

Most latest browsers actually support an option to disable that behaviour if needed.
See the preventScroll option (false by default) on mdn.

Here's the current support for it.
I think support is not too much of an issue as this can be seen as an enhancement anyway.

I actually need this feature as part of react-focus-on but I know the code for the returnFocus handler actually lives in react-focus-lock.

Would you be willing to add a new prop returnFocusOptions to support that optional behaviour?

If so, I can draft you a PR today.

Let me know!

✌️

tabindex="-1" on last element prevents Tab key from cycling back to the top

Scenario:
The last focusable element in the modal has attribute tabindex="-1" or CSS visibility: hidden and is such not tabbable by the browser.
The last focusable element in the modal is within a parent that has style display: none and does not explicitly have tabindex="-1" set.

Expected Result:
When using Tab key to navigate, this element is skipped, so the expectation is for focus to cycle back to the first tabbable element at the top of the modal. This behaviour is working as expected if I change the tabindex on the last element to 0.

Actual Result:
When using Tab key to navigate, the focus remains stuck on the last tabbable element in the modal and does not cycle back to the top.

v2.0.0 throws error on codesandbox

Hi there, on the latest version, v2.0.0, there seems to be an issue compiling on codesandbox. I've make a sandbox here to reproduce it.

ModuleNotFoundError: Could not find module in path: 'focus-lock/constants' relative to '/node_modules/react-focus-lock/dist/cjs/Lock.js'

I don't get this with any version prior to v2.0.0.

I'll take a look and see if I can find any recent updates in that area but might need some help resolving.

Thanks!

Pressing Escape on text fields makes the first element focused

Hey, I'll try to explain the issue but let me know if it's easier to show a real example.

I have a modal window (wrapped with <FocusLock>) with a form:

<FocusLock>
  <button>First focused element</button>
  <input type="text" />
</FocusLock>

When I open that modal the first button becomes focused. Then I press tab key to make my text field active. Then I press Escape to turn off edit mode (to unblock other hotkeys on that modal window).

Actual behavior: it makes the first button focused.

I'm not sure how it should be handled but I totally don't want to make the first element focused. I checked other examples that you mentioned in the article. They just remove focus, how it works for other forms on the page.

Focus Lock + react-a11y issue

I'm getting the exceptions below when using FocusLock on a project that uses react-a11y:

react-dom.development.js:55 Uncaught Invariant Violation: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
    at invariant (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:55:15)
    at scheduleWork (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:18734:5)
    at Object.enqueueSetState (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:12457:5)
    at ProxyComponent.Component.setState (webpack:///./node_modules/react/cjs/react.development.js?:375:16)
    at Object.eval [as ref] (webpack:///./node_modules/react-focus-lock/dist/es2015/Lock.js?:95:20)
    at ref (webpack:///./node_modules/react-a11y/lib/a11y.js?:97:31)
    at commitDetachRef (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:16405:7)
    at commitAllHostEffects (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:17502:9)
    at HTMLUnknownElement.callCallback (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:149:14)
    at Object.invokeGuardedCallbackDev (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:199:16)
The above error occurred in the <div> component:
    in div (created by FocusLock)
    in FocusLock (created by Drawer)
    in div (created by Drawer)
    in div (created by Drawer)
    in Drawer
    in _Timeout (created by CoreLayout)
    in div (created by CoreLayout)
    in CoreLayout (created by Context.Consumer)
    in Connect(CoreLayout) (created by Route)
    in Route (created by withRouter(Connect(CoreLayout)))
    in withRouter(Connect(CoreLayout))
    in ErrorBoundary (created by Context.Consumer)
    in Connect(ErrorBoundary) (created by InjectIntl(Connect(ErrorBoundary)))
    in InjectIntl(Connect(ErrorBoundary)) (created by Route)
    in Route (created by withRouter(InjectIntl(Connect(ErrorBoundary))))
    in withRouter(InjectIntl(Connect(ErrorBoundary)))
    in Router (created by BrowserRouter)
    in BrowserRouter (created by AppContainer)
    in IntlProvider (created by LocaleProvider)
    in LocaleProvider (created by Context.Consumer)
    in Connect(LocaleProvider) (created by AppContainer)
    in Provider (created by AppContainer)
    in AppContainer
    in AppContainer

Below is a demo of the issue:
Edit React focus lock

Disable focus after openning a dialog

How can I disable initial focus on the first focusable element after dialog opening? I need to have no focus after popup open.

I understand that you focus the first element to move focus inside the trap. But if we will focus some element and then we will lose focus on next Tab key press focus will be inside still popup.

I wrote this code in popup wrapper, but it didn’t work:

  componentDidMount () {
      setTimeout(() => {
        document.activeElement.blur()
      }, 100)
  }

focus lock and nvda browse mode.

I am using focus lock on my modal. tabbing is working perfectly. continuous tabbing inside the modal working as a loop. This is good for me. But in browse mode of NVDA , inside the focus lock arrow keys are working but it is not looping. Could you please give some suggestion to fix this?

[v1.19.0] Focus jumps to the first element

Hi, @theKashey !

Thanks for the great tool, we use it in our project for a long time.

I noticed an issue in v1.19.0: focus lock works perfectly, but if you change your browser tab to another one and then switch back, focus jumps to the first element of the locked group. The bug can be easily reproduced in Firefox 66, Chrome 73 and Safari 12.1 on Mac.

Here is an screencast:

bug screencast

It might be useful to diagnose the issue: these buttons have an onClick event with event.currentTarget.blur() to prevent applying of focus styles after a click.

I have downgraded to 1.18.3, and it works as expected now.

Let me know if I can you with the problem.

Thanks!

Tests are failed locally

Hello,

I do want to use this component but it failed because PropTypes object is not available through React anymore. I made a fork and ran tests locally, and almost all of them are failed:

screen shot 2017-10-31 at 13 05 25

Focus Lock + Jest issue

foucs lock throws the following exception for running tests on node version 10.3.0.

TypeError: Cannot read property 'activeElement' of null at getFocusMerge (focusMerge.js: 100:32)

not sure what's the issue.

Any leads could be well appreciated.

Thanks,
Jay

Focus guards causing page behind to scroll to the bottom when tabbing through

To replicate:

  1. Visit
    https://madou.github.io/react-best-modal/?selectedKind=Styling&selectedStory=and%20finally%20also%20disabling%20body%20from%20scrolling%20behind&full=0&addons=1&stories=1&panelRight=0

  2. Click show modal

  3. Tab once

  4. See the background behind has scrolled to the bottom

This doesn't happen when I turn on noFocusGuards prop (however that then lets us focus to the nav bar - not great)

I'll make up a standalone example without best modal soon! πŸ‘

Sideeffect from React-side-effect

React-focus-lock, to have only one lock "active" in a single point of time uses react-side-effect, which tracks mounted instances, thus letting only "last mounted" lock to be active.

Meanwhile react-side-effect SSR support leads to the memory leak - instance got mounted(in componentWillMount, but never got unmounted, as long there is no componentWillUnmount with renderToString.

This is not an issue for "fullcream" render, but as long focus lock does nothing on server - lets migrate to react-clientside-effect, which does nothing on server side.

Return focus not working

happy new year!

this line gets executed when unmounting:

this.originalFocusedElement.focus();

this line is executed sometime after the unmounting:

result = moveFocusInside(observed, lastActiveFocus);

which means focus can never (synchronously) be returned to the initial element. (even if returnFocus is turned off in focus lock. atm i'm getting around this by a setTimeout, but that's not great.

thoughts?

Fix linting issues

Hey, sorry, my latest PR makes this PR red because linter is not passed. Let me know if you have no time to fix it, so I'll create a PR.

Multiple modals and returnFocus

Focus is not returned with multiple <FocusLock>s.

Sample CRA project: https://github.com/andraaspar/react-focus-lock-stack-missing

To reproduce:

  1. Click β€˜Open modal 1’
  2. On the modal, click β€˜Open modal 2’
  3. On the 2nd modal, click β€˜X2’ β†’ Focus is set to β€˜Open modal 2’ instead of β€˜X1’
  4. On the modal, click β€˜X1’ β†’ Focus is not returned to β€˜Open modal 1’

Initially disabled element breaks focus wrap

First off: this is an awesome library that's saved me a big headache in implementation. Thanks for your work on this!

I've found an issue with having an initially disabled button in the mix. In my case, I disable the save button on a modal form if no changes have been made. However, I've found that having that element initially disabled breaks the focus wrap so that it stops on the button. Perhaps something to do with how the lock is initialized?

Here's a minimal example:

class ExampleForm extends React.Component {
  state = {
    string: ""
  }

  render(){
    return (
      <FocusLock>
        <input 
          onChange={ ({ target }) => this.setState({ string: target.value }) } 
          type="text" 
          tabIndex="1" 
        />
        <button 
          disabled={this.state.string.length == 0} 
          tabIndex="1"
        >
          Save
        </button>
      </FocusLock>
    )
  }
}

What I may do in the meantime is change how I disable buttons, although it would be ideal to stick with the browser implementation. Thanks for taking a look at this - let me know if there's any other info I can gather.

Wrong name of file in published package

It looks like we have the wrong name of 'AutoFocusInside' file.

When I pulled the latest package I got this error:

./node_modules/react-focus-lock/dist/index.js
Module not found: /Users/ezhlobo/Work/Datarockets/accubitz/accubitz-react/node_modules/react-focus-lock/dist/AutoFocusInside.js does not match the corresponding path on disk AutofocusInside.js.

Can I add some selectors to whitelist?

Hi!

Thanks for the amazing plugin. What I have here is not an issue but a short question actually. Let’s suppose that I have a customer support or helpdesk plugin or smth like that (Intercom, Livetex, etc.). Usually this type of plugins create separate modal dialogs. When I have my own modal dialog (wrapped with FocusLock) on the screen I can't focus on a third-party dialog.

So, the question is β€” can I add a selector (from third-party dialog) to a some kind of whitelist? Should I use group property for this?

Thanks in advance.

Iframe inside FocusLock losing focus

I'm using the FocusLock to wrap a modal that contains React components and a React iframe component. The iframe is a text editor which contains a toolbar with a dropdown.

When I press enter to open the dropdown options the iframe loses focus and the first focusable element inside the modal is focused again.

Is there a way to not lose the iframe focus that already contains accessibility features?

Clickable elements below the fold in Safari

Problem

In macOS Safari, clicking links below the fold inside the FocusLock component causes focus to jump to the first link instead of going the destination of the link.

Here's a codesandbox that reproduces the issue: https://codesandbox.io/s/responsive-navigation-safari-bug-6hvj4

Notes

  • Removing the FocusLock component from the Drawer component fixes this issue.

  • Removing tabIndex="-1" from the wrapper div created by @reach/router also fixes this issue:

    image

Related to primer/doctocat#56

Not working with portals

How to reproduce?
Just render portal inside FocusTrap (example)

Why it's matter?
Dropdown menus usually render in portals. And it would be impossible to put focus on them to handle keydowns for example.

I don't know if it is possible to solve this issue

nested FocusLock

@theKashey could you provide a story for a nested FocusLock? I am trying to achieve this with a dialog (modal) that opens another dialog.

When I open the first dialog the focus lock has no autofocus and is not trapped.
When I open the second dialog the focus lock works as expected.

Here is the code for the dialog component.

<FocusLock>
    <Dialog
        {...props}
    >
        {children}
    </Dialog>
</FocusLock>

Pass all the rest props to Lock div

Hi! I want to use react-focus-lock for creating Modal with animation, but I don't want to produce more divs than necessary.

Would be great if lockProps in FocusLock could be created using rest operator from own props. In that case you don't need to manually pass className from parent to div like you do now.
Like

const {
      children,
      disabled,
      noFocusGuards,
      persistentFocus,
      autoFocus,
      allowTextSelection,
      group,
      whiteList,
      ...lockProps
} = this.props;

const { observed } = this.state;

if (typeof allowTextSelection !== 'undefined') {
   // eslint-disable-next-line no-console
  console.warn('React-Focus-Lock: allowTextSelection is deprecated and enabled by default');
}

lockProps.ref = this.setObserveNode;
lockProps.onBlur = onBlur;
lockProps.onFocus = onFocus;
lockProps[constants.FOCUS_GROUP] = group;
lockProps[constants.FOCUS_DISABLED] = disabled && 'disabled';

return (
      <Fragment>
        {!noFocusGuards && [
          <div key="guard-first" data-focus-guard tabIndex={disabled ? -1 : 0} style={hidden} />, // nearest focus guard
          <div key="guard-nearest" data-focus-guard tabIndex={disabled ? -1 : 1} style={hidden} />, // first tabbed element guard
        ]}
        <div {...lockProps}>
        ....

The point is to take out of props object all own props and pass down all the rest as is. In that case I can pass any valid div props to FocusLock. In my case I want to wait for transitionEnd on modal animation, like <FocusLock className={...} onTransitionEnd={...} onClick={...}>.
As you also might have noticed I assign other specific props directly to lockProps and just write <div {...lockProps}> that would be transformed to createElement('div', lockProps, ..), to avoid one more spread operator on jsx (that could be useful if focus-lock is being rendered inside spring transitions, like in react-motions, react-spring).

Imports are not permitted in module augmentations.

Please see the attachment below. Getting this error when trying to build React app. Any ideas how to fix this?

Error message: imports are not permitted in module augmentations
[ts] Imports are not permitted in module augmentations. Consider moving them to the enclosing external module.

react-focus-lock

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.