Skip to content

Commit

Permalink
fix: Clicking on open shadowDOM components within a focus trap's cont…
Browse files Browse the repository at this point in the history
…ainer when `clickOutsideDeactivates=true` should not deactivate the focus trap (#960)

* custom button bug.

* span

* update demo

* review fixes

* changeset

* Update small-kangaroos-burn.md

* Update small-kangaroos-burn.md

* cleanup

* review fixes

* jsdoc

* review fixes

* review fixes

Check if composedPath includes container only so that iframe elements don't trigger false positives. Use optional chaining operator where applicable

* cleanup

* review fix

* update demo bundle.

* lint

* fix demo html

* review fixes

* Revert "review fixes"

This reverts commit cef4ba3.

* cleanup

* add comment
  • Loading branch information
driskull committed May 17, 2023
1 parent 8a80a0a commit db62ce3
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-kangaroos-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'focus-trap': patch
---

Clicking on open shadowDOM components within a focus trap's container when `clickOutsideDeactivates=true` should not deactivate the focus trap. #959
91 changes: 63 additions & 28 deletions docs/demo-bundle.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/demo-bundle.js.map

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions docs/js/in-open-shadow-dom.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
const { createFocusTrap } = require('../../index');
module.exports = () => {
class CustomButton extends HTMLElement {
constructor() {
super();

this.attachShadow({ mode: 'open' }).innerHTML =
'<button id="button-inside-custom-button"><slot></slot></button>';
}
}

class CustomSpan extends HTMLElement {
constructor() {
super();

this.attachShadow({ mode: 'open' }).innerHTML =
'<span id="span-inside-custom-span"><slot></slot></span>';
}
}

class FocusTrapModal extends HTMLElement {
constructor() {
super();
Expand All @@ -14,6 +32,9 @@ module.exports = () => {
<a href="#">with</a> <a href="#">some</a> <a href="#">focusable</a> parts.
</p>
<p>
<custom-button>Shadow Button</custom-button>
<button>Light DOM Button</button>
<custom-span>Shadow Span</custom-span>
<button id="deactivate-in-open-shadow-dom" aria-describedby="in-open-shadow-dom-heading">
deactivate trap
</button>
Expand All @@ -32,7 +53,11 @@ module.exports = () => {
const focusTrap = createFocusTrap(modalEl, {
onActivate: () => modalEl.classList.add('is-active'),
onDeactivate: () => modalEl.classList.remove('is-active'),
clickOutsideDeactivates: false, // set to true to verify clicking on shadowDOM components within a focus trap's container should not deactivate the focus trap.
escapeDeactivates: true,
tabbableOptions: {
getShadowRoot: true,
},
});

document
Expand All @@ -45,4 +70,6 @@ module.exports = () => {
}

customElements.define('focus-trap-modal', FocusTrapModal);
customElements.define('custom-button', CustomButton);
customElements.define('custom-span', CustomSpan);
};
2 changes: 2 additions & 0 deletions docs/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ require('./multiple-elements')();
require('./multiple-elements-delete')();
require('./multiple-elements-delete-all')();
require('./multiple-elements-multiple-traps')();

// TEST MANUALLY (Cypress doesn't support Shadow DOM well)
require('./in-open-shadow-dom')();

// TEST MANUALLY (Cypress doesn't support Shadow DOM well)
Expand Down
16 changes: 11 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,16 @@ const createFocusTrap = function (elements, userOptions) {
/**
* Finds the index of the container that contains the element.
* @param {HTMLElement} element
* @param {Event} [event]
* @returns {number} Index of the container in either `state.containers` or
* `state.containerGroups` (the order/length of these lists are the same); -1
* if the element isn't found.
*/
const findContainerIndex = function (element) {
const findContainerIndex = function (element, event) {
const composedPath =
typeof event?.composedPath === 'function'
? event.composedPath()
: undefined;
// NOTE: search `containerGroups` because it's possible a group contains no tabbable
// nodes, but still contains focusable nodes (e.g. if they all have `tabindex=-1`)
// and we still need to find the element in there
Expand All @@ -193,6 +198,7 @@ const createFocusTrap = function (elements, userOptions) {
// web components if the `tabbableOptions.getShadowRoot` option was used for
// the trap, enabling shadow DOM support in tabbable (`Node.contains()` doesn't
// look inside web components even if open)
composedPath?.includes(container) ||
tabbableNodes.find((node) => node === element)
);
};
Expand Down Expand Up @@ -380,7 +386,7 @@ const createFocusTrap = function (elements, userOptions) {
const checkPointerDown = function (e) {
const target = getActualTarget(e);

if (findContainerIndex(target) >= 0) {
if (findContainerIndex(target, e) >= 0) {
// allow the click since it ocurred inside the trap
return;
}
Expand Down Expand Up @@ -414,7 +420,7 @@ const createFocusTrap = function (elements, userOptions) {
// In case focus escapes the trap for some strange reason, pull it back in.
const checkFocusIn = function (e) {
const target = getActualTarget(e);
const targetContained = findContainerIndex(target) >= 0;
const targetContained = findContainerIndex(target, e) >= 0;

// In Firefox when you Tab out of an iframe the Document is briefly focused.
if (targetContained || target instanceof Document) {
Expand Down Expand Up @@ -442,7 +448,7 @@ const createFocusTrap = function (elements, userOptions) {
// make sure the target is actually contained in a group
// NOTE: the target may also be the container itself if it's focusable
// with tabIndex='-1' and was given initial focus
const containerIndex = findContainerIndex(target);
const containerIndex = findContainerIndex(target, event);
const containerGroup =
containerIndex >= 0 ? state.containerGroups[containerIndex] : undefined;

Expand Down Expand Up @@ -578,7 +584,7 @@ const createFocusTrap = function (elements, userOptions) {
const checkClick = function (e) {
const target = getActualTarget(e);

if (findContainerIndex(target) >= 0) {
if (findContainerIndex(target, e) >= 0) {
return;
}

Expand Down

0 comments on commit db62ce3

Please sign in to comment.