diff --git a/examples/solid-ts/src/routes/select-shadow-dom.tsx b/examples/solid-ts/src/routes/select-shadow-dom.tsx new file mode 100644 index 0000000000..f58b20c256 --- /dev/null +++ b/examples/solid-ts/src/routes/select-shadow-dom.tsx @@ -0,0 +1,63 @@ +import * as select from "@zag-js/select" +import { selectData } from "@zag-js/shared" +import styles from "@zag-js/shared/src/style.css?inline" +import { normalizeProps, useMachine } from "@zag-js/solid" +import { Index, createMemo, createSignal, createUniqueId, splitProps } from "solid-js" +import { Portal } from "solid-js/web" + +function Select(props: Partial & { portalRef?: HTMLElement }) { + const [portalProps, machineProps] = splitProps(props, ["portalRef"]) + + const [state, send] = useMachine( + select.machine({ + ...machineProps, + collection: select.collection({ items: selectData }), + id: createUniqueId(), + }), + ) + + const api = createMemo(() => select.connect(state, send, normalizeProps)) + + return ( +
+
+ +
+ +
+
    + + {(item) => ( +
  • + {item().label} + +
  • + )} +
    +
+
+
+
+ ) +} + +export default function Page() { + let mountRef!: HTMLElement + const [shadowRef, setShadowRef] = createSignal(null) + const getRootNode = () => shadowRef()?.shadowRoot! + + return ( +
+ +
+ +
+ ) +} diff --git a/examples/solid-ts/src/routes/tooltip-shadow-dom.tsx b/examples/solid-ts/src/routes/tooltip-shadow-dom.tsx new file mode 100644 index 0000000000..fa50ab7053 --- /dev/null +++ b/examples/solid-ts/src/routes/tooltip-shadow-dom.tsx @@ -0,0 +1,33 @@ +import { normalizeProps, useMachine } from "@zag-js/solid" +import * as tooltip from "@zag-js/tooltip" +import styles from "@zag-js/shared/src/style.css?inline" +import { createMemo, createSignal, Show } from "solid-js" +import { Portal } from "solid-js/web" + +export default function Page() { + let mountRef!: HTMLElement + const [shadowRef, setShadowRef] = createSignal(null) + + const getRootNode = () => shadowRef()?.shadowRoot! + + const [state, send] = useMachine(tooltip.machine({ id: "1", getRootNode })) + const api = createMemo(() => tooltip.connect(state, send, normalizeProps)) + + return ( +
+

Testing

+ + + + + +
+
Tooltip
+
+
+
+
+ +
+ ) +} diff --git a/packages/utilities/dismissable/src/dismissable-layer.ts b/packages/utilities/dismissable/src/dismissable-layer.ts index 4eb475701b..ceb32903d5 100644 --- a/packages/utilities/dismissable/src/dismissable-layer.ts +++ b/packages/utilities/dismissable/src/dismissable-layer.ts @@ -105,7 +105,10 @@ function trackDismissableElementImpl(node: MaybeElement, options: DismissableEle const _containers = Array.isArray(containers) ? containers : [containers] const persistentElements = options.persistentElements?.map((fn) => fn()).filter(isHTMLElement) if (persistentElements) _containers.push(...persistentElements) - return _containers.some((node) => contains(node, target)) || layerStack.isInNestedLayer(node, target) + return ( + _containers.filter(isHTMLElement).some((node) => contains(node, target)) || + layerStack.isInNestedLayer(node, target) + ) } const cleanups = [ diff --git a/packages/utilities/dom-query/src/contains.ts b/packages/utilities/dom-query/src/contains.ts index f217a39800..b771cdfa8a 100644 --- a/packages/utilities/dom-query/src/contains.ts +++ b/packages/utilities/dom-query/src/contains.ts @@ -1,9 +1,21 @@ -import { isHTMLElement } from "./is" +import { isHTMLElement, isShadowRoot } from "./is" type Target = HTMLElement | EventTarget | null | undefined export function contains(parent: Target, child: Target) { if (!parent || !child) return false if (!isHTMLElement(parent) || !isHTMLElement(child)) return false - return parent === child || parent.contains(child) + + const rootNode = child.getRootNode?.({ composed: true }) + if (parent.contains(child)) return true + + if (rootNode && isShadowRoot(rootNode)) { + let next: any = child + while (next) { + if (parent === next) return true + next = next.parentNode || next.host + } + } + + return false } diff --git a/packages/utilities/dom-query/src/env.ts b/packages/utilities/dom-query/src/env.ts index 15295c2fdf..ecd3ca30e5 100644 --- a/packages/utilities/dom-query/src/env.ts +++ b/packages/utilities/dom-query/src/env.ts @@ -28,3 +28,8 @@ export function getActiveElement(rootNode: Document | ShadowRoot): HTMLElement | return activeElement } + +export function getRootNode(el: Element | ShadowRoot): DocumentFragment { + if (isShadowRoot(el)) return el + return el.getRootNode({ composed: true }) as DocumentFragment +} diff --git a/packages/utilities/interact-outside/src/index.ts b/packages/utilities/interact-outside/src/index.ts index f85d50fb68..eb3209def4 100644 --- a/packages/utilities/interact-outside/src/index.ts +++ b/packages/utilities/interact-outside/src/index.ts @@ -1,13 +1,15 @@ import { addDomEvent, fireCustomEvent, isContextMenuEvent } from "@zag-js/dom-event" import { contains, - getDocument, + getRootNode, getEventTarget, getNearestOverflowAncestor, getWindow, isFocusable, isHTMLElement, raf, + getDocument, + isShadowRoot, } from "@zag-js/dom-query" import { callAll } from "@zag-js/utils" import { getParentWindow, getWindowFrames } from "./frame-utils" @@ -108,8 +110,8 @@ function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOp if (!node) return - const doc = getDocument(node) - const win = getWindow(node) + const rootNode = getRootNode(node) + const win = getWindow(rootNode) const frames = getWindowFrames(win) const parentWin = getParentWindow(win) @@ -117,6 +119,9 @@ function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOp const target = getEventTarget(event) if (!isHTMLElement(target)) return false + // Custom exclude function + if (exclude?.(target)) return false + // ignore disconnected nodes (removed from DOM) if (!target.isConnected) return false @@ -127,7 +132,7 @@ function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOp if (isEventPointWithin(node, event)) return false // Ex: page content that is scrollable - const triggerEl = doc.querySelector(`[aria-controls="${node!.id}"]`) + const triggerEl = rootNode.querySelector(`[aria-controls="${node!.id}"]`) if (triggerEl) { const triggerAncestor = getNearestOverflowAncestor(triggerEl) if (isEventWithinScrollbar(event, triggerAncestor)) return false @@ -137,8 +142,8 @@ function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOp const nodeAncestor = getNearestOverflowAncestor(node!) if (isEventWithinScrollbar(event, nodeAncestor)) return false - // Custom exclude function - return !exclude?.(target) + // Final fallback + return true } const pointerdownCleanups: Set = new Set() @@ -148,6 +153,12 @@ function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOp function handler() { const func = defer ? raf : (v: any) => v() const composedPath = event.composedPath?.() ?? [event.target] + + const target = getEventTarget(event) + + // skip shadow root event since it's handled by the host + if (target && isShadowRoot(target.getRootNode())) return + func(() => { if (!node || !isEventOutside(event)) return @@ -172,16 +183,21 @@ function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOp // flush any pending pointerup events pointerdownCleanups.forEach((fn) => fn()) // add a pointerup event listener to the document and all frame documents - pointerdownCleanups.add(addDomEvent(doc, "click", handler, { once: true })) + pointerdownCleanups.add(addDomEvent(rootNode, "click", handler, { once: true })) pointerdownCleanups.add(parentWin.addEventListener("click", handler, { once: true })) pointerdownCleanups.add(frames.addEventListener("click", handler, { once: true })) } else { handler() } } + const cleanups = new Set() + const doc = getDocument(rootNode) const timer = setTimeout(() => { + if (isShadowRoot(node.getRootNode())) { + cleanups.add(addDomEvent((rootNode as ShadowRoot).host, "pointerdown", onPointerDown, true)) + } cleanups.add(addDomEvent(doc, "pointerdown", onPointerDown, true)) cleanups.add(parentWin.addEventListener("pointerdown", onPointerDown, true)) cleanups.add(frames.addEventListener("pointerdown", onPointerDown, true)) @@ -210,7 +226,7 @@ function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOp }) } - cleanups.add(addDomEvent(doc, "focusin", onFocusin, true)) + cleanups.add(addDomEvent(rootNode, "focusin", onFocusin, true)) cleanups.add(parentWin.addEventListener("focusin", onFocusin, true)) cleanups.add(frames.addEventListener("focusin", onFocusin, true))