Skip to content

Commit 25d93b8

Browse files
committed
refactor: track rect utilities
1 parent 1b1bcbc commit 25d93b8

33 files changed

+115
-1418
lines changed

.changeset/soft-apples-build.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@zag-js/radio-group": patch
3+
"@zag-js/dom-query": patch
4+
"@zag-js/tabs": patch
5+
---
6+
7+
Fix resize observe for radio group and tabs indicator

examples/next-ts/package.json

-6
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@
3737
"@zag-js/docs": "workspace:*",
3838
"@zag-js/dom-query": "workspace:*",
3939
"@zag-js/editable": "workspace:*",
40-
"@zag-js/element-rect": "workspace:*",
41-
"@zag-js/element-size": "workspace:*",
4240
"@zag-js/file-upload": "workspace:*",
4341
"@zag-js/file-utils": "workspace:*",
4442
"@zag-js/floating-panel": "workspace:*",
@@ -106,10 +104,6 @@
106104
"@next/eslint-plugin-next": "15.2.4",
107105
"eslint-plugin-react": "7.37.5",
108106
"eslint-plugin-react-hooks": "5.2.0",
109-
"@atlaskit/pragmatic-drag-and-drop": "1.5.2",
110-
"@atlaskit/pragmatic-drag-and-drop-flourish": "2.0.2",
111-
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
112-
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "2.1.0",
113107
"typescript": "5.8.2"
114108
},
115109
"license": "MIT"

examples/nuxt-ts/package.json

-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@
3333
"@zag-js/docs": "workspace:*",
3434
"@zag-js/dom-query": "workspace:*",
3535
"@zag-js/editable": "workspace:*",
36-
"@zag-js/element-rect": "workspace:*",
37-
"@zag-js/element-size": "workspace:*",
3836
"@zag-js/file-upload": "workspace:*",
3937
"@zag-js/file-utils": "workspace:*",
4038
"@zag-js/floating-panel": "workspace:*",

examples/preact-ts/package.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@
3232
"@zag-js/docs": "workspace:*",
3333
"@zag-js/dom-query": "workspace:*",
3434
"@zag-js/editable": "workspace:*",
35-
"@zag-js/element-rect": "workspace:*",
36-
"@zag-js/element-size": "workspace:*",
3735
"@zag-js/file-upload": "workspace:*",
3836
"@zag-js/file-utils": "workspace:*",
3937
"@zag-js/floating-panel": "workspace:*",
@@ -93,4 +91,4 @@
9391
"typescript": "5.8.2",
9492
"vite": "6.2.5"
9593
}
96-
}
94+
}

examples/solid-ts/package.json

-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@
3636
"@zag-js/docs": "workspace:*",
3737
"@zag-js/dom-query": "workspace:*",
3838
"@zag-js/editable": "workspace:*",
39-
"@zag-js/element-rect": "workspace:*",
40-
"@zag-js/element-size": "workspace:*",
4139
"@zag-js/file-upload": "workspace:*",
4240
"@zag-js/file-utils": "workspace:*",
4341
"@zag-js/floating-panel": "workspace:*",

examples/svelte-ts/package.json

-2
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434
"@zag-js/docs": "workspace:*",
3535
"@zag-js/dom-query": "workspace:*",
3636
"@zag-js/editable": "workspace:*",
37-
"@zag-js/element-rect": "workspace:*",
38-
"@zag-js/element-size": "workspace:*",
3937
"@zag-js/file-upload": "workspace:*",
4038
"@zag-js/file-utils": "workspace:*",
4139
"@zag-js/floating-panel": "workspace:*",

examples/vanilla-ts/package.json

-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@
3737
"@zag-js/docs": "workspace:*",
3838
"@zag-js/dom-query": "workspace:*",
3939
"@zag-js/editable": "workspace:*",
40-
"@zag-js/element-rect": "workspace:*",
41-
"@zag-js/element-size": "workspace:*",
4240
"@zag-js/file-upload": "workspace:*",
4341
"@zag-js/file-utils": "workspace:*",
4442
"@zag-js/floating-panel": "workspace:*",

