Git Product home page Git Product logo

tabbable's Introduction

focus-trap CI license

All Contributors

Trap focus within a DOM node.

There may come a time when you find it important to trap focus within a DOM node — so that when a user hits Tab or Shift+Tab or clicks around, she can't escape a certain cycle of focusable elements.

You will definitely face this challenge when you are trying to build accessible modals.

This module is a little, modular vanilla JS solution to that problem.

Use it in your higher-level components. For example, if you are using React check out focus-trap-react, a light wrapper around this library. If you are not a React user, consider creating light wrappers in your framework-of-choice.

What it does

When a focus trap is activated, this is what should happen:

  • Some element within the focus trap receives focus. By default, this will be the first element in the focus trap's tab order (as determined by tabbable). Alternately, you can specify an element that should receive this initial focus.
  • The Tab and Shift+Tab keys will cycle through the focus trap's tabbable elements but will not leave the focus trap.
  • Clicks within the focus trap behave normally; but clicks outside the focus trap are blocked.
  • The Escape key will deactivate the focus trap.

When the focus trap is deactivated, this is what should happen:

  • Focus is passed to whichever element had focus when the trap was activated (e.g. the button that opened the modal or menu).
  • Tabbing and clicking behave normally everywhere.

Check out the demos.

For more advanced usage (e.g. focus traps within focus traps), you can also pause a focus trap's behavior without deactivating it entirely, then unpause at will.

Installation

npm install focus-trap

UMD

You can also use a UMD version published to unpkg.com as dist/focus-trap.umd.js and dist/focus-trap.umd.min.js.

NOTE: The UMD build does not bundle the tabbable dependency. Therefore you will have to also include that one, and include it before focus-trap.

<head>
  <script src="https://unpkg.com/tabbable/dist/index.umd.js"></script>
  <script src="https://unpkg.com/focus-trap/dist/focus-trap.umd.js"></script>
</head>

Browser Support

As old and as broad as reasonably possible, excluding browsers that are out of support or have nearly no user base.

Focused on desktop browsers, particularly Chrome, Edge, FireFox, Safari, and Opera.

Focus-trap is not officially tested on any mobile browsers or devices.

❗️ Safari: By default, Safari does not tab through all elements on a page, which alters the normal DOM-based tab order expected by focus-trap. If you use or support Safari with this library, make sure you and your users know they must enable the Preferences > Advanced > Press Tab to highlight each item on a webpage feature. Otherwise, your traps will not work the way you expect them to.

⚠️ Microsoft no longer supports any version of IE, so IE is no longer supported by this library.

💬 Focus-trap relies on tabbable so its browser support is at least what tabbable supports.

💬 Keep in mind that performance optimization and old browser support are often at odds, so tabbable may not always be able to use the most optimal (typically modern) APIs in all cases.

Usage

createFocusTrap()

import * as focusTrap from 'focus-trap'; // ESM
const focusTrap = require('focus-trap'); // CJS
// UMD: `focusTrap` is defined as a global on `window`

trap = focusTrap.createFocusTrap(element[, createOptions]);

Returns a new focus trap on element (one or more "containers" of tabbable nodes that, together, form the total set of nodes that can be visited, with clicks or the tab key, within the trap).

element can be:

  • a DOM node (the focus trap itself);
  • a selector string (which will be passed to document.querySelector() to find the DOM node); or
  • an array of DOM nodes or selector strings (where the order determines where the focus will go after the last tabbable element of a DOM node/selector is reached).

A focus trap must have at least one container with at least one tabbable/focusable node in it to be considered valid. While nodes can be added/removed at runtime, with the trap adjusting to added/removed tabbable nodes, an error will be thrown if the trap ever gets into a state where it determines none of its containers have any tabbable nodes in them and the fallbackFocus option does not resolve to an alternate node where focus can go.

