From 47f62ac22fdebd8e39c00eed690e8661359c93c6 Mon Sep 17 00:00:00 2001 From: Stefan Cameron Date: Wed, 17 Aug 2022 16:24:41 -0500 Subject: [PATCH] Update tabbable to v6.0.0 (#770) --- .changeset/cyan-baboons-smile.md | 5 ++ .github/pull_request_template.md | 2 +- docs/demo-bundle.js | 82 +++++++++++++++++++++----------- docs/demo-bundle.js.map | 2 +- docs/js/index.js | 3 +- package.json | 2 +- yarn.lock | 8 ++-- 7 files changed, 68 insertions(+), 36 deletions(-) create mode 100644 .changeset/cyan-baboons-smile.md diff --git a/.changeset/cyan-baboons-smile.md b/.changeset/cyan-baboons-smile.md new file mode 100644 index 00000000..918f4ca2 --- /dev/null +++ b/.changeset/cyan-baboons-smile.md @@ -0,0 +1,5 @@ +--- +'focus-trap': major +--- + +🚨 **Breaking:** Tabbable dependency has been updated to v6.0.0 and contains a breaking change related to detached nodes with its default `displayCheck` setting. See tabbable's [changelog](https://github.com/focus-trap/tabbable/blob/master/CHANGELOG.md#600) for more information. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 97a3cf73..fbadb7f7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -17,7 +17,7 @@ __Please leave this checklist in your PR.__ - Issue being fixed is referenced. - Unit test coverage added/updated. - E2E (i.e. demos) test coverage added/updated. - - ⚠️ Non-covered demos (look for `IS_CYPRESS_ENV === ''` [here](https://github.com/focus-trap/focus-trap/blob/master/docs/js/index.js), as well as `in-open-shadow-dom.js` and `negative-tabindex.js` that can't be fully tested in Cypress) __manually__ verified. + - ⚠️ Non-covered demos (look for `// TEST MANUALLY` comments [here](https://github.com/focus-trap/focus-trap/blob/master/docs/js/index.js)) that can't be fully tested in Cypress have been __manually__ verified. - Typings added/updated. - Changes do not break SSR: - Careful to test `typeof document/window !== 'undefined'` before using it in code that gets executed on load. diff --git a/docs/demo-bundle.js b/docs/demo-bundle.js index 2141e7e6..ad18e418 100644 --- a/docs/demo-bundle.js +++ b/docs/demo-bundle.js @@ -6,7 +6,7 @@ var focusTrapDemoBundle = (function () { 'use strict'; (function() { - const env = {"BUILD_ENV":"demo","IS_CYPRESS_ENV":"chrome"}; + const env = {"BUILD_ENV":"demo"}; try { if (process) { process.env = Object.assign({}, process.env); @@ -636,7 +636,7 @@ var focusTrapDemoBundle = (function () { } /*! - * tabbable 5.3.3 + * tabbable 6.0.0 * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE */ @@ -847,6 +847,45 @@ var focusTrapDemoBundle = (function () { var isNonTabbableRadio = function isNonTabbableRadio(node) { return isRadio(node) && !isTabbableRadio(node); + }; // determines if a node is ultimately attached to the window's document + + + var isNodeAttached = function isNodeAttached(node) { + var _nodeRootHost; // The root node is the shadow root if the node is in a shadow DOM; some document otherwise + // (but NOT _the_ document; see second 'If' comment below for more). + // If rootNode is shadow root, it'll have a host, which is the element to which the shadow + // is attached, and the one we need to check if it's in the document or not (because the + // shadow, and all nodes it contains, is never considered in the document since shadows + // behave like self-contained DOMs; but if the shadow's HOST, which is part of the document, + // is hidden, or is not in the document itself but is detached, it will affect the shadow's + // visibility, including all the nodes it contains). The host could be any normal node, + // or a custom element (i.e. web component). Either way, that's the one that is considered + // part of the document, not the shadow root, nor any of its children (i.e. the node being + // tested). + // To further complicate things, we have to look all the way up until we find a shadow HOST + // that is attached (or find none) because the node might be in nested shadows... + // If rootNode is not a shadow root, it won't have a host, and so rootNode should be the + // document (per the docs) and while it's a Document-type object, that document does not + // appear to be the same as the node's `ownerDocument` for some reason, so it's safer + // to ignore the rootNode at this point, and use `node.ownerDocument`. Otherwise, + // using `rootNode.contains(node)` will _always_ be true we'll get false-positives when + // node is actually detached. + + + var nodeRootHost = getRootNode(node).host; + var attached = !!((_nodeRootHost = nodeRootHost) !== null && _nodeRootHost !== void 0 && _nodeRootHost.ownerDocument.contains(nodeRootHost) || node.ownerDocument.contains(node)); + + while (!attached && nodeRootHost) { + var _nodeRootHost2; // since it's not attached and we have a root host, the node MUST be in a nested shadow DOM, + // which means we need to get the host's host and check if that parent host is contained + // in (i.e. attached to) the document + + + nodeRootHost = getRootNode(nodeRootHost).host; + attached = !!((_nodeRootHost2 = nodeRootHost) !== null && _nodeRootHost2 !== void 0 && _nodeRootHost2.ownerDocument.contains(nodeRootHost)); + } + + return attached; }; var isZeroArea = function isZeroArea(node) { @@ -874,29 +913,9 @@ var focusTrapDemoBundle = (function () { if (matches.call(nodeUnderDetails, 'details:not([open]) *')) { return true; - } // The root node is the shadow root if the node is in a shadow DOM; some document otherwise - // (but NOT _the_ document; see second 'If' comment below for more). - // If rootNode is shadow root, it'll have a host, which is the element to which the shadow - // is attached, and the one we need to check if it's in the document or not (because the - // shadow, and all nodes it contains, is never considered in the document since shadows - // behave like self-contained DOMs; but if the shadow's HOST, which is part of the document, - // is hidden, or is not in the document itself but is detached, it will affect the shadow's - // visibility, including all the nodes it contains). The host could be any normal node, - // or a custom element (i.e. web component). Either way, that's the one that is considered - // part of the document, not the shadow root, nor any of its children (i.e. the node being - // tested). - // If rootNode is not a shadow root, it won't have a host, and so rootNode should be the - // document (per the docs) and while it's a Document-type object, that document does not - // appear to be the same as the node's `ownerDocument` for some reason, so it's safer - // to ignore the rootNode at this point, and use `node.ownerDocument`. Otherwise, - // using `rootNode.contains(node)` will _always_ be true we'll get false-positives when - // node is actually detached. - - - var nodeRootHost = getRootNode(node).host; - var nodeIsAttached = (nodeRootHost === null || nodeRootHost === void 0 ? void 0 : nodeRootHost.ownerDocument.contains(nodeRootHost)) || node.ownerDocument.contains(node); + } - if (!displayCheck || displayCheck === 'full') { + if (!displayCheck || displayCheck === 'full' || displayCheck === 'legacy-full') { if (typeof getShadowRoot === 'function') { // figure out if we should consider the node to be in an undisclosed shadow and use the // 'non-zero-area' fallback @@ -934,7 +953,7 @@ var focusTrapDemoBundle = (function () { // `isTabbable()` or `isFocusable()` -- regardless of `getShadowRoot` option setting. - if (nodeIsAttached) { + if (isNodeAttached(node)) { // this works wherever the node is: if there's at least one client rect, it's // somehow displayed; it also covers the CSS 'display: contents' case where the // node itself is hidden in place of its contents; and there's no need to search @@ -953,6 +972,14 @@ var focusTrapDemoBundle = (function () { // APIs on nodes in detached containers has actually implicitly used tabbable in what // was later (as of v5.2.0 on Apr 9, 2021) called `displayCheck="none"` mode -- essentially // considering __everything__ to be visible because of the innability to determine styles. + // + // v6.0.0: As of this major release, the default 'full' option __no longer treats detached + // nodes as visible with the 'none' fallback.__ + + + if (displayCheck !== 'legacy-full') { + return true; // hidden + } // else, fallback to 'none' mode and consider the node visible } else if (displayCheck === 'non-zero-area') { // NOTE: Even though this tests that the node's client rect is non-zero to determine @@ -961,7 +988,8 @@ var focusTrapDemoBundle = (function () { // this mode, we do want to consider nodes that have a zero area to be hidden at all // times, and that includes attached or not. return isZeroArea(node); - } // visible, as far as we can tell, or per current `displayCheck` mode + } // visible, as far as we can tell, or per current `displayCheck=none` mode, we assume + // it's visible return false; @@ -3643,7 +3671,7 @@ var focusTrapDemoBundle = (function () { // eslint-disable-next-line no-undef -- process is defined via Rollup if (!process.env.IS_CYPRESS_ENV) { - requireInIframe()(); + requireInIframe()(); // TEST MANUALLY (causes Cypress to fail due to security context violations) } allowOutsideClick(); diff --git a/docs/demo-bundle.js.map b/docs/demo-bundle.js.map index 7496e11d..0b542a74 100644 --- a/docs/demo-bundle.js.map +++ b/docs/demo-bundle.js.map @@ -1 +1 @@ -{"version":3,"file":"demo-bundle.js","sources":["../node_modules/tabbable/dist/index.esm.js","../index.js","js/default.js","js/animated-dialog.js","js/animated-trigger.js","js/escape-deactivates.js","js/initial-element-no-escape.js","js/initially-focused-container.js","js/hidden-treasures.js","js/nested.js","js/sibling.js","js/tricky-initial-focus.js","js/input-activation.js","js/delay.js","js/radio.js","js/iframe.js","../node_modules/regenerator-runtime/runtime.js","js/in-iframe.js","js/allow-outside-click.js","js/click-outside-deactivates.js","js/set-return-focus.js","js/set-return-focus-function.js","js/no-delay.js","js/multiple-elements.js","js/multiple-elements-delete.js","js/multiple-elements-delete-all.js","js/multiple-elements-multiple-traps.js","js/in-open-shadow-dom.js","js/with-shadow-dom.js","js/negative-tabindex.js","js/negative-tabindex-last.js","js/with-open-web-component.js","js/index.js"],"sourcesContent":["/*!\n* tabbable 5.3.3\n* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE\n*/\nvar candidateSelectors = ['input', 'select', 'textarea', 'a[href]', 'button', '[tabindex]:not(slot)', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable=\"false\"])', 'details>summary:first-of-type', 'details'];\nvar candidateSelector = /* #__PURE__ */candidateSelectors.join(',');\nvar NoElement = typeof Element === 'undefined';\nvar matches = NoElement ? function () {} : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;\nvar getRootNode = !NoElement && Element.prototype.getRootNode ? function (element) {\n return element.getRootNode();\n} : function (element) {\n return element.ownerDocument;\n};\n/**\n * @param {Element} el container to check in\n * @param {boolean} includeContainer add container to check\n * @param {(node: Element) => boolean} filter filter candidates\n * @returns {Element[]}\n */\n\nvar getCandidates = function getCandidates(el, includeContainer, filter) {\n var candidates = Array.prototype.slice.apply(el.querySelectorAll(candidateSelector));\n\n if (includeContainer && matches.call(el, candidateSelector)) {\n candidates.unshift(el);\n }\n\n candidates = candidates.filter(filter);\n return candidates;\n};\n/**\n * @callback GetShadowRoot\n * @param {Element} element to check for shadow root\n * @returns {ShadowRoot|boolean} ShadowRoot if available or boolean indicating if a shadowRoot is attached but not available.\n */\n\n/**\n * @callback ShadowRootFilter\n * @param {Element} shadowHostNode the element which contains shadow content\n * @returns {boolean} true if a shadow root could potentially contain valid candidates.\n */\n\n/**\n * @typedef {Object} CandidatesScope\n * @property {Element} scope contains inner candidates\n * @property {Element[]} candidates\n */\n\n/**\n * @typedef {Object} IterativeOptions\n * @property {GetShadowRoot|boolean} getShadowRoot true if shadow support is enabled; falsy if not;\n * if a function, implies shadow support is enabled and either returns the shadow root of an element\n * or a boolean stating if it has an undisclosed shadow root\n * @property {(node: Element) => boolean} filter filter candidates\n * @property {boolean} flatten if true then result will flatten any CandidatesScope into the returned list\n * @property {ShadowRootFilter} shadowRootFilter filter shadow roots;\n */\n\n/**\n * @param {Element[]} elements list of element containers to match candidates from\n * @param {boolean} includeContainer add container list to check\n * @param {IterativeOptions} options\n * @returns {Array.}\n */\n\n\nvar getCandidatesIteratively = function getCandidatesIteratively(elements, includeContainer, options) {\n var candidates = [];\n var elementsToCheck = Array.from(elements);\n\n while (elementsToCheck.length) {\n var element = elementsToCheck.shift();\n\n if (element.tagName === 'SLOT') {\n // add shadow dom slot scope (slot itself cannot be focusable)\n var assigned = element.assignedElements();\n var content = assigned.length ? assigned : element.children;\n var nestedCandidates = getCandidatesIteratively(content, true, options);\n\n if (options.flatten) {\n candidates.push.apply(candidates, nestedCandidates);\n } else {\n candidates.push({\n scope: element,\n candidates: nestedCandidates\n });\n }\n } else {\n // check candidate element\n var validCandidate = matches.call(element, candidateSelector);\n\n if (validCandidate && options.filter(element) && (includeContainer || !elements.includes(element))) {\n candidates.push(element);\n } // iterate over shadow content if possible\n\n\n var shadowRoot = element.shadowRoot || // check for an undisclosed shadow\n typeof options.getShadowRoot === 'function' && options.getShadowRoot(element);\n var validShadowRoot = !options.shadowRootFilter || options.shadowRootFilter(element);\n\n if (shadowRoot && validShadowRoot) {\n // add shadow dom scope IIF a shadow root node was given; otherwise, an undisclosed\n // shadow exists, so look at light dom children as fallback BUT create a scope for any\n // child candidates found because they're likely slotted elements (elements that are\n // children of the web component element (which has the shadow), in the light dom, but\n // slotted somewhere _inside_ the undisclosed shadow) -- the scope is created below,\n // _after_ we return from this recursive call\n var _nestedCandidates = getCandidatesIteratively(shadowRoot === true ? element.children : shadowRoot.children, true, options);\n\n if (options.flatten) {\n candidates.push.apply(candidates, _nestedCandidates);\n } else {\n candidates.push({\n scope: element,\n candidates: _nestedCandidates\n });\n }\n } else {\n // there's not shadow so just dig into the element's (light dom) children\n // __without__ giving the element special scope treatment\n elementsToCheck.unshift.apply(elementsToCheck, element.children);\n }\n }\n }\n\n return candidates;\n};\n\nvar getTabindex = function getTabindex(node, isScope) {\n if (node.tabIndex < 0) {\n // in Chrome,
,