Skip to content

Commit 89a99d8

Browse files
committed
refactor: time picker
1 parent 7e3e1c8 commit 89a99d8

File tree

7 files changed

+608
-569
lines changed

7 files changed

+608
-569
lines changed

packages/machines/time-picker/src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ export { machine } from "./time-picker.machine"
44
export * from "./time-picker.parse"
55
export * from "./time-picker.props"
66
export type {
7-
MachineApi as Api,
7+
TimePickerApi as Api,
88
CellProps,
99
ColumnProps,
10-
UserDefinedContext as Context,
1110
ElementIds,
1211
FocusChangeDetails,
1312
OpenChangeDetails,
1413
PeriodCellProps,
1514
PositioningOptions,
16-
Service,
15+
TimePickerProps as Props,
16+
TimePickerService as Service,
1717
Time,
1818
TimePeriod,
1919
TimeUnit,

packages/machines/time-picker/src/time-picker.connect.ts

Lines changed: 60 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { ariaAttr, dataAttr, getEventKey, isComposingEvent } from "@zag-js/dom-q
22
import { getPlacementStyles } from "@zag-js/popper"
33
import type { EventKeyMap, NormalizeProps, PropTypes } from "@zag-js/types"
44
import { parts } from "./time-picker.anatomy"
5-
import { dom } from "./time-picker.dom"
6-
import type { MachineApi, Send, State } from "./time-picker.types"
5+
import * as dom from "./time-picker.dom"
6+
import type { TimePickerApi, TimePickerService } from "./time-picker.types"
77
import {
88
get12HourFormatPeriodHour,
99
getHourPeriod,
@@ -12,28 +12,34 @@ import {
1212
padStart,
1313
} from "./time-picker.utils"
1414

15-
export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>): MachineApi<T> {
16-
const disabled = state.context.disabled
17-
const readOnly = state.context.readOnly
15+
export function connect<T extends PropTypes>(
16+
service: TimePickerService,
17+
normalize: NormalizeProps<T>,
18+
): TimePickerApi<T> {
19+
const { state, send, prop, computed, scope, context } = service
1820

19-
const locale = state.context.locale
21+
const disabled = prop("disabled")
22+
const readOnly = prop("readOnly")
23+
24+
const locale = prop("locale")
2025
const hour12 = is12HourFormat(locale)
2126

22-
const min = state.context.min
23-
const max = state.context.max
24-
const steps = state.context.steps
27+
const min = prop("min")
28+
const max = prop("max")
29+
const steps = prop("steps")
2530

2631
const focused = state.matches("focused")
2732
const open = state.hasTag("open")
2833

29-
const value = state.context.value
30-
const valueAsString = state.context.valueAsString
31-
const currentTime = state.context.currentTime
34+
const value = context.get("value")
35+
const valueAsString = computed("valueAsString")
36+
const currentTime = context.get("currentTime")
37+
const focusedColumn = context.get("focusedColumn")
3238

33-
const currentPlacement = state.context.currentPlacement
39+
const currentPlacement = context.get("currentPlacement")
3440
const popperStyles = getPlacementStyles({
35-
...state.context.positioning,
36-
placement: state.context.currentPlacement,
41+
...prop("positioning"),
42+
placement: currentPlacement,
3743
})
3844

3945
return {
@@ -47,7 +53,7 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
4753
},
4854
setOpen(nextOpen) {
4955
if (nextOpen === open) return
50-
send(nextOpen ? "OPEN" : "CLOSE")
56+
send({ type: nextOpen ? "OPEN" : "CLOSE" })
5157
},
5258
setUnitValue(unit, value) {
5359
send({ type: "UNIT.SET", unit, value })
@@ -56,7 +62,7 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
5662
send({ type: "VALUE.SET", value })
5763
},
5864
clearValue() {
59-
send("VALUE.CLEAR")
65+
send({ type: "VALUE.CLEAR" })
6066
},
6167
getHours() {
6268
const length = hour12 ? 12 : 24
@@ -90,8 +96,8 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
9096
getLabelProps() {
9197
return normalize.label({
9298
...parts.label.attrs,
93-
dir: state.context.dir,
94-
htmlFor: dom.getInputId(state.context),
99+
dir: prop("dir"),
100+
htmlFor: dom.getInputId(scope),
95101
"data-state": open ? "open" : "closed",
96102
"data-disabled": dataAttr(disabled),
97103
"data-readonly": dataAttr(readOnly),
@@ -101,27 +107,27 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
101107
getControlProps() {
102108
return normalize.element({
103109
...parts.control.attrs,
104-
dir: state.context.dir,
105-
id: dom.getControlId(state.context),
110+
dir: prop("dir"),
111+
id: dom.getControlId(scope),
106112
"data-disabled": dataAttr(disabled),
107113
})
108114
},
109115

110116
getInputProps() {
111117
return normalize.input({
112118
...parts.input.attrs,
113-
dir: state.context.dir,
119+
dir: prop("dir"),
114120
autoComplete: "off",
115121
autoCorrect: "off",
116122
spellCheck: "false",
117-
id: dom.getInputId(state.context),
118-
name: state.context.name,
123+
id: dom.getInputId(scope),
124+
name: prop("name"),
119125
defaultValue: valueAsString,
120-
placeholder: getInputPlaceholder(state.context),
126+
placeholder: getInputPlaceholder(prop("placeholder"), prop("allowSeconds"), locale),
121127
disabled,
122128
readOnly,
123129
onFocus() {
124-
send("INPUT.FOCUS")
130+
send({ type: "INPUT.FOCUS" })
125131
},
126132
onBlur(event) {
127133
send({ type: "INPUT.BLUR", value: event.currentTarget.value })
@@ -138,42 +144,42 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
138144
getTriggerProps() {
139145
return normalize.button({
140146
...parts.trigger.attrs,
141-
id: dom.getTriggerId(state.context),
147+
id: dom.getTriggerId(scope),
142148
type: "button",
143-
"data-placement": state.context.currentPlacement,
149+
"data-placement": currentPlacement,
144150
disabled,
145151
"data-readonly": dataAttr(readOnly),
146152
"aria-label": open ? "Close calendar" : "Open calendar",
147-
"aria-controls": dom.getContentId(state.context),
153+
"aria-controls": dom.getContentId(scope),
148154
"data-state": open ? "open" : "closed",
149155
onClick(event) {
150156
if (event.defaultPrevented) return
151-
send("TRIGGER.CLICK")
157+
send({ type: "TRIGGER.CLICK" })
152158
},
153159
})
154160
},
155161

156162
getClearTriggerProps() {
157163
return normalize.button({
158164
...parts.clearTrigger.attrs,
159-
id: dom.getClearTriggerId(state.context),
165+
id: dom.getClearTriggerId(scope),
160166
type: "button",
161-
hidden: !state.context.value,
167+
hidden: !value,
162168
disabled,
163169
"data-readonly": dataAttr(readOnly),
164170
"aria-label": "Clear time",
165171
onClick(event) {
166172
if (event.defaultPrevented) return
167-
send("VALUE.CLEAR")
173+
send({ type: "VALUE.CLEAR" })
168174
},
169175
})
170176
},
171177

172178
getPositionerProps() {
173179
return normalize.element({
174180
...parts.positioner.attrs,
175-
dir: state.context.dir,
176-
id: dom.getPositionerId(state.context),
181+
dir: prop("dir"),
182+
id: dom.getPositionerId(scope),
177183
style: popperStyles.floating,
178184
})
179185
},
@@ -187,8 +193,8 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
187193
getContentProps() {
188194
return normalize.element({
189195
...parts.content.attrs,
190-
dir: state.context.dir,
191-
id: dom.getContentId(state.context),
196+
dir: prop("dir"),
197+
id: dom.getContentId(scope),
192198
hidden: !open,
193199
tabIndex: 0,
194200
role: "application",
@@ -219,12 +225,12 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
219225
// prevent tabbing out of the time picker
220226
Tab() {},
221227
Escape() {
222-
if (!state.context.disableLayer) return
228+
if (!prop("disableLayer")) return
223229
send({ type: "CONTENT.ESCAPE" })
224230
},
225231
}
226232

227-
const exec = keyMap[getEventKey(event, state.context)]
233+
const exec = keyMap[getEventKey(event, { dir: prop("dir") })]
228234

229235
if (exec) {
230236
exec(event)
@@ -235,24 +241,24 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
235241
},
236242

237243
getColumnProps(props) {
238-
const hidden = (props.unit === "second" && !state.context.allowSeconds) || (props.unit === "period" && !hour12)
244+
const hidden = (props.unit === "second" && !prop("allowSeconds")) || (props.unit === "period" && !hour12)
239245
return normalize.element({
240246
...parts.column.attrs,
241-
id: dom.getColumnId(state.context, props.unit),
247+
id: dom.getColumnId(scope, props.unit),
242248
"data-unit": props.unit,
243-
"data-focus": dataAttr(state.context.focusedColumn === props.unit),
249+
"data-focus": dataAttr(focusedColumn === props.unit),
244250
hidden,
245251
})
246252
},
247253

248254
getHourCellProps(props) {
249255
const hour = props.value
250256
const isSelectable = !(
251-
(min && get12HourFormatPeriodHour(hour, state.context.period) < min.hour) ||
252-
(max && get12HourFormatPeriodHour(hour, state.context.period) > max.hour)
257+
(min && get12HourFormatPeriodHour(hour, computed("period")) < min.hour) ||
258+
(max && get12HourFormatPeriodHour(hour, computed("period")) > max.hour)
253259
)
254-
const isSelected = state.context.value?.hour === get12HourFormatPeriodHour(hour, state.context.period)
255-
const isFocused = state.context.focusedColumn === "hour" && state.context.focusedValue === hour
260+
const isSelected = value?.hour === get12HourFormatPeriodHour(hour, computed("period"))
261+
const isFocused = focusedColumn === "hour" && context.get("focusedValue") === hour
256262

257263
const currentHour = hour12 && currentTime ? currentTime?.hour % 12 : currentTime?.hour
258264
const isCurrent = currentHour === hour || (hour === 12 && currentHour === 0)
@@ -279,17 +285,17 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
279285

280286
getMinuteCellProps(props) {
281287
const minute = props.value
282-
const { value } = state.context
288+
const value = context.get("value")
283289
const minMinute = min?.set({ second: 0 })
284290
const maxMinute = max?.set({ second: 0 })
285291

286292
const isSelectable = !(
287293
(minMinute && value && minMinute.compare(value.set({ minute })) > 0) ||
288294
(maxMinute && value && maxMinute.compare(value.set({ minute })) < 0)
289295
)
290-
const isSelected = state.context.value?.minute === minute
296+
const isSelected = value?.minute === minute
291297
const isCurrent = currentTime?.minute === minute
292-
const isFocused = state.context.focusedColumn === "minute" && state.context.focusedValue === minute
298+
const isFocused = focusedColumn === "minute" && context.get("focusedValue") === minute
293299

294300
return normalize.button({
295301
...parts.cell.attrs,
@@ -318,9 +324,9 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
318324
(min && value?.minute && min.compare(value.set({ second })) > 0) ||
319325
(max && value?.minute && max.compare(value.set({ second })) < 0)
320326
)
321-
const isSelected = state.context.value?.second === second
327+
const isSelected = value?.second === second
322328
const isCurrent = currentTime?.second === second
323-
const isFocused = state.context.focusedColumn === "second" && state.context.focusedValue === second
329+
const isFocused = focusedColumn === "second" && context.get("focusedValue") === second
324330

325331
return normalize.button({
326332
...parts.cell.attrs,
@@ -343,10 +349,10 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
343349
},
344350

345351
getPeriodCellProps(props) {
346-
const isSelected = state.context.period === props.value
347-
const currentPeriod = getHourPeriod(currentTime?.hour, state.context.locale)
352+
const isSelected = computed("period") === props.value
353+
const currentPeriod = getHourPeriod(currentTime?.hour, locale)
348354
const isCurrent = currentPeriod === props.value
349-
const isFocused = state.context.focusedColumn === "period" && state.context.focusedValue === props.value
355+
const isFocused = focusedColumn === "period" && context.get("focusedValue") === props.value
350356

351357
return normalize.button({
352358
...parts.cell.attrs,
Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,39 @@
1-
import { createScope, query, queryAll } from "@zag-js/dom-query"
2-
import type { MachineContext as Ctx, TimeUnit } from "./time-picker.types"
1+
import { query, queryAll } from "@zag-js/dom-query"
2+
import type { TimeUnit } from "./time-picker.types"
3+
import type { Scope } from "@zag-js/core"
34

4-
export const dom = createScope({
5-
getContentId: (ctx: Ctx) => ctx.ids?.content ?? `time-picker:${ctx.id}:content`,
6-
getColumnId: (ctx: Ctx, unit: TimeUnit) => ctx.ids?.column?.(unit) ?? `time-picker:${ctx.id}:column:${unit}`,
7-
getControlId: (ctx: Ctx) => ctx.ids?.control ?? `time-picker:${ctx.id}:control`,
8-
getClearTriggerId: (ctx: Ctx) => ctx.ids?.clearTrigger ?? `time-picker:${ctx.id}:clear-trigger`,
9-
getPositionerId: (ctx: Ctx) => ctx.ids?.positioner ?? `time-picker:${ctx.id}:positioner`,
10-
getInputId: (ctx: Ctx) => ctx.ids?.input ?? `time-picker:${ctx.id}:input`,
11-
getTriggerId: (ctx: Ctx) => ctx.ids?.trigger ?? `time-picker:${ctx.id}:trigger`,
5+
export const getContentId = (ctx: Scope) => ctx.ids?.content ?? `time-picker:${ctx.id}:content`
6+
export const getColumnId = (ctx: Scope, unit: TimeUnit) =>
7+
ctx.ids?.column?.(unit) ?? `time-picker:${ctx.id}:column:${unit}`
8+
export const getControlId = (ctx: Scope) => ctx.ids?.control ?? `time-picker:${ctx.id}:control`
9+
export const getClearTriggerId = (ctx: Scope) => ctx.ids?.clearTrigger ?? `time-picker:${ctx.id}:clear-trigger`
10+
export const getPositionerId = (ctx: Scope) => ctx.ids?.positioner ?? `time-picker:${ctx.id}:positioner`
11+
export const getInputId = (ctx: Scope) => ctx.ids?.input ?? `time-picker:${ctx.id}:input`
12+
export const getTriggerId = (ctx: Scope) => ctx.ids?.trigger ?? `time-picker:${ctx.id}:trigger`
1213

13-
getContentEl: (ctx: Ctx) => dom.getById(ctx, dom.getContentId(ctx)),
14-
getColumnEl: (ctx: Ctx, unit: TimeUnit) => query(dom.getContentEl(ctx), `[data-part=column][data-unit=${unit}]`),
15-
getColumnEls: (ctx: Ctx) => queryAll(dom.getContentEl(ctx), `[data-part=column]:not([hidden])`),
16-
getColumnCellEls: (ctx: Ctx, unit: TimeUnit) => queryAll(dom.getColumnEl(ctx, unit), `[data-part=cell]`),
14+
export const getContentEl = (ctx: Scope) => ctx.getById(getContentId(ctx))
15+
export const getColumnEl = (ctx: Scope, unit: TimeUnit) =>
16+
query(getContentEl(ctx), `[data-part=column][data-unit=${unit}]`)
17+
export const getColumnEls = (ctx: Scope) => queryAll(getContentEl(ctx), `[data-part=column]:not([hidden])`)
18+
export const getColumnCellEls = (ctx: Scope, unit: TimeUnit) => queryAll(getColumnEl(ctx, unit), `[data-part=cell]`)
1719

18-
getControlEl: (ctx: Ctx) => dom.getById(ctx, dom.getControlId(ctx)),
19-
getClearTriggerEl: (ctx: Ctx) => dom.getById(ctx, dom.getClearTriggerId(ctx)),
20-
getPositionerEl: (ctx: Ctx) => dom.getById(ctx, dom.getPositionerId(ctx)),
21-
getInputEl: (ctx: Ctx) => dom.getById<HTMLInputElement>(ctx, dom.getInputId(ctx)),
22-
getTriggerEl: (ctx: Ctx) => dom.getById(ctx, dom.getTriggerId(ctx)),
20+
export const getControlEl = (ctx: Scope) => ctx.getById(getControlId(ctx))
21+
export const getClearTriggerEl = (ctx: Scope) => ctx.getById(getClearTriggerId(ctx))
22+
export const getPositionerEl = (ctx: Scope) => ctx.getById(getPositionerId(ctx))
23+
export const getInputEl = (ctx: Scope) => ctx.getById<HTMLInputElement>(getInputId(ctx))
24+
export const getTriggerEl = (ctx: Scope) => ctx.getById(getTriggerId(ctx))
2325

24-
getFocusedCell: (ctx: Ctx) => query(dom.getContentEl(ctx), `[data-part=cell][data-focus]`),
25-
getInitialFocusCell: (ctx: Ctx, unit: TimeUnit): HTMLElement | null => {
26-
const contentEl = dom.getContentEl(ctx)
27-
let cellEl = query(contentEl, `[data-part=cell][data-unit=${unit}][aria-current]`)
28-
cellEl ||= query(contentEl, `[data-part=cell][data-unit=${unit}][data-now]`)
29-
cellEl ||= query(contentEl, `[data-part=cell][data-unit=${unit}]`)
30-
return cellEl
31-
},
26+
export const getFocusedCell = (ctx: Scope) => query(getContentEl(ctx), `[data-part=cell][data-focus]`)
27+
export const getInitialFocusCell = (ctx: Scope, unit: TimeUnit): HTMLElement | null => {
28+
const contentEl = getContentEl(ctx)
29+
let cellEl = query(contentEl, `[data-part=cell][data-unit=${unit}][aria-current]`)
30+
cellEl ||= query(contentEl, `[data-part=cell][data-unit=${unit}][data-now]`)
31+
cellEl ||= query(contentEl, `[data-part=cell][data-unit=${unit}]`)
32+
return cellEl
33+
}
3234

33-
getColumnUnit: (el: HTMLElement): TimeUnit => el.dataset.unit as TimeUnit,
34-
getCellValue: (el: HTMLElement | null): any => {
35-
const value = el?.dataset.value
36-
return el?.dataset.unit === "period" ? value : Number(value ?? "0")
37-
},
38-
})
35+
export const getColumnUnit = (el: HTMLElement): TimeUnit => el.dataset.unit as TimeUnit
36+
export const getCellValue = (el: HTMLElement | null): any => {
37+
const value = el?.dataset.value
38+
return el?.dataset.unit === "period" ? value : Number(value ?? "0")
39+
}

0 commit comments

Comments
 (0)