packages/machines/radio-group/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
"dependencies": {
3737
"@zag-js/anatomy": "workspace:*",
3838
"@zag-js/dom-query": "workspace:*",
39-
"@zag-js/element-rect": "workspace:*",
4039
"@zag-js/focus-visible": "workspace:*",
4140
"@zag-js/utils": "workspace:*",
4241
"@zag-js/core": "workspace:*",

packages/machines/radio-group/src/radio-group.connect.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@ export function connect<T extends PropTypes>(
1010
normalize: NormalizeProps<T>,
1111
): RadioGroupApi<T> {
1212
const { context, send, computed, prop, scope } = service
13+
1314
const groupDisabled = computed("isDisabled")
1415
const readOnly = prop("readOnly")
1516

1617
function getItemState(props: ItemProps): ItemState {
18+
const focused = context.get("focusedValue") === props.value
19+
const focusVisible = focused && isFocusVisible()
1720
return {
1821
value: props.value,
1922
invalid: !!props.invalid,
2023
disabled: !!props.disabled || groupDisabled,
2124
checked: context.get("value") === props.value,
22-
focused: context.get("focusedValue") === props.value,
25+
focused,
26+
focusVisible,
2327
hovered: context.get("hoveredValue") === props.value,
2428
active: context.get("activeValue") === props.value,
2529
}
@@ -29,7 +33,7 @@ export function connect<T extends PropTypes>(
2933
const itemState = getItemState(props)
3034
return {
3135
"data-focus": dataAttr(itemState.focused),
32-
"data-focus-visible": dataAttr(itemState.focused && context.get("focusVisible")),
36+
"data-focus-visible": dataAttr(itemState.focusVisible),
3337
"data-disabled": dataAttr(itemState.disabled),
3438
"data-readonly": dataAttr(readOnly),
3539
"data-state": itemState.checked ? "checked" : "unchecked",
@@ -166,11 +170,10 @@ export function connect<T extends PropTypes>(
166170
}
167171
},
168172
onBlur() {
169-
send({ type: "SET_FOCUSED", value: null, focused: false, focusVisible: false })
173+
send({ type: "SET_FOCUSED", value: null, focused: false })
170174
},
171175
onFocus() {
172-
const focusVisible = isFocusVisible()
173-
send({ type: "SET_FOCUSED", value: props.value, focused: true, focusVisible })
176+
send({ type: "SET_FOCUSED", value: props.value, focused: true })
174177
},
175178
onKeyDown(event) {
176179
if (event.defaultPrevented) return

packages/machines/radio-group/src/radio-group.machine.ts

+6-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createGuards, createMachine } from "@zag-js/core"
2-
import { dispatchInputCheckedEvent, nextTick, trackFormControl } from "@zag-js/dom-query"
3-
import { trackElementRect } from "@zag-js/element-rect"
2+
import { dispatchInputCheckedEvent, trackElementRect, trackFormControl } from "@zag-js/dom-query"
43
import { trackFocusVisible } from "@zag-js/focus-visible"
54
import { isString } from "@zag-js/utils"
65
import * as dom from "./radio-group.dom"
@@ -47,9 +46,6 @@ export const machine = createMachine<RadioGroupSchema>({
4746
fieldsetDisabled: bindable<boolean>(() => ({
4847
defaultValue: false,
4948
})),
50-
focusVisible: bindable<boolean>(() => ({
51-
defaultValue: false,
52-
})),
5349
ssr: bindable<boolean>(() => ({
5450
defaultValue: true,
5551
})),
@@ -136,7 +132,6 @@ export const machine = createMachine<RadioGroupSchema>({
136132
},
137133
setFocused({ context, event }) {
138134
context.set("focusedValue", event.value)
139-
context.set("focusVisible", event.focusVisible)
140135
},
141136
syncInputElements({ context, scope }) {
142137
const inputs = dom.getInputEls(scope)
@@ -162,19 +157,17 @@ export const machine = createMachine<RadioGroupSchema>({
162157
const radioEl = dom.getRadioEl(scope, value)
163158

164159
if (value == null || !radioEl) {
160+
context.set("canIndicatorTransition", false)
165161
context.set("indicatorRect", {})
166162
return
167163
}
168164

169-
const indicatorCleanup = trackElementRect(radioEl, {
170-
getRect(el) {
165+
const indicatorCleanup = trackElementRect([radioEl], {
166+
measure(el) {
171167
return dom.getOffsetRect(el)
172168
},
173-
onChange(rect) {
174-
context.set("indicatorRect", dom.resolveRect(rect))
175-
nextTick(() => {
176-
context.set("canIndicatorTransition", false)
177-
})
169+
onEntry({ rects }) {
170+
context.set("indicatorRect", dom.resolveRect(rects[0]))
178171
},
179172
})
180173

packages/machines/radio-group/src/radio-group.types.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,6 @@ interface PrivateContext {
9696
* Whether the radio group's fieldset is disabled
9797
*/
9898
fieldsetDisabled: boolean
99-
/**
100-
* Whether the radio group is in focus
101-
*/
102-
focusVisible: boolean
10399
/**
104100
* Whether the radio group is in server-side rendering
105101
*/
@@ -169,6 +165,10 @@ export interface ItemState {
169165
* Whether the item is focused
170166
*/
171167
focused: boolean
168+
/**
169+
* Whether the item is focused and the focus is visible
170+
*/
171+
focusVisible: boolean
172172
/**
173173
* Whether the item is hovered
174174
*/

packages/machines/slider/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
"@zag-js/core": "workspace:*",
3232
"@zag-js/dom-query": "workspace:*",
3333
"@zag-js/utils": "workspace:*",
34-
"@zag-js/element-size": "workspace:*",
3534
"@zag-js/types": "workspace:*"
3635
},
3736
"devDependencies": {

packages/machines/slider/src/slider.connect.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export function connect<T extends PropTypes>(service: SliderService, normalize:
102102
onClick(event) {
103103
if (!interactive) return
104104
event.preventDefault()
105-
dom.getFirstEl(scope)?.focus()
105+
dom.getFirstThumbEl(scope)?.focus()
106106
},
107107
style: {
108108
userSelect: "none",

packages/machines/slider/src/slider.dom.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ export const getMarkerId = (ctx: Scope, value: number) => ctx.ids?.marker?.(valu
1717

1818
export const getRootEl = (ctx: Scope) => ctx.getById(getRootId(ctx))
1919
export const getThumbEl = (ctx: Scope, index: number) => ctx.getById(getThumbId(ctx, index))
20+
export const getThumbEls = (ctx: Scope) => queryAll(getControlEl(ctx), "[role=slider]")
21+
export const getFirstThumbEl = (ctx: Scope) => getThumbEls(ctx)[0]
2022
export const getHiddenInputEl = (ctx: Scope, index: number) =>
2123
ctx.getById<HTMLInputElement>(getHiddenInputId(ctx, index))
2224
export const getControlEl = (ctx: Scope) => ctx.getById(getControlId(ctx))
23-
export const getElements = (ctx: Scope) => queryAll(getControlEl(ctx), "[role=slider]")
24-
export const getFirstEl = (ctx: Scope) => getElements(ctx)[0]
2525
export const getRangeEl = (ctx: Scope) => ctx.getById(getRangeId(ctx))
2626

27-
export const getValueFromPoint = (params: Params<SliderSchema>, point: Point) => {
27+
export const getPointValue = (params: Params<SliderSchema>, point: Point) => {
2828
const { prop, scope } = params
2929
const controlEl = getControlEl(scope)
3030
if (!controlEl) return
@@ -44,3 +44,10 @@ export const dispatchChangeEvent = (ctx: Scope, value: number[]) => {
4444
dispatchInputValueEvent(inputEl, { value })
4545
})
4646
}
47+
48+
export const getOffsetRect = (el: HTMLElement | undefined) => ({
49+
left: el?.offsetLeft ?? 0,
50+
top: el?.offsetTop ?? 0,
51+
width: el?.offsetWidth ?? 0,
52+
height: el?.offsetHeight ?? 0,
53+
})

packages/machines/slider/src/slider.machine.ts

+26-13
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { createMachine } from "@zag-js/core"
2-
import { raf, setElementValue, trackFormControl, trackPointerMove } from "@zag-js/dom-query"
3-
import { trackElementsSize, type ElementSize } from "@zag-js/element-size"
4-
import { clampValue, getValuePercent, getValueRanges, isEqual, setValueAtIndex, snapValueToStep } from "@zag-js/utils"
2+
import { raf, setElementValue, trackFormControl, trackPointerMove, trackElementRect } from "@zag-js/dom-query"
3+
import type { Size } from "@zag-js/types"
4+
import {
5+
clampValue,
6+
getValuePercent,
7+
getValueRanges,
8+
isEqual,
9+
pick,
10+
setValueAtIndex,
11+
snapValueToStep,
12+
} from "@zag-js/utils"
513
import * as dom from "./slider.dom"
614
import type { SliderSchema } from "./slider.types"
715
import { constrainValue, decrement, getClosestIndex, getRangeAtIndex, increment, normalizeValues } from "./slider.utils"
816

9-
const isEqualSize = (a: ElementSize | null, b: ElementSize | null) => {
17+
const isEqualSize = (a: Size | null, b: Size | null) => {
1018
return a?.width === b?.width && a?.height === b?.height
1119
}
1220

@@ -91,7 +99,7 @@ export const machine = createMachine<SliderSchema>({
9199
})
92100
},
93101

94-
effects: ["trackFormControlState", "trackThumbsSize"],
102+
effects: ["trackFormControlState", "trackThumbSize"],
95103

96104
on: {
97105
SET_VALUE: [
@@ -201,19 +209,24 @@ export const machine = createMachine<SliderSchema>({
201209
},
202210
})
203211
},
204-
trackThumbsSize({ context, scope, prop }) {
212+
213+
trackThumbSize({ context, scope, prop }) {
205214
if (prop("thumbAlignment") !== "contain" || prop("thumbSize")) return
206215

207-
return trackElementsSize({
208-
getNodes: () => dom.getElements(scope),
209-
observeMutation: true,
210-
callback(size) {
211-
if (!size || isEqualSize(context.get("thumbSize"), size)) return
216+
return trackElementRect(dom.getThumbEls(scope), {
217+
box: "border-box",
218+
measure(el) {
219+
return dom.getOffsetRect(el)
220+
},
221+
onEntry({ rects }) {
222+
const size = pick(rects[0], ["width", "height"])
223+
if (isEqualSize(context.get("thumbSize"), size)) return
212224
context.set("thumbSize", size)
213225
},
214226
})
215227
},
216228
},
229+
217230
actions: {
218231
dispatchChangeEvent({ context, scope }) {
219232
dom.dispatchChangeEvent(scope, context.get("value"))
@@ -230,7 +243,7 @@ export const machine = createMachine<SliderSchema>({
230243
setClosestThumbIndex(params) {
231244
const { context, event } = params
232245

233-
const pointValue = dom.getValueFromPoint(params, event.point)
246+
const pointValue = dom.getPointValue(params, event.point)
234247
if (pointValue == null) return
235248

236249
const focusedIndex = getClosestIndex(params, pointValue)
@@ -245,7 +258,7 @@ export const machine = createMachine<SliderSchema>({
245258
setPointerValue(params) {
246259
queueMicrotask(() => {
247260
const { context, event } = params
248-
const pointValue = dom.getValueFromPoint(params, event.point)
261+
const pointValue = dom.getPointValue(params, event.point)
249262
if (pointValue == null) return
250263

251264
const focusedIndex = context.get("focusedIndex")

packages/machines/tabs/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
"dependencies": {
2929
"@zag-js/anatomy": "workspace:*",
3030
"@zag-js/dom-query": "workspace:*",
31-
"@zag-js/element-rect": "workspace:*",
3231
"@zag-js/utils": "workspace:*",
3332
"@zag-js/core": "workspace:*",
3433
"@zag-js/types": "workspace:*"

packages/machines/tabs/src/tabs.machine.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createGuards, createMachine } from "@zag-js/core"
2-
import { clickIfLink, getFocusables, isAnchorElement, nextTick, raf } from "@zag-js/dom-query"
2+
import { clickIfLink, getFocusables, isAnchorElement, nextTick, raf, trackElementRect } from "@zag-js/dom-query"
33
import * as dom from "./tabs.dom"
44
import type { TabsSchema } from "./tabs.types"
55

@@ -287,17 +287,17 @@ export const machine = createMachine<TabsSchema>({
287287
const indicatorEl = dom.getIndicatorEl(scope)
288288
if (!triggerEl || !indicatorEl) return
289289

290-
const exec = () => {
291-
const rect = dom.getOffsetRect(triggerEl)
292-
context.set("indicatorRect", dom.resolveRect(rect))
293-
}
294-
295-
exec()
296-
const win = scope.getWin()
297-
const obs = new win.ResizeObserver(exec)
298-
obs.observe(triggerEl)
290+
const indicatorCleanup = trackElementRect([triggerEl], {
291+
measure(el) {
292+
return dom.getOffsetRect(el)
293+
},
294+
onEntry({ rects }) {
295+
const [rect] = rects
296+
context.set("indicatorRect", dom.resolveRect(rect))
297+
},
298+
})
299299

300-
refs.set("indicatorCleanup", () => obs.disconnect())
300+
refs.set("indicatorCleanup", indicatorCleanup)
301301
},
302302
navigateIfNeeded({ context, prop, scope }) {
303303
const value = context.get("value")

packages/utilities/dom-query/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ export * from "./press"
1616
export * from "./proxy-tab-focus"
1717
export * from "./query"
1818
export * from "./raf"
19+
export * from "./resize-observer"
1920
export * from "./scope"
2021
export * from "./searchable"
2122
export * from "./set"
22-
export { MAX_Z_INDEX, ariaAttr, dataAttr } from "./shared"
23+
export { ariaAttr, dataAttr, MAX_Z_INDEX } from "./shared"
2324
export * from "./tabbable"
2425
export * from "./text-selection"
2526
export * from "./typeahead"

0 commit comments

Comments
 (0)