Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clear-queens-lose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zag-js/core": patch
---

Change return type of Scope.getRootNode() from Node to Element to support querySelector / querySelectorAll.
9 changes: 9 additions & 0 deletions .changeset/custom-trigger-id-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@zag-js/popover": patch
"@zag-js/dialog": patch
"@zag-js/drawer": patch
"@zag-js/hover-card": patch
"@zag-js/tooltip": patch
---

Fix custom trigger elements (via `ids.trigger`) being ignored when shared across components (e.g. wrapping a `Popover.Trigger` in a `Tooltip` with the same id), causing broken positioning and a close-then-reopen cycle on trigger clicks.
5 changes: 5 additions & 0 deletions .changeset/dismissable-pointer-event-null-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zag-js/dismissable": patch
---

Fix crash (`Cannot read properties of null (reading 'style')`) when a pointer-blocking dialog or popover closes during SPA route teardown.
5 changes: 5 additions & 0 deletions .changeset/flat-taxes-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zag-js/dom-query": patch
---

Accept ShadowRoot as argument for query and queryAll.
10 changes: 10 additions & 0 deletions .changeset/pretty-candies-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@zag-js/hover-card": patch
"@zag-js/popover": patch
"@zag-js/tooltip": patch
"@zag-js/dialog": patch
"@zag-js/drawer": patch
"@zag-js/menu": patch
---

Fix trigger element lookups in shadow root.
2 changes: 1 addition & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export interface BindableFn {
export interface Scope {
id?: string | undefined
ids?: Record<string, any> | undefined
getRootNode: () => ShadowRoot | Document | Node
getRootNode: () => ShadowRoot | Document | Element
getById: <T extends Element = HTMLElement>(id: string) => T | null
getActiveElement: () => HTMLElement | null
isActiveElement: (elem: HTMLElement | null) => boolean
Expand Down
7 changes: 5 additions & 2 deletions packages/machines/dialog/src/dialog.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ export const getDescriptionEl = (ctx: Scope) => ctx.getById(getDescriptionId(ctx
export const getCloseTriggerEl = (ctx: Scope) => ctx.getById(getCloseTriggerId(ctx))

export const getTriggerEls = (ctx: Scope) =>
queryAll(ctx.getDoc(), `[data-scope="dialog"][data-part="trigger"][data-ownedby="${ctx.id}"]`)
queryAll(ctx.getRootNode(), `[data-scope="dialog"][data-part="trigger"][data-ownedby="${ctx.id}"]`)

export const getActiveTriggerEl = (ctx: Scope, value: string | null): HTMLElement | null => {
return value == null ? getTriggerEls(ctx)[0] : ctx.getById(getTriggerId(ctx, value))
if (value == null) {
return getTriggerEl(ctx) ?? getTriggerEls(ctx)[0]
}
return ctx.getById(getTriggerId(ctx, value))
}
2 changes: 1 addition & 1 deletion packages/machines/dialog/src/dialog.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export const machine = createMachine<DialogSchema>({
defer: true,
pointerBlocking: prop("modal"),
layerStyleTargets: [() => dom.getBackdropEl(scope), () => dom.getPositionerEl(scope)],
exclude: dom.getTriggerEls(scope),
exclude: [dom.getTriggerEl(scope), ...dom.getTriggerEls(scope)].filter(Boolean) as HTMLElement[],
onInteractOutside(event) {
prop("onInteractOutside")?.(event)
if (!prop("closeOnInteractOutside")) {
Expand Down
2 changes: 1 addition & 1 deletion packages/machines/drawer/src/drawer.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const getTriggerId = (ctx: Scope, value?: string) => {
}

export const getTriggerEls = (ctx: Scope): HTMLElement[] =>
queryAll<HTMLElement>(ctx.getDoc(), `[data-scope="drawer"][data-part="trigger"][data-ownedby="${ctx.id}"]`)
queryAll<HTMLElement>(ctx.getRootNode(), `[data-scope="drawer"][data-part="trigger"][data-ownedby="${ctx.id}"]`)

export const getActiveTriggerEl = (ctx: Scope, value: string | null): HTMLElement | null => {
if (value == null) return getTriggerEl(ctx) ?? getTriggerEls(ctx)[0]
Expand Down
2 changes: 1 addition & 1 deletion packages/machines/drawer/src/drawer.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@ export const machine = createMachine<DrawerSchema>({
defer: true,
pointerBlocking: prop("modal"),
layerStyleTargets: [() => dom.getBackdropEl(scope), () => dom.getPositionerEl(scope)],
exclude: [dom.getTriggerEl(scope)],
exclude: [dom.getTriggerEl(scope), ...dom.getTriggerEls(scope)].filter(Boolean) as HTMLElement[],
onInteractOutside(event) {
prop("onInteractOutside")?.(event)
if (!prop("closeOnInteractOutside")) {
Expand Down
10 changes: 8 additions & 2 deletions packages/machines/hover-card/src/hover-card.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ export const getContentEl = (scope: Scope) => scope.getById(getContentId(scope))
export const getPositionerEl = (scope: Scope) => scope.getById(getPositionerId(scope))

export const getTriggerEls = (scope: Scope): HTMLElement[] =>
queryAll<HTMLElement>(scope.getDoc(), `[data-scope="hover-card"][data-part="trigger"][data-ownedby="${scope.id}"]`)
queryAll<HTMLElement>(
scope.getRootNode(),
`[data-scope="hover-card"][data-part="trigger"][data-ownedby="${scope.id}"]`,
)

export const getActiveTriggerEl = (scope: Scope, value: string | null): HTMLElement | null => {
return value == null ? getTriggerEls(scope)[0] : scope.getById(getTriggerId(scope, value))
if (value == null) {
return getTriggerEl(scope) ?? getTriggerEls(scope)[0]
}
return scope.getById(getTriggerId(scope, value))
}
2 changes: 1 addition & 1 deletion packages/machines/hover-card/src/hover-card.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ export const machine = createMachine<HoverCardSchema>({
return trackDismissableElement(getContentEl, {
type: "popover",
defer: true,
exclude: dom.getTriggerEls(scope),
exclude: [dom.getTriggerEl(scope), ...dom.getTriggerEls(scope)].filter(Boolean) as HTMLElement[],
onDismiss() {
send({ type: "CLOSE", src: "interact-outside" })
},
Expand Down
4 changes: 2 additions & 2 deletions packages/machines/menu/src/menu.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ export const getArrowEl = (ctx: Scope) => ctx.getById(getArrowId(ctx))
export const getContextTriggerEl = (ctx: Scope) => ctx.getById(getContextTriggerId(ctx))

export const getTriggerEls = (ctx: Scope): HTMLElement[] =>
queryAll<HTMLElement>(ctx.getDoc(), `[data-scope="menu"][data-part="trigger"][data-ownedby="${ctx.id}"]`)
queryAll<HTMLElement>(ctx.getRootNode(), `[data-scope="menu"][data-part="trigger"][data-ownedby="${ctx.id}"]`)

export const getContextTriggerEls = (ctx: Scope): HTMLElement[] =>
queryAll<HTMLElement>(ctx.getDoc(), `[data-scope="menu"][data-part="context-trigger"][data-ownedby="${ctx.id}"]`)
queryAll<HTMLElement>(ctx.getRootNode(), `[data-scope="menu"][data-part="context-trigger"][data-ownedby="${ctx.id}"]`)

export const getActiveTriggerEl = (ctx: Scope, value: string | null): HTMLElement | null => {
// When value is null, use ID-based lookup (works for submenus with trigger-item)
Expand Down
9 changes: 7 additions & 2 deletions packages/machines/popover/src/popover.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ export const getCloseTriggerId = (scope: Scope) => scope.ids?.closeTrigger ?? `p

export const getAnchorEl = (scope: Scope) => scope.getById(getAnchorId(scope))

export const getTriggerEl = (scope: Scope) => scope.getById(getTriggerId(scope))

export const getTriggerEls = (scope: Scope): HTMLElement[] =>
queryAll<HTMLElement>(scope.getDoc(), `[data-scope="popover"][data-part="trigger"][data-ownedby="${scope.id}"]`)
queryAll<HTMLElement>(scope.getRootNode(), `[data-scope="popover"][data-part="trigger"][data-ownedby="${scope.id}"]`)

export const getActiveTriggerEl = (scope: Scope, value: string | null): HTMLElement | null => {
return value == null ? getTriggerEls(scope)[0] : scope.getById(getTriggerId(scope, value))
if (value == null) {
return getTriggerEl(scope) ?? getTriggerEls(scope)[0]
}
return scope.getById(getTriggerId(scope, value))
}
export const getContentEl = (scope: Scope) => scope.getById(getContentId(scope))
export const getPositionerEl = (scope: Scope) => scope.getById(getPositionerId(scope))
Expand Down
2 changes: 1 addition & 1 deletion packages/machines/popover/src/popover.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export const machine = createMachine<PopoverSchema>({
return trackDismissableElement(getContentEl, {
type: "popover",
pointerBlocking: prop("modal"),
exclude: dom.getTriggerEls(scope),
exclude: [dom.getTriggerEl(scope), ...dom.getTriggerEls(scope)].filter(Boolean) as HTMLElement[],
defer: true,
onEscapeKeyDown(event) {
prop("onEscapeKeyDown")?.(event)
Expand Down
7 changes: 5 additions & 2 deletions packages/machines/tooltip/src/tooltip.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ export const getPositionerEl = (scope: Scope) => scope.getById(getPositionerId(s
export const getArrowEl = (scope: Scope) => scope.getById(getArrowId(scope))

export const getTriggerEls = (scope: Scope): HTMLElement[] =>
queryAll<HTMLElement>(scope.getDoc(), `[data-scope="tooltip"][data-part="trigger"][data-ownedby="${scope.id}"]`)
queryAll<HTMLElement>(scope.getRootNode(), `[data-scope="tooltip"][data-part="trigger"][data-ownedby="${scope.id}"]`)

export const getActiveTriggerEl = (scope: Scope, value: string | null): HTMLElement | null => {
return value == null ? getTriggerEls(scope)[0] : scope.getById(getTriggerId(scope, value))
if (value == null) {
return getTriggerEl(scope) ?? getTriggerEls(scope)[0]
}
return scope.getById(getTriggerId(scope, value))
}
16 changes: 10 additions & 6 deletions packages/utilities/dismissable/src/pointer-event-outside.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ export function disablePointerEventsOutside(node: HTMLElement, persistentElement
const cleanups: VoidFunction[] = []

if (layerStack.hasPointerBlockingLayer() && !doc.body.hasAttribute("data-inert")) {
originalBodyPointerEvents = document.body.style.pointerEvents
originalBodyPointerEvents = doc.body.style.pointerEvents
queueMicrotask(() => {
doc.body.style.pointerEvents = "none"
doc.body.setAttribute("data-inert", "")
const body = doc.body
if (!body) return
body.style.pointerEvents = "none"
body.setAttribute("data-inert", "")
})
}

Expand All @@ -41,9 +43,11 @@ export function disablePointerEventsOutside(node: HTMLElement, persistentElement
return () => {
if (layerStack.hasPointerBlockingLayer()) return
queueMicrotask(() => {
doc.body.style.pointerEvents = originalBodyPointerEvents
doc.body.removeAttribute("data-inert")
if (doc.body.style.length === 0) doc.body.removeAttribute("style")
const body = doc.body
if (!body) return
body.style.pointerEvents = originalBodyPointerEvents
body.removeAttribute("data-inert")
if (body.style.length === 0) body.removeAttribute("style")
})
cleanups.forEach((fn) => fn())
}
Expand Down
2 changes: 1 addition & 1 deletion packages/utilities/dom-query/src/query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type Root = Document | Element | null | undefined
type Root = Document | ShadowRoot | Element | null | undefined

export function queryAll<T extends Element = HTMLElement>(root: Root, selector: string) {
return Array.from(root?.querySelectorAll<T>(selector) ?? [])
Expand Down
3 changes: 1 addition & 2 deletions website/demos/image-cropper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ export function ImageCropper(props: ImageCropperProps) {
<div {...api.getRootProps()}>
<div className={styles.Viewport} {...api.getViewportProps()}>
<img
src="https://placedog.net/500/280?id=2"
src="/image-cropper-demo.jpg"
alt="Dog to be cropped"
crossOrigin="anonymous"
width={500}
height={280}
className={styles.Image}
Expand Down
Binary file added website/public/image-cropper-demo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading