Conversation
…hadow root `ariaHideOutside` skips any node that contains one of its targets and hides the rest. It tested that with `Node.contains`, which does not cross shadow boundaries: for a layer portalled into a shadow root, `body.contains(layer)` is false. The walk therefore accepted `root` itself and set `aria-hidden` on `<body>`, hiding the open layer along with the page. Chromium ignores `aria-hidden` on `<body>`, which masks it by hand; accessibility-tree tooling such as Playwright's `getByRole` does not. Use `containsComposed` (added for createInteractOutside in kobaltedev#722) for both containment checks: the tree walk, where it fixes the bug, and the MutationObserver's, for consistency. With it, the shadow host's ancestors are skipped and the page beside them is hidden, as intended. The walk still does not descend into shadow roots, so siblings of a target inside the same shadow root are left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
👷 Deploy request for kobalte pending review.Visit the deploys page to approve it
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Follow-up to #722, same shadow-boundary blind spot in a different primitive.
ariaHideOutsidewalks the page fromroot(document.bodyby default). It skips any node that contains one of its targets and hides the rest. The containment test wasNode.contains, which does not cross shadow boundaries, so for a layer portalled into a shadow root:body.contains(layer)isfalse. The walk acceptsrootitself and setsaria-hidden="true"on<body>, which hides the open layer along with everything else.This affects every modal layer that uses
createHideOutside(menus and dialogs by default; popover, select and combobox whenmodal) whosePortalmounts into a shadow root.In practice, browsers ignore
aria-hiddenon<body>, so screen-reader users aren't locked out. What they lose is the modal isolation: the page behind the layer is never hidden. I checked this in Firefox 155 by reading its accessibility tree over AT-SPI, which is what Orca consumes. Every element stays exposed witharia-hiddenon<body>, whether it's set statically or after load, whilearia-hiddenon an ordinary wrapper element prunes its subtree as expected. Chromium's tree (via CDP) shows the same. Safari/VoiceOver isn't tested.Tooling that implements the attribute literally does break. Playwright's
getByRolefinds nothing inside the open menu.Fix
Use
containsComposed(added in #722 forcreateInteractOutside) for both containment checks inariaHideOutside:MutationObserver's check, for consistency. The observer never sees mutations inside shadow roots, so in the layouts tested this is behaviour-neutral.For targets that aren't in a shadow root,
containsComposedgives the same answer ascontains, so this changes nothing outside shadow DOM.I imported the helper from
create-interact-outside/utilsrather than moving it, to keep the diff small. Happy to move it somewhere shared if you prefer.Tests
Two cases added to
aria-hide-outside.test.tsx, in aShadow DOMblock:bodyand the host stay visible, the siblingmainis hidden, revert restores itbodyis hidden)bodyafter the call is still hidden,bodyisn'tbodyis hidden)The 11 existing
ariaHideOutsidetests are unchanged and pass.Known limitation (unchanged by this PR)
The walk doesn't descend into shadow roots, which leaves two gaps. Neither is made worse by this change:
Portalwrapper div would be hidden.Handling those properly means recursing into open shadow roots and observing each of them, which felt like more than a bug fix should take on.
Verification
create-hide-outsidesuite: 13/13 pass; both new tests fail without the fix.biome checkclean,pnpm -F @kobalte/core buildsucceeds.@kobalte/coresuite shows the same intermittenttoggle-group/toast/tabsfailures on unmodifiedmainas with this change; the only difference is +2 passing tests.<body>stays visible and the page wrapper is hidden. That app currently showsaria-hiddenon<body>in Chromium on 0.13.14.The same pattern exists in
@solid-primitives/interaction'sariaHideOutside, which Kobalte'ssolid2branch uses. I mentioned it in solidjs-community/solid-primitives#1056 and can port this there once that PR lands.🤖 Generated with Claude Code