createOptions

  • onActivate {() => void}: A function that will be called before sending focus to the target element upon activation.
  • onPostActivate {() => void}: A function that will be called after sending focus to the target element upon activation.
  • onPause {() => void}: A function that will be called immediately after the trap's state is updated to be paused.
  • onPostPause {() => void}: A function that will be called after the trap has been completely paused and is no longer managing/trapping focus.
  • onUnpause {() => void}: A function that will be called immediately after the trap's state is updated to be active again, but prior to updating its knowledge of what nodes are tabbable within its containers, and prior to actively managing/trapping focus.
  • onPostUnpause {() => void}: A function that will be called after the trap has been completely unpaused and is once again managing/trapping focus.
  • checkCanFocusTrap {(containers: Array<HTMLElement | SVGElement>) => Promise<void>}: Animated dialogs have a small delay between when onActivate is called and when the focus trap is focusable. checkCanFocusTrap expects a promise to be returned. When that promise settles (resolves or rejects), focus will be sent to the first tabbable node (in tab order) in the focus trap (or the node configured in the initialFocus option).
  • onDeactivate {() => void}: A function that will be called before returning focus to the node that had focus prior to activation (or configured with the setReturnFocus option) upon deactivation.
  • onPostDeactivate {() => void}: A function that will be called after the trap is deactivated, after onDeactivate. If the returnFocus deactivation option was set, it will be called after returning focus to the node that had focus prior to activation (or configured with the setReturnFocus option) upon deactivation; otherwise, it will be called after deactivation completes.
  • checkCanReturnFocus {(trigger: HTMLElement | SVGElement) => Promise<void>}: An animated trigger button will have a small delay between when onDeactivate is called and when the focus is able to be sent back to the trigger. checkCanReturnFocus expects a promise to be returned. When that promise settles (resolves or rejects), focus will be sent to to the node that had focus prior to the activation of the trap (or the node configured in the setReturnFocus option).
  • initialFocus {HTMLElement | SVGElement | string | false | undefined | (() => HTMLElement | SVGElement | string | false | undefined)}: By default, when a focus trap is activated the first element in the focus trap's tab order will receive focus. With this option you can specify a different element to receive that initial focus. Can be a DOM node, or a selector string (which will be passed to document.querySelector() to find the DOM node), or a function that returns any of these. You can also set this option to false (or to a function that returns false) to prevent any initial focus at all when the trap activates.
    • 💬 Setting this option to false (or a function that returns false) will prevent the fallbackFocus option from being used.
    • Returning undefined from a function will result in the default behavior.
    • ⚠️ See warning below about Shadow DOM and selector strings.
  • fallbackFocus {HTMLElement | SVGElement | string | () => HTMLElement | SVGElement | string}: By default, an error will be thrown if the focus trap contains no elements in its tab order. With this option you can specify a fallback element to programmatically receive focus if no other tabbable elements are found. For example, you may want a popover's <div> to receive focus if the popover's content includes no tabbable elements. Make sure the fallback element has a negative tabindex so it can be programmatically focused. The option value can be a DOM node, a selector string (which will be passed to document.querySelector() to find the DOM node), or a function that returns any of these.
    • 💬 If initialFocus is false (or a function that returns false), this function will not be called when the trap is activated, and no element will be initially focused. This function may still be called while the trap is active if things change such that there are no longer any tabbable nodes in the trap.
    • ⚠️ See warning below about Shadow DOM and selector strings.
  • escapeDeactivates {boolean} | (e: KeyboardEvent) => boolean): Default: true. If false or returns false, the Escape key will not trigger deactivation of the focus trap. This can be useful if you want to force the user to make a decision instead of allowing an easy way out. Note that if a function is given, it's only called if the ESC key was pressed.
  • clickOutsideDeactivates {boolean | (e: MouseEvent | TouchEvent) => boolean}: If true or returns true, a click outside the focus trap will immediately deactivate the focus trap and allow the click event to do its thing (i.e. to pass-through to the element that was clicked). This option takes precedence over allowOutsideClick when it's set to true. Default: false.
    • 💬 If a function is provided, it will be called up to twice (but only if the click occurs outside the trap's containers): First on the mousedown (or touchstart on mobile) event and, if true was returned, again on the click event. It will get the same node each time, and it's recommended that the returned value is also the same each time. Be sure to check the event type if the double call is an issue in your code.
    • ⚠️ If you're using a password manager such as 1Password, where the app adds a clickable icon to all fillable fields, you should avoid using this option, and instead use the allowOutsideClick option to better control exactly when the focus trap can be deactivated. The clickable icons are usually positioned absolutely, floating on top of the fields, and therefore not part of the container the trap is managing. When using the clickOutsideDeactivates option, clicking on a field's 1Password icon will likely cause the trap to be unintentionally deactivated.
  • allowOutsideClick {boolean | (e: MouseEvent | TouchEvent) => boolean}: If set and is or returns true, a click outside the focus trap will not be prevented (letting focus temporarily escape the trap, without deactivating it), even if clickOutsideDeactivates=false. Default: false.
    • 💬 If this is a function, it will be called up to twice on every click (but only if the click occurs outside the trap's containers): First on mousedown (or touchstart on mobile), and then on the actual click if the function returned true on the first event. Be sure to check the event type if the double call is an issue in your code.
    • 💡 When clickOutsideDeactivates=true, this option is ignored (i.e. if it's a function, it will not be called).
    • Use this option to control if (and even which) clicks are allowed outside the trap in conjunction with clickOutsideDeactivates=false.
  • returnFocusOnDeactivate {boolean}: Default: true. If false, when the trap is deactivated, focus will not return to the element that had focus before activation.
    • 💬 When using this option in conjunction with clickOutsideDeactivates=true:
      • If returnFocusOnDeactivate=true and the outside click causing deactivation is on a focusable element, focus will not return to that element; instead, it will return to the node focused just before activation.
      • If returnFocusOnDeactivate=false and the outside click is on a focusable node, focus will remain on that node instead of the node focused just before activation. If the outside click is on a non-focusable node, then "nothing" will have focus post-deactivation.
  • setReturnFocus {HTMLElement | SVGElement | string | (previousActiveElement: HTMLElement | SVGElement) => HTMLElement | SVGElement | string | false}: By default, on deactivation, if returnFocusOnDeactivate=true (or if returnFocus=true in the deactivation options), focus will be returned to the element that was focused just before activation. With this option, you can specify another element to programmatically receive focus after deactivation. It can be a DOM node, a selector string (which will be passed to document.querySelector() to find the DOM node upon deactivation), or a function that returns any of these to call upon deactivation (i.e. the selector and function options are only executed at the time the trap is deactivated). Can also be false (or return false) to leave focus where it is at the time of deactivation.
    • 💬 Using the selector or function options is a good way to return focus to a DOM node that may not exist at the time the trap is activated.
    • ⚠️ See warning below about Shadow DOM and selector strings.
  • preventScroll {boolean}: By default, focus() will scroll to the element if not in viewport. It can produce unintended effects like scrolling back to the top of a modal. If set to true, no scroll will happen.
  • delayInitialFocus {boolean}: Default: true. Delays the autofocus to the next execution frame when the focus trap is activated. This prevents elements within the focusable element from capturing the event that triggered the focus trap activation.
  • document {Document}: Default: window.document. Document where the focus trap will be active. This enables the use of FocusTrap inside an iFrame.
    • ⚠️ Note that FocusTrap will be unable to trap focus outside the iFrame if you configure this option to be the iFrame's document. It will only trap focus inside of it (as the demo shows). If you want to trap focus outside as well, then your FocusTrap must be configured on an element that contains the iFrame.
  • tabbableOptions: (optional) tabbable options configurable on FocusTrap (all the common options).
  • trapStack (optional) {Array<FocusTrap>}: Define the global trap stack. This makes it possible to share the same stack in multiple instances of focus-trap in the same page such that auto-activation/pausing of traps is properly coordinated among all instances as activating a trap when another is already active should result in the other being auto-paused. By default, each instance will have its own internal stack, leading to conflicts if they each try to trap the focus at the same time.
  • isKeyForward {(event: KeyboardEvent) => boolean}: (optional) Determines if the given keyboard event is a "tab forward" event that will move the focus to the next trapped element in tab order. Defaults to the TAB key. Use this to override the trap's behavior if you want to use arrow keys to control keyboard navigation within the trap, for example. Also see isKeyBackward() option.
    • ⚠️ Using this option will not automatically prevent use of the TAB key as the browser will continue to respond to it by moving focus forward because that's what using the TAB key does in a browser, but it will no longer respect the trap's container edges as it normally would. You will need to add your own keydown handler to call preventDefault() on a TAB key event if you want to completely suppress the use of the TAB key.
  • isKeyBackward {(event: KeyboardEvent) => boolean}: (optional) Determines if the given keyboard event is a "tab backward" event that will move the focus to the previous trapped element in tab order. Defaults to the SHIFT+TAB key. Use this to override the trap's behavior if you want to use arrow keys to control keyboard navigation within the trap, for example. Also see isKeyForward() option.
    • ⚠️ Using this option will not automatically prevent use of the SHIFT+TAB key as the browser will continue to respond to it by moving focus backward because that's what using the SHIFT+TAB key sequence does in a browser, but it will no longer respect the trap's container edges as it normally would. You will need to add your own keydown handler to call preventDefault() on a TAB key event if you want to completely suppress the use of the SHIFT+TAB key sequence.

Shadow DOM

Selector strings

⚠️ Beware that putting a focus-trap inside an open Shadow DOM means you must not use selector strings for options that support these (because nodes inside Shadow DOMs, even open shadows, are not visible via document.querySelector()).

Closed shadows

If you have closed shadow roots that you would like considered for tabbable/focusable nodes, use the tabbableOptions.getShadowRoot option to provide Tabbable (used internally) with a reference to a given node's shadow root so that it can be searched for candidates.

Positive Tabindexes

⚠️ Using positive tab indexes (i.e. <button tabindex="1">Label</button>) is not recommended, primarily for accessibility reasons. Supporting them properly also means a lot of hoops to jump through when Shadow DOM is used as some key DOM APIs like Node.compareDocumentPosition() do not properly support Shadow DOM.

As such, focus-trap considers using positive tabindexes an edge case and only supports them in single-container traps with some caveats for related edge case behavior (see the demo for more details).

If you try to create a multi-container trap where at least one container has one node with a positive tabindex, an exception will be thrown:

At least one node with a positive tabindex was found in one of your focus-trap's multiple containers. Positive tabindexes are only supported in single-container focus-traps.

trap.active

trap.active: boolean

True if the trap is currently active.

trap.paused

trap.paused: boolean

True if the trap is currently paused.

trap.activate()

trap.activate([activateOptions]) => FocusTrap

Activates the focus trap, adding various event listeners to the document.

If focus is already within it the trap, it remains unaffected. Otherwise, focus-trap will try to focus the following nodes, in order:

  • createOptions.initialFocus
  • The first tabbable node in the trap
  • createOptions.fallbackFocus

If none of the above exist, an error will be thrown. You cannot have a focus trap that lacks focus.

Returns the trap.

activateOptions:

These options are used to override the focus trap's default behavior for this particular activation.

  • onActivate {() => void}: Default: whatever you chose for createOptions.onActivate. null or false are the equivalent of a noop.
  • onPostActivate {() => void}: Default: whatever you chose for createOptions.onPostActivate. null or false are the equivalent of a noop.
  • checkCanFocusTrap {(containers: Array<HTMLElement | SVGElement>) => Promise<void>}: Default: whatever you chose for createOptions.checkCanFocusTrap.

trap.deactivate()

trap.deactivate([deactivateOptions]) => FocusTrap

Deactivates the focus trap.

Returns the trap.

deactivateOptions:

These options are used to override the focus trap's default behavior for this particular deactivation.

  • returnFocus {boolean}: Default: whatever you set for createOptions.returnFocusOnDeactivate. If true, then the setReturnFocus option (specified when the trap was created) is used to determine where focus will be returned.
  • onDeactivate {() => void}: Default: whatever you set for createOptions.onDeactivate. null or false are the equivalent of a noop.
  • onPostDeactivate {() => void}: Default: whatever you set for createOptions.onPostDeactivate. null or false are the equivalent of a noop.
  • checkCanReturnFocus {(trigger: HTMLElement | SVGElement) => Promise<void>}: Default: whatever you set for createOptions.checkCanReturnFocus. Not called if the returnFocus option is falsy. trigger is either the originally focused node prior to activation, or the result of the setReturnFocus configuration option.

trap.pause()

trap.pause([pauseOptions]) => FocusTrap

Pause an active focus trap's event listening without deactivating the trap.

If the focus trap has not been activated, nothing happens.

Returns the trap.

Any onDeactivate callback will not be called, and focus will not return to the element that was focused before the trap's activation. But the trap's behavior will be paused.

This is useful in various cases, one of which is when you want one focus trap within another. demo-six exemplifies how you can implement this.

pauseOptions:

These options are used to override the focus trap's default behavior for this particular pausing.

  • onPause {() => void}: Default: whatever you chose for createOptions.onPause. null or false are the equivalent of a noop.
  • onPostPause {() => void}: Default: whatever you chose for createOptions.onPostPause. null or false are the equivalent of a noop.

trap.unpause()

trap.unpause([unpauseOptions]) => FocusTrap

Unpause an active focus trap. (See pause(), above.)

Focus is forced into the trap just as described for focusTrap.activate().

If the focus trap has not been activated or has not been paused, nothing happens.

Returns the trap.

unpauseOptions:

These options are used to override the focus trap's default behavior for this particular unpausing.

  • onUnpause {() => void}: Default: whatever you chose for createOptions.onUnpause. null or false are the equivalent of a noop.
  • onPostUnpause {() => void}: Default: whatever you chose for createOptions.onPostUnpause. null or false are the equivalent of a noop.

trap.updateContainerElements()

trap.updateContainerElements(HTMLElement | SVGElement | string | Array<HTMLElement | SVGElement | string>) => FocusTrap

Update the element(s) that are used as containers for the focus trap.

When you call createFocusTrap(), you give it an element (or selector), or an array of elements (or selectors) to keep the focus within. This method simply allows you to update which elements to keep the focus within even while the trap is active.

A use case for this is found in focus-trap-react, where React ref's may not be initialized yet, but when they are you want to have them be a container element.

Returns the trap.

Examples

Read code in docs/ and see how it works.

Here's generally what happens in default.js (the "default behavior" demo):

const { createFocusTrap } = require('../../index');

const container = document.getElementById('default');

const focusTrap = createFocusTrap('#default', {
  onActivate: () => container.classList.add('is-active'),
  onDeactivate: () => container.classList.remove('is-active'),
});

document
  .getElementById('activate-default')
  .addEventListener('click', focusTrap.activate);
document
  .getElementById('deactivate-default')
  .addEventListener('click', focusTrap.deactivate);

Other details

One at a time

Only one focus trap can be listening at a time. If a second focus trap is activated the first will automatically pause. The first trap is unpaused and again traps focus when the second is deactivated.

Focus trap manages a queue of traps: if A activates; then B activates, pausing A; then C activates, pausing B; when C then deactivates, B is unpaused; and when B then deactivates, A is unpaused.

Use predictable elements for the first and last tabbable elements in your trap

The focus trap will work best if the first and last focusable elements in your trap are simple elements that all browsers treat the same, like buttons and inputs.**

Tabbing will work as expected with trickier, less predictable elements — like iframes, shadow trees, audio and video elements, etc. — as long as they are between more predictable elements (that is, if they are not the first or last tabbable element in the trap).

This limitation is ultimately rooted in browser inconsistencies and inadequacies, but it comes to focus-trap through its dependency Tabbable. You can read about more details in the Tabbable documentation.

Your trap should include a tabbable element or a focusable container

You can't have a focus trap without focus, so an error will be thrown if you try to initialize focus-trap with an element that contains no tabbable nodes.

If you find yourself in this situation, you should give you container tabindex="-1" and set it as initialFocus or fallbackFocus. A couple of demos illustrate this.

Development

Because of the nature of the functionality, involving keyboard and click and (especially) focus events, JavaScript unit tests don't make sense. After all, JSDom does not fully support focus events. Since the demo was developed to also be the test, we use Cypress to automate running through all demos in the demo page.

Help

Testing in JSDom

⚠️ JSDom is not officially supported. Your mileage may vary, and tests may break from one release to the next (even a patch or minor release).

This topic is just here to help with what we know may affect your tests.

In general, a focus trap is best tested in a full browser environment such as Cypress, Playwright, or Nightwatch where a full DOM is available.

Sometimes, that's not entirely desirable, and depending on what you're testing, you may be able to get away with using JSDom (e.g. via Jest), but you'll have to configure your traps using the tabbableOptions.displayCheck: 'none' option.

See Testing tabbable in JSDom for more details.

ERROR: Your focus-trap must have at least one container with at least one tabbable node in it at all times

This error happens when the containers you specified when you setup your focus trap do not have -- or no longer have -- any tabbable elements in them, which means that focus will inevitably escape your trap because focus must always go somewhere.

You will hit this error if your trap does not have (or no longer has) any tabbable (and therefore focusable) elements in it, and it was not configured with a backup element (see the fallbackFocus option -- which must still be in the trap, but does not necessarily have to be tabbable, i.e. it could have tabindex="-1", making it focusable, but not tabbable).

This often happens when traps are related to elements that appear and disappear dynamically. Typically, the error will fire either as the element is being shown (because the trap gets created before the trapped children have been inserted into the DOM), or as it's being hidden (because the trapped children are destroyed before the trap is either destroyed or disabled).

First element in trap is unreachable with the TAB key

If you create a trap and try to use the TAB key to set focus to the first element in your trap, the first element seems unreachable because focus keeps skipping over it for some reason.

This can happen in projects where the Angular-related zone.js module is being used because Zone can interfere with Focus-trap's ability to control where focus goes when it leaves an edge node (that is, a node that is on the edge of a container in which it is trapping focus).

What is actually happening is that Focus-trap is correctly wrapping focus around to that first element (or last element, if going in reverse with SHIFT+TAB, and you're seeing that get skipped) and setting focus to it, but because of Zone's interference (in which Focus-trap's call to preventDefault() on the focus event triggered by the TAB key press is rendered ineffective), once Focus-trap is done handling the event, the browser hasn't received the signal that its default behavior should be prevented, and so it proceeds to move focus to the next element -- effectively "skipping" over the element to which Focus-trap set focus, making it seem "unreachable".

Unfortunately, there's no good workaround to this issue from Focus-trap's perspective. The issue was reported to Angular (not by Focus-trap) and has a PR (also not by Focus-trap) for a fix.

This was originally investigated in #1165 if you want to go deeper.

Contributing

See CONTRIBUTING.

Contributors

In alphabetical order:

Anders Thorsen
Anders Thorsen

🐛
Benjamin Parish
Benjamin Parish

🐛
Clint Goodman
Clint Goodman

💻 📖 💡 ⚠️
Daniel Tonon
Daniel Tonon

📖 🔧 ️️️️♿️ 💻
DaviDevMod
DaviDevMod

📖 💻 🐛
David Clark
David Clark

💻 🐛 🚇 ⚠️ 📖 🚧
Dependabot
Dependabot

🚧
Joas Schilling
Joas Schilling

👀
John Molakvoæ
John Molakvoæ

🤔
Kasper Garnæs
Kasper Garnæs

📖 🐛 💻
Matt Driscoll
Matt Driscoll

🐛 💻
Maxime
Maxime

🐛
Michael Reynolds
Michael Reynolds

🐛
Nate Liu
Nate Liu

⚠️
Piotr Panek
Piotr Panek

🐛 📖 💻 ⚠️
Randy Puro
Randy Puro

🐛
Sadick
Sadick

💻 ⚠️ 📖
Scott Blinch
Scott Blinch

📖
Sean McPherson
Sean McPherson

💻 📖
Sebastian Kriems
Sebastian Kriems

🐛
Slapbox
Slapbox

🐛
Stefan Cameron
Stefan Cameron

💻 🐛 🚇 ⚠️ 📖 🚧
Tyler Hawkins
Tyler Hawkins

🔧 ⚠️ 📖
Vasiliki Boutas
Vasiliki Boutas

🐛
Vinicius Reis
Vinicius Reis

💻 🤔
Wandrille Verlut
Wandrille Verlut

💻 ⚠️ 📖 🔧
Will Mruzek
Will Mruzek

💻 📖 💡 ⚠️ 💬
Zioth
Zioth

🤔 🐛
glushkova91
glushkova91

📖
jpveooys
jpveooys

🐛
Ábris Simon
Ábris Simon

💻 🐛

tabbable's People

Contributors

acdvorak avatar aceysmith avatar allcontributors[bot] avatar andarist avatar bbenjamin avatar benjamin-t-frost avatar chaance avatar davidevmod avatar davidtheclark avatar dependabot[bot] avatar felipeochoa avatar github-actions[bot] avatar hypnosphi avatar idoros avatar rgrove avatar rvsia avatar shamsartem avatar stefcameron avatar stidges avatar stof avatar thawkin3 avatar tidychips avatar vonagam avatar white-water avatar yuheiy 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

tabbable's Issues

Security policy questions

The Drupal project is considering adding this library as one of our dependencies and so we're performing a standard stability review. We're looking into adopting this in https://www.drupal.org/project/drupal/issues/3113649. I fully acknowledge the likelihood of security issues tabbable are very low, so I particularly appreciate the time taken to answer these.

Since there isn't a policy at https://github.com/focus-trap/tabbable/security I'm curious if you have any official policies documented somewhere regarding:

Security releases
For example, does more than one version receive security fixes, or only the current version? It looks like 5.x is the first after the maintainer switch, so this may not apply currently, but would like to know if it would be applicable with a >=6.x release.
Release windows/cadence
For example, do they happen as necessary on any given day, or on a set schedule after a certain passage of time (e.g. once a month)? Looking at all the recent releases (which is great to see!), I can probably make some assumptions, but would like to confirm.
Backwards compatibility guarantees
Tabbable uses semver, so I assume the major version promises not to break BC. Are there any guarantees that a geven version will be supported for some period of time (an LTS version, for example), also with the understanding that things possibly changed between 4 and 5?

Thanks, I'm pleased to see all the recent activity on this library!

Accept more than one container in which to look for tabbable nodes

It'd be nice to be able to pass more than one element to tabbable. The current tabbable doesn't allow us to easily find the relative tab orders of elements in multiple containers, if we were to call tabbable on each container.

Would it be reasonable to add this feature to tabbable? Happy to send a pull request if that's okay.

Example:

Suppose the numbers following "el" represent the tab orders of focusable the elements...

container1: el1, el4, el3
container2: el2, el5, el6

Calling tabbable on container1 and container2 would give us...

container1: el1, el3, el4
container2: el2, el5, el6

However, we wouldn't know how to combine the two lists of els, or know that el2 from container2 should be tabbed to before el3 from container1.

Proposal

Allow tabbable to accept multiple elements: tabbable(el1, el2, ...). tabbable returns an array of tabbable nodes in tab order from all of the passed elements/nodes.

Use cases

  • Composite modals (ie. modals with multiple "dialogs").
  • Non-fullscreen modals/overlays (e.g. overlay does not cover a top nav bar).

Seeking co-maintainers!

I've been shifting my focus away from UI development, so don't plan on addressing new issues myself. If you use this library and want to see development continue, you can make that happen by becoming a co-maintainer — with permissions to triage issues, merge PRs, cut releases, etc.

Please comment below if you're interested!

Another possibility is for a dedicated owner to fork this code and create a new package. I'd be happy to link to that library from the README of this one.

tabbable breaks when processing an element with a "scope" attribute

I have a situation where a TH element has both scope="col" and tabindex="0" attributes, which is causing Tabbable to break with "Cannot read properties of undefined (reading 'forEach').

index.js:425 Uncaught TypeError: Cannot read properties of undefined (reading 'forEach')
at e (index.js:425:14)
at index.js:429:32
at Array.forEach ()
at e (index.js:425:14)
at e.tabbable (index.js:473:10)

Tabbable appears to be evaluating !! item.scope as true because the attribute is present, and assumes there is a ShadowRoot to process.

Optimizing displayCheck: 'full'

It is possible to assess whether an element, or one of its ancestors, have display: none without having to traverse the DOM.

This condition can be simplified to:

if (!displayCheck || displayCheck === 'full') {
  return !node.getClientRects().length;
}

I actually don't know if checking for getClientRects().length is really equivalent to that expensive while loop.

According to the specs, the getClientRects() method returns an empty list

If the element on which it was invoked does not have an associated layout box

What exactly are all the cases in which an element does not have an associated layout box? I am not sure.

But the e2e tests currently present in the project are all passing.
However I didn't go through the tests myself and I don't know what they do, I only checked this very small set of possible edge cases and it looks like the method should be good enough.

May I open a PR?

By the way, since the getBoundingClientRect() method internally uses getClientRects(), the "full" option would become faster than the "non-zero-area" one. But I guess that the latter would still be useful for accessibility purpose.

Contenteditable

jQuery UI isn't picking up on it either, but I think contenteditable elements should be tabbable. You can try here: codepen

The fix is easy, I think; just add [contenteditable]:not([contenteditable="false"]) to list of candidate selectors.

Anchors without an href and with a tabindex of 0 don't get picked up

This can be fixed by changing line 16 of index.js from..

|| (candidate.tagName === 'A' && !candidate.href && !candidate.tabIndex)

to..

|| (candidate.tagName === 'A' && !candidate.href && (!candidateIndex && candidateIndex !== 0))

I won't be using tabbable this way, but someone might be.

Tab into iframe

Tabbable does not select iframes it as candidate, and thus it is impossible to tab into them. This is needed in some scenarios, for instance when you have a reCaptcha (which is constructed as an iframe) in a form. One solution could be to just add 'iframe' to the candidateSelectors, but perhaps there is some issues with that?

Tab Across Web Components

It would be great if tabbable could identify the tab ordering between input fields across multiple web components.

Unfortunately since query selectors can't span shadow roots, it may require a very different approach.

Radios in a set are all tabbable but only one at a time should be

When you tab through a radio set (radio inputs sharing a name), only one of them is actually tabbable — one of the following:

  • The one that has been selected.
  • If none are selected and you're moving forward with tab, the first one.
  • If none are selected and you're moving backward with shift-tab, the last one.

This library doesn't take that into account: it treats radio inputs like other inputs, assuming they're all tabbable.

This would be a tricky problem to fix without adding a fair amount of delicate complexity. The best solution might be similar to the iframe problem: #23 (comment) — only hijack tab and shift+tab if you're leaving the focus trap. However, the big blocking failing of this solution remains to be solved: I think the approach will only work if the first and last tabbable elements in the container are recognizable by the library. For example, if the first element that's actually tabbable (according to the browser) is in an iframe or the shadow dom or is a selected radio button that's not the first one of it's group — then how is this library going to know that that's what should receive focus?

Open to solutions.

cc @doodirock

isTabbableRadio doesn't properly escape query selectors

It's possible for radio button names to contain characters that are invalid when used in a CSS selector (e.g. "). Below is an example that causes the tabbable function to throw an error due to an invalid selector constructed at:

'input[type="radio"][name="' + node.name + '"]'

HTML:

<div>
  <input type="radio" id="huey" name="&quot;duck&quot;" value="huey"
         checked>
  <label for="huey">Huey</label>
</div>

<div>
  <input type="radio" id="dewey" name="&quot;duck&quot;" value="dewey">
  <label for="dewey">Dewey</label>
</div>

JS:

import { tabbable } from 'tabbable';
// Throws DOMException: Document.querySelectorAll: 'input[type="radio"][name=""duck""]' is not a valid selector
tabbable(document.body);

It's also possible to inject values into the selector with a name like:

<input type="radio" id="dewey" name="&quot;] .some-class[data-attr=&quot;some-attribute" value="dewey">

A potential solution is CSS.escape, but it does require a polyfill for IE.

IE11, svg with tabindex=0 does not have tabIndex property

In IE11, a SVG with tabindex="0" does not have a tabIndex property so gets added to the orderedTabbables array no matter where it is in the DOM.

candidateIndex = parseInt(candidate.getAttribute('tabindex'), 10) || candidate.tabIndex;

Always triggers the OR condition if tabindex="0", which then assumes tabIndex is a valid property. SVGs return undefined for the tabIndex property.

Add code coverage badge to the README

Now that the test suite is converted over to use Jest and DOM Testing Library, what do you think about adding a code coverage badge to the README to show off the code coverage?

I see in the focus-trap-react repo we used Codecov along with GitHub Actions for the CI. In this tabbable repo it's also using GitHub Actions for the CI, so I assume we can generate the code coverage report there and then feed the results to Codecov, right? Or maybe even generate the badge without using Codecov if that's not something you want to do, and we could just use GitHub Actions directly?

Thoughts?

jsdom issue: 'slot):not([inert]' is not a valid selector

If you regenerate package-lock and run tabbable's unit tests, you get the error SyntaxError: 'slot):not([inert]' is not a valid selector
I've narrowed this down to the update of one of jsdom's dependencies: [email protected] to [email protected]
This breaks unit tests for any consumers of tabbable who install latest and use jsdom.

I'm not sure if the fix is re-writing the [tabindex]:not(slot):not([inert]) selector, or if this is a bug that nwsapi needs to fix. Please advise.

Can't make it work testing with tabbable mocked

Hi guys, sorry for leaving this here but I can't find any solution. I'm trying to mock tabbable to use with jest and I followed the instructions that you leaved but I'm getting an error "TypeError: tabbable.tabbable is not a function" when test runs.

My root for jest is defined in '/packages/core', so that is why I placed mock folder there. Any ideas where could be the issue?
image

Remove nwsapi v2.2.2 override once bug is fixed

Blocked by dperini/nwsapi#83

Relates to #982

Would revert #995

Might want to make sure package-lock.json is also now using the nwsapi version that has the fix instead of being still configured for v2.2.2 (which is why #982 isn't visible unless you regenerate the lock file and NPM picks the current-latest problematic version of nwsapi).

Incorrect README import instructions

In the README it states to import this module you need to use a named import instead of a default import.

import { tabbable } from 'tabbable';

tabbable is undefined.

You my want to update it to reflect the need for a default import.

Firefox throws an error when checking disabled on non button element

Hi,

I am not sure if this is related to a recent change of Firefox, but today I got this strange error:

image

After checking this relates to this line: https://github.com/focus-trap/tabbable/blob/master/src/index.js#L405

Apparently, Firefox seems to throw an exception whenever you access the disabled attribute on an element that does not implement it.

Chrome does not experience any issue, but this should probably be changed, by checking the attribute instead.

Thanks :)

v5.3.0 breaks literally all of our usage of `tabbable` 😅 when called on a node not attached to the document

The change to use getClientRects().length completely breaks this library for us: #604

We use tabbable when initializing dialogs, to find candidate element(s) to auto-focus when the dialog appears. These dialogs are React components and the call to tabbable() occurs during the componentDidMount lifecycle method. At that moment, the DOM elements we want to traverse using tabbable are fully formed, but they are not yet attached to the document. So getClientRects().length always returns 0, and thus every single one of our calls to tabbable fails. (At this library level, we have no way to control when the dialog is actually attached to the document.)

Could this change be reverted, or can we scope it to only be used when document.body.contains(node)?

Restore actual browser testing with Cypress

We removed Karma in #226 but that removed browser-based testing. tabbable, focus-trap, and focus-trap-react now all use the @testing-library and focus-trap/react use Cypress. Let's add Cypress tests to tabbable.

getComputedStyle fails with shadow dom

I'm trying to use focus-trap in a StencilJS component and the various checks that rely on getComputedStyle fail when it hits a shadow root.

Pretty sure this could be fixed with a simple check, I'll take a crack at a PR. Thanks!

7vfqzmxv.entry.js:formatted:2 Uncaught TypeError: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.
    at p.hasDisplayNone (7vfqzmxv.entry.js:formatted:2)
    at p.hasDisplayNone (7vfqzmxv.entry.js:formatted:2)
    at p.hasDisplayNone (7vfqzmxv.entry.js:formatted:2)
    at p.hasDisplayNone (7vfqzmxv.entry.js:formatted:2)
    at p.isUntouchable (7vfqzmxv.entry.js:formatted:2)
    at l (7vfqzmxv.entry.js:formatted:2)
    at s (7vfqzmxv.entry.js:formatted:2)
    at i (7vfqzmxv.entry.js:formatted:2)
    at w (7vfqzmxv.entry.js:formatted:2)
    at Object.activate (7vfqzmxv.entry.js:formatted:2)

Option to check a container too

Right now only nodes within a container get tested, but not a container itself. I have use case when i want a container to be possibly included in resulted array.

In current state i may get what i need, by retrieving tabbables of a parent of a container and then filtering results by inclusion of container. My concern is that of performance. Siblings of a container can be big, while container itself may be small or empty, it may result in much of unnecessary work.

Another way is to add option which will tell to prepend a container to candidates list if it matches one of selectors.

What are your opinion on adding such option? It is ok or is it out of scope of this library?

IE 9+ Support

Hi David,
just a brief remark: According to your README file you are supporting IE9+. I noticed that since version 1.0.3 you are now using the Array.prototype.find Method in [1]. This will break in IE <= 11.

[1] index.js#L53

Update all dependencies

Dependencies haven't been updated in a while, and focus-trap, which depends on this module, uses Webpack now after focus-trap/focus-trap#124, so I'm also going to change the build system to Webpack.

I hope to address #51 while I'm at it.

Also along with focus-trap/focus-trap#124, and along with focus-trap/focus-trap-react#78, I'll be updating the ESLint and Prettier configuration to match. This way, all 3 repos now number the https://github.com/focus-trap "roof" will use the same stack and the same linting/formatting rules.

The project will move to Yarn since the other 2 repos use Yarn also.

Finally, I'll switch the CI to a GitHub Workflow, just like the other 2 repos are using now.

Make this package tree-shakeable

I'm building a UI library which exports a bunch of stuff, only some of my components rely on this package so it would be desirable that if a consumer of my package doesn't use component relying on tabbable it would be tree shaken out of the final bundle. However it's not the case right now.

Let's focus on 2 common tree-shaking deopts:

  • no ESM bundle (this package) exports only CJS format
  • top-level static properties, as in module.exports = tabbable; tabbable.isTabbable = isTabbable;

Action items:

  • migrate source to ESM + build CJS format from it, point to CJS from package.json#main and to ESM from package.json#module so bundlers can pick appropriate file
  • change exports shape to export default tabbable; export { isTabbable, isFocusable };

Intent to implement - yes (was already implemented in #38 )

support inert attribute

Elements that are marked with inert or are nested in another inert element shouldn't be tabbable/focusable. Since this is just supported behind browsers flags at the moment, maybe tabbable can add support with a default false flag for now.

version 5.1.1 contains an arrow function

I am trying to use the library in internet explorer 11

in the transpiled version there is an arrow function
I can see it coming from line 49

let tabbableNodes = orderedTabbables
    .sort(sortOrderedTabbables)
    .map((a) => a.node)
    .concat(regularTabbables);

https://github.com/focus-trap/tabbable/blob/master/src/index.js#L49

We have two options, we can use a traditional function expression in the index.js, or we can configure the babel settings to have a target of internet explorer 9 to match the version support in the docs

Provide TS typings

While it's possible to type callable signature with static properties in TS it would be better to change exports shape to default export + 2 named exports (which I mention also in #39 ).

It would also be better to use mixed exports, even if it means having to use require('tabbable').default by CJS consumers - because then TS typings would be accurate. Faking default with module.exports = _default is a can of worms and might cause issues in webpack.

couple question

First of all, thank you.

A couple questions; Is it possible to add <summary>/<details> ? What about hidden html attribute ?

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.