-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat: traverse open shadow DOM roots in accessibility tree #183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,9 @@ | ||
| import { isElement } from "./isElement"; | ||
|
|
||
| /** | ||
| * Shadow-aware ID lookup: searches the container and all descendant | ||
| * shadow roots for an element with the given ID. | ||
| */ | ||
| export function getNodeByIdRef({ | ||
| container, | ||
| idRef, | ||
|
|
@@ -11,5 +15,48 @@ export function getNodeByIdRef({ | |
| return null; | ||
| } | ||
|
|
||
| return container.querySelector(`#${CSS.escape(idRef)}`); | ||
| const selector = `#${CSS.escape(idRef)}`; | ||
|
|
||
| // Try light DOM first | ||
| const result = container.querySelector(selector); | ||
|
|
||
| if (result) { | ||
| return result; | ||
| } | ||
|
|
||
| // Search inside shadow roots | ||
| return findInShadowRoots(container, selector); | ||
| } | ||
|
|
||
| function findInShadowRoots( | ||
| root: Element, | ||
| selector: string | ||
| ): Element | null { | ||
| if (root.shadowRoot) { | ||
| const found = root.shadowRoot.querySelector(selector); | ||
|
|
||
| if (found) { | ||
| return found; | ||
| } | ||
|
|
||
| for (const child of root.shadowRoot.querySelectorAll("*")) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to previous comment here - a dfs walk would be more performant as would avoid the full sub tree scans, which suits given the early exit behaviour, so we should hopefully be able to achieve faster than O(n) on average |
||
| const result = findInShadowRoots(child, selector); | ||
|
|
||
| if (result) { | ||
| return result; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for (const child of root.querySelectorAll("*")) { | ||
| if (child.shadowRoot) { | ||
| const result = findInShadowRoots(child, selector); | ||
|
|
||
| if (result) { | ||
| return result; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,39 @@ | ||
| import { isElement } from "./isElement"; | ||
| import type { Root } from "./Virtual"; | ||
|
|
||
| const OBSERVE_OPTIONS: MutationObserverInit = { | ||
| attributes: true, | ||
| characterData: true, | ||
| childList: true, | ||
| subtree: true, | ||
| }; | ||
|
|
||
| /** | ||
| * Recursively find all open shadow roots within a node tree. | ||
| */ | ||
| function collectShadowRoots(node: Node): ShadowRoot[] { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same again! Though interestingly maybe should have read this part first 🤔 Given we are attaching mutation observers onto each shadow root, which means we (1) have a list of all shadow roots and (2) know whenever the change, there’s an opportunity here to create a cache of shadow roots which can be iterated over in the other methods, which expect would be far more performant as would reduce the full tree walk down to once. Observation of changes could then invalidate that cache. |
||
| const roots: ShadowRoot[] = []; | ||
|
|
||
| if (isElement(node) && node.shadowRoot) { | ||
| roots.push(node.shadowRoot); | ||
| node.shadowRoot.querySelectorAll("*").forEach((child) => { | ||
| if (child.shadowRoot) { | ||
| roots.push(...collectShadowRoots(child)); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| if (isElement(node)) { | ||
| node.querySelectorAll("*").forEach((child) => { | ||
| if (child.shadowRoot) { | ||
| roots.push(...collectShadowRoots(child)); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| return roots; | ||
| } | ||
|
|
||
| export function observeDOM( | ||
| root: Root | undefined, | ||
| node: Node, | ||
|
|
@@ -10,21 +43,28 @@ export function observeDOM( | |
| return () => {}; | ||
| } | ||
|
|
||
| const MutationObserver = | ||
| const MutationObserverCtor = | ||
| typeof root !== "undefined" ? root?.MutationObserver : null; | ||
|
|
||
| if (MutationObserver) { | ||
| const mutationObserver = new MutationObserver(onChange); | ||
| if (MutationObserverCtor) { | ||
| const observers: MutationObserver[] = []; | ||
|
|
||
| mutationObserver.observe(node, { | ||
| attributes: true, | ||
| characterData: true, | ||
| childList: true, | ||
| subtree: true, | ||
| }); | ||
| // Observe the main container | ||
| const mainObserver = new MutationObserverCtor(onChange); | ||
| mainObserver.observe(node, OBSERVE_OPTIONS); | ||
| observers.push(mainObserver); | ||
|
|
||
| // Observe all shadow roots within the container | ||
| const shadowRoots = collectShadowRoots(node); | ||
|
|
||
| for (const shadowRoot of shadowRoots) { | ||
| const shadowObserver = new MutationObserverCtor(onChange); | ||
| shadowObserver.observe(shadowRoot, OBSERVE_OPTIONS); | ||
| observers.push(shadowObserver); | ||
| } | ||
|
|
||
| return () => { | ||
| mutationObserver.disconnect(); | ||
| observers.forEach((observer) => observer.disconnect()); | ||
| }; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m a little concerned how well this scales and if there any optimisations we can make 🤔
Querying for the selector match and then again for the wildcard, recursed for every shadow root will be resulting in multiple visits per node and expect be a degree of memory cost higher than if we were to implement a tree walk (either TreeWalker, or manually DFS stack) and check each node against the selector (eg with matches).
If we need to genuinely hit every node on the page then we need to take care to get it right!