Skip to content

Commit

Permalink
Add support for closed shadow DOMs (#610)
Browse files Browse the repository at this point in the history
* Add support for closed shadow DOMs

Fixes #601 (I hope)

This change exposes Tabbable's new `getShadowRoot()` configuration
option, which makes it possible for Tabbable to walk even closed
shadows by giving Tabbable a reference to the closed `ShadowRoot`
for a given element.

* 6.8.0-beta.0

* Investigation and pre-fix prep for #643

See `// DEBUG` comments.

* [#643] Make sure tab key doesn't get stuck before focusing inside web component

Fixes #643

See additional changes in CHANGELOG entry.

* Prepare for v6.8.0-beta.2

* 6.8.0-beta.2

* Updated demo bundle

* Add changeset

* Bump tabbable to v5.3.0
  • Loading branch information
stefcameron committed Apr 20, 2022
1 parent d392ff2 commit 21458c9
Show file tree
Hide file tree
Showing 13 changed files with 732 additions and 256 deletions.
5 changes: 5 additions & 0 deletions .changeset/wet-elephants-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'focus-trap': minor
---

Bumps tabbable to v5.3.0 and includes all changes from the past v6.8.0 beta releases. The big new feature is opt-in Shadow DOM support in tabbable, and a new `getShadowRoot` tabbable option exposed in a new `tabbableOptions` focus-trap config option.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## 6.8.0-beta.2

- When updating tabbable nodes, make sure that `getShadowRoot` tabbable option is also passed to `focusable()`.
- Fix bug where having a tabbable node inside a web component in the middle of a tab sequence would cause the tab key to seemingly stop working just before focus should move to it ((#643)[https://github.com/focus-trap/focus-trap/issues/643]).
- Bumps tabbable to `v5.3.0-beta.1`

## 6.8.0-beta.1

- Previous beta didn't include new source. This one does.

## 6.8.0-beta.0

- Adds new `tabbableOptions` configuration option, which allows specifically for the new `getShadowRoot` Tabbable configuration option: `focusTrap.createFocusTrap(rootElement, { tabbableOptions: { getShadowRoot: (node) => closedShadowRoot } })`, for example (where your code has the reference to `closedShadowRoot` previously created on `node` which Tabbable cannot find on its own).
- Bumps tabbable to `v5.3.0-beta.0`

## 6.7.3

### Patch Changes
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,22 @@ Returns a new focus trap on `element` (one or more "containers" of tabbable node
- **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 allows to use FocusTrap in an iFrame context.
- **tabbableOptions**: (optional) Specific [tabbable](https://github.com/focus-trap/tabbable) options that are configurable on FocusTrap.
- **tabbableOptions.getShadowRoot**: See [getShadowRoot](https://github.com/focus-trap/tabbable#getshadowroot) on Tabbable for more details.

#### Shadow DOM and selector strings
#### Shadow DOM

##### Selector strings

⚠️ Beware that putting a focus-trap **inside** an open Shadow DOM means you must either:

- **Not use selector strings** for options that support these (because nodes inside Shadow DOMs, even open shadows, are not visible via `document.querySelector()`); OR
- You must **use the `document` option** to configure the focus trap to use your *shadow host* element as its document. The downside of this option is that, while selector queries on nodes inside your trap will now work, the trap will not prevent focus from being set on nodes outside your Shadow DOM, which is the same drawback as putting a focus trap <a href="https://focus-trap.github.io/focus-trap/#demo-in-iframe">inside an iframe</a>.

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

### trap.activate([activateOptions])

Activates the focus trap, adding various event listeners to the document.
Expand Down
86 changes: 86 additions & 0 deletions cypress/integration/focus-trap-demo.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1538,4 +1538,90 @@ describe('focus-trap', () => {
// focus to an element with negative tabindex and tabbing away from it in one direction
// or another.
// });

// NOTE: Unfortunately, the https://github.com/Bkucera/cypress-plugin-tab plugin doesn't
// support web components, so we can't successfully run this test because it will skip
// over the web component when tabbing from 'button 3', jumping to 'button 4' instead of
// the expectation of going to the 'open-web-component' button. Remember that
// cypress-plugin-tab using some other internal tabbable-like library to determine what
// the tabbable nodes are and moves focus that way.
// describe('demo: with-open-web-component', () => {
// it.only('traps focus tab sequence and allows deactivation by clicking deactivate button', () => {
// cy.get('#demo-with-open-web-component').as('testRoot');
//
// // activate trap
// cy.get('@testRoot')
// .findByRole('button', { name: /^activate trap/ })
// .as('lastlyFocusedElementBeforeTrapIsActivated')
// .click();
//
// // 1st element should be focused
// cy.get('@testRoot')
// .findByRole('button', { name: 'button 1' })
// .as('firstElementInTrap')
// .should('be.focused');
//
// // trap is active (keep focus in trap by blocking clicks on outside focusable element)
// cy.get('#return-to-repo').click();
// cy.get('@firstElementInTrap').should('be.focused');
//
// // trap is active (keep focus in trap by blocking clicks on outside un-focusable element)
// cy.get('#with-open-web-component-heading').click();
// cy.get('@firstElementInTrap').should('be.focused');
//
// // trap is active (keep focus in trap by tabbing through the focus trap's tabbable elements)
// cy.get('@firstElementInTrap')
// .tab()
// .should('have.text', 'button 2')
// .should('be.focused')
// .tab()
// .should('have.text', 'button 3')
// .should('be.focused')
// .tab()
// .should('have.text', 'open-web-component')
// .should('be.focused')
// .tab()
// .should('have.text', 'button 4')
// .should('be.focused')
// .tab()
// .should('have.text', 'button 5')
// .should('be.focused')
// .tab()
// .as('lastElementInTrap')
// .should('contain', 'deactivate trap')
// .should('be.focused')
// .tab();
//
// // trap is active (keep focus in trap by shift-tabbing through the focus trap's tabbable elements)
// cy.get('@firstElementInTrap').should('be.focused').tab({ shift: true });
// cy.get('@lastElementInTrap')
// .should('be.focused')
// .tab({ shift: true })
// .should('have.text', 'button 5')
// .should('be.focused')
// .tab({ shift: true })
// .should('have.text', 'button 4')
// .should('be.focused')
// .tab({ shift: true })
// .should('have.text', 'open-web-component')
// .should('be.focused')
// .tab({ shift: true })
// .should('have.text', 'button 3')
// .should('be.focused')
// .tab({ shift: true })
// .should('have.text', 'button 2')
// .should('be.focused')
// .tab({ shift: true });
//
// cy.get('@firstElementInTrap').should('be.focused');
//
// // focus can be transitioned freely when trap is deactivated
// cy.get('@testRoot')
// .findByRole('button', { name: /^deactivate trap/ })
// .click();
// verifyFocusIsNotTrapped(
// cy.get('@lastlyFocusedElementBeforeTrapIsActivated')
// );
// });
// });
});
Loading

0 comments on commit 21458c9

Please sign in to comment.