Skip to content

Commit

Permalink
refactor: date picker
Browse files Browse the repository at this point in the history
  • Loading branch information
segunadebayo committed Feb 3, 2024
1 parent f8748d7 commit e68f731
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 69 deletions.
5 changes: 5 additions & 0 deletions .changeset/green-lobsters-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zag-js/date-picker": minor
---

[BREAKING] Change date picker to `inputProps` to `getInputProps` to support multiple inputs.
6 changes: 4 additions & 2 deletions .xstate/date-picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ const fetchMachine = createMachine({
actions: ["focusFirstSelectedDate", "focusActiveCell"]
},
"INPUT.FOCUS": {
target: "focused"
target: "focused",
actions: ["setActiveIndex"]
},
"TRIGGER.CLICK": [{
cond: "isOpenControlled",
Expand Down Expand Up @@ -151,7 +152,8 @@ const fetchMachine = createMachine({
actions: ["focusParsedDate", "selectFocusedDate"]
},
"INPUT.BLUR": {
target: "idle"
target: "idle",
actions: ["setActiveIndexToStart"]
},
OPEN: [{
cond: "isOpenControlled",
Expand Down
3 changes: 2 additions & 1 deletion examples/next-app/components/date-range-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export function DateRangePicker(props: Props) {
</output>

<div {...api.controlProps}>
<input {...api.inputProps} />
<input {...api.getInputProps({ index: 0 })} />
<input {...api.getInputProps({ index: 1 })} />
<button {...api.clearTriggerProps}></button>
<button {...api.triggerProps}>🗓</button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion examples/next-ts/pages/date-picker-multi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function Page() {
</output>

<div {...api.controlProps}>
<input {...api.inputProps} />
<input {...api.getInputProps()} />
<button {...api.clearTriggerProps}></button>
<button {...api.triggerProps}>🗓</button>
</div>
Expand Down
3 changes: 2 additions & 1 deletion examples/next-ts/pages/date-picker-range.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export default function Page() {
</output>

<div {...api.controlProps}>
<input {...api.inputProps} />
<input {...api.getInputProps({ index: 0 })} />
<input {...api.getInputProps({ index: 1 })} />
<button {...api.clearTriggerProps}></button>
<button {...api.triggerProps}>🗓</button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion examples/next-ts/pages/date-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function Page() {
</output>

<div {...api.controlProps}>
<input {...api.inputProps} />
<input {...api.getInputProps()} />
<button {...api.clearTriggerProps}></button>
<button {...api.triggerProps}>🗓</button>
</div>
Expand Down
76 changes: 40 additions & 36 deletions packages/machines/date-picker/src/date-picker.connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
labelProps: normalize.label({
...parts.label.attrs,
dir: state.context.dir,
htmlFor: dom.getInputId(state.context),
htmlFor: dom.getInputId(state.context, 0),
"data-state": isOpen ? "open" : "closed",
"data-disabled": dataAttr(disabled),
"data-readonly": dataAttr(readOnly),
Expand Down Expand Up @@ -622,41 +622,45 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
})
},

inputProps: normalize.input({
...parts.input.attrs,
id: dom.getInputId(state.context),
autoComplete: "off",
autoCorrect: "off",
spellCheck: "false",
dir: state.context.dir,
name: state.context.name,
"data-state": isOpen ? "open" : "closed",
readOnly,
disabled,
placeholder: getInputPlaceholder(locale),
defaultValue: state.context.inputValue,
onBeforeInput(event) {
const { data } = getNativeEvent(event)
if (!isValidCharacter(data, separator)) {
event.preventDefault()
}
},
onFocus() {
send("INPUT.FOCUS")
},
onBlur(event) {
send({ type: "INPUT.BLUR", value: event.currentTarget.value })
},
onKeyDown(event) {
if (event.key !== "Enter" || !isInteractive) return
if (isUnavailable(state.context.focusedValue)) return
send({ type: "INPUT.ENTER", value: event.currentTarget.value })
},
onChange(event) {
const { value } = event.target
send({ type: "INPUT.CHANGE", value: ensureValidCharacters(value, separator) })
},
}),
getInputProps(props = {}) {
const { index = 0 } = props

return normalize.input({
...parts.input.attrs,
id: dom.getInputId(state.context, index),
autoComplete: "off",
autoCorrect: "off",
spellCheck: "false",
dir: state.context.dir,
name: state.context.name,
"data-state": isOpen ? "open" : "closed",
readOnly,
disabled,
placeholder: getInputPlaceholder(locale),
defaultValue: state.context.inputValue[index],
onBeforeInput(event) {
const { data } = getNativeEvent(event)
if (!isValidCharacter(data, separator)) {
event.preventDefault()
}
},
onFocus() {
send({ type: "INPUT.FOCUS", index })
},
onBlur(event) {
send({ type: "INPUT.BLUR", value: event.currentTarget.value, index })
},
onKeyDown(event) {
if (event.key !== "Enter" || !isInteractive) return
if (isUnavailable(state.context.focusedValue)) return
send({ type: "INPUT.ENTER", value: event.currentTarget.value, index })
},
onChange(event) {
const { value } = event.target
send({ type: "INPUT.CHANGE", value: ensureValidCharacters(value, separator), index })
},
})
},

monthSelectProps: normalize.select({
...parts.monthSelect.attrs,
Expand Down
6 changes: 3 additions & 3 deletions packages/machines/date-picker/src/date-picker.dom.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createScope, query } from "@zag-js/dom-query"
import { createScope, query, queryAll } from "@zag-js/dom-query"
import type { DateView, MachineContext as Ctx } from "./date-picker.types"

export const dom = createScope({
Expand All @@ -14,7 +14,7 @@ export const dom = createScope({
getViewTriggerId: (ctx: Ctx, view: DateView) => ctx.ids?.viewTrigger?.(view) ?? `datepicker:${ctx.id}:view:${view}`,
getClearTriggerId: (ctx: Ctx) => ctx.ids?.clearTrigger ?? `datepicker:${ctx.id}:clear`,
getControlId: (ctx: Ctx) => ctx.ids?.control ?? `datepicker:${ctx.id}:control`,
getInputId: (ctx: Ctx) => ctx.ids?.input ?? `datepicker:${ctx.id}:input`,
getInputId: (ctx: Ctx, index: number) => ctx.ids?.input?.(index) ?? `datepicker:${ctx.id}:input:${index}`,
getTriggerId: (ctx: Ctx) => ctx.ids?.trigger ?? `datepicker:${ctx.id}:trigger`,
getPositionerId: (ctx: Ctx) => ctx.ids?.positioner ?? `datepicker:${ctx.id}:positioner`,
getMonthSelectId: (ctx: Ctx) => ctx.ids?.monthSelect ?? `datepicker:${ctx.id}:month-select`,
Expand All @@ -27,7 +27,7 @@ export const dom = createScope({
),
getTriggerEl: (ctx: Ctx) => dom.getById<HTMLButtonElement>(ctx, dom.getTriggerId(ctx)),
getContentEl: (ctx: Ctx) => dom.getById(ctx, dom.getContentId(ctx)),
getInputEl: (ctx: Ctx) => dom.getById<HTMLInputElement>(ctx, dom.getInputId(ctx)),
getInputEls: (ctx: Ctx) => queryAll<HTMLInputElement>(dom.getControlEl(ctx), `[data-part=input]`),
getYearSelectEl: (ctx: Ctx) => dom.getById<HTMLSelectElement>(ctx, dom.getYearSelectId(ctx)),
getMonthSelectEl: (ctx: Ctx) => dom.getById<HTMLSelectElement>(ctx, dom.getMonthSelectId(ctx)),
getClearTriggerEl: (ctx: Ctx) => dom.getById<HTMLButtonElement>(ctx, dom.getClearTriggerId(ctx)),
Expand Down
39 changes: 26 additions & 13 deletions packages/machines/date-picker/src/date-picker.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ const transformContext = (ctx: Partial<MachineContext>): MachineContext => {
const startValue = alignDate(focusedValue, "start", { months: numOfMonths }, locale)

// format input value
const inputValue = ctx.format?.(value) ?? formatValue({ locale, timeZone, selectionMode, value })
const inputValue = ctx.format
? value.map((val) => ctx.format!(val))
: formatValue({ locale, timeZone, selectionMode, value })

return {
locale,
Expand Down Expand Up @@ -166,6 +168,7 @@ export function machine(userContext: UserDefinedContext) {
},
"INPUT.FOCUS": {
target: "focused",
actions: ["setActiveIndex"],
},
"TRIGGER.CLICK": [
{
Expand Down Expand Up @@ -212,6 +215,7 @@ export function machine(userContext: UserDefinedContext) {
},
"INPUT.BLUR": {
target: "idle",
actions: ["setActiveIndexToStart"],
},
OPEN: [
{
Expand Down Expand Up @@ -551,7 +555,7 @@ export function machine(userContext: UserDefinedContext) {
},
trackDismissableElement(ctx, _evt, { send }) {
return trackDismissableElement(dom.getContentEl(ctx), {
exclude: [dom.getInputEl(ctx), dom.getTriggerEl(ctx), dom.getClearTriggerEl(ctx)],
exclude: [...dom.getInputEls(ctx), dom.getTriggerEl(ctx), dom.getClearTriggerEl(ctx)],
onInteractOutside(event) {
ctx.restoreFocus = !event.detail.focusable
},
Expand Down Expand Up @@ -596,17 +600,16 @@ export function machine(userContext: UserDefinedContext) {
set.focusedValue(ctx, ctx.value[0])
},
setInputValue(ctx) {
const input = dom.getInputEl(ctx)
if (!input) return
ctx.inputValue = ctx.format?.(ctx.value) ?? formatValue(ctx)
ctx.inputValue = ctx.format ? ctx.value.map((date) => ctx.format!(date)) : formatValue(ctx)
},
syncInputElement(ctx) {
const inputEl = dom.getInputEl(ctx)
if (!inputEl || inputEl.value === ctx.inputValue) return
const inputEls = dom.getInputEls(ctx)
raf(() => {
// move cursor to the end
inputEl.value = ctx.inputValue
inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length)
inputEls.forEach((inputEl, index) => {
const currentValue = ctx.inputValue[index]
if (currentValue == null) return
dom.setValue(inputEl, currentValue)
})
})
},
setFocusedDate(ctx, evt) {
Expand Down Expand Up @@ -762,6 +765,9 @@ export function machine(userContext: UserDefinedContext) {
const range = getDecadeRange(ctx.focusedValue.year)
set.focusedValue(ctx, ctx.focusedValue.set({ year: range.at(-1) }))
},
setActiveIndex(ctx, evt) {
ctx.activeIndex = evt.index
},
setActiveIndexToEnd(ctx) {
ctx.activeIndex = 1
},
Expand Down Expand Up @@ -790,8 +796,14 @@ export function machine(userContext: UserDefinedContext) {
},
focusInputElement(ctx) {
raf(() => {
const inputEl = dom.getInputEl(ctx)
const inputEls = dom.getInputEls(ctx)

const lastIndexWithValue = inputEls.findLastIndex((inputEl) => inputEl.value !== "")
const indexToFocus = Math.max(lastIndexWithValue, 0)

const inputEl = inputEls[indexToFocus]
inputEl?.focus({ preventScroll: true })
// move cursor to the end
inputEl?.setSelectionRange(inputEl.value.length, inputEl.value.length)
})
},
Expand All @@ -806,8 +818,9 @@ export function machine(userContext: UserDefinedContext) {
yearSelectEl.value = ctx.focusedValue.year.toString()
},
focusParsedDate(ctx, evt) {
ctx.inputValue = evt.value
const date = parseDateString(ctx.inputValue, ctx.locale, ctx.timeZone)
if (evt.index == null) return
ctx.inputValue[evt.index] = evt.value
const date = parseDateString(evt.value, ctx.locale, ctx.timeZone)
set.focusedValue(ctx, date)
},
resetView(ctx, _evt, { initialContext }) {
Expand Down
14 changes: 9 additions & 5 deletions packages/machines/date-picker/src/date-picker.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export type ElementIds = Partial<{
viewTrigger(view: DateView): string
clearTrigger: string
control: string
input: string
input(index: number): string
trigger: string
monthSelect: string
yearSelect: string
Expand Down Expand Up @@ -168,11 +168,11 @@ interface PublicContext extends DirectionProperty, CommonProperties {
/**
* The format of the date to display in the input.
*/
format?: (date: DateValue[]) => string
format?: (date: DateValue) => string
/**
* The format of the date to display in the input.
*/
parse?: (value: string) => DateValue[]
parse?: (value: string) => DateValue
/**
* The view of the calendar
* @default "day"
Expand Down Expand Up @@ -217,7 +217,7 @@ type PrivateContext = Context<{
* @internal
* The input element's value
*/
inputValue: string
inputValue: string[]
/**
* @internal
* The current hovered date. Useful for range selection mode.
Expand Down Expand Up @@ -360,6 +360,10 @@ export interface ViewProps {
view?: DateView
}

export interface InputProps {
index?: number
}

export interface MonthGridProps {
columns?: number
format?: "short" | "long"
Expand Down Expand Up @@ -574,7 +578,7 @@ export interface MachineApi<T extends PropTypes = PropTypes> {
triggerProps: T["button"]
getViewTriggerProps(props?: ViewProps): T["button"]
getViewControlProps(props?: ViewProps): T["element"]
inputProps: T["input"]
getInputProps(props?: InputProps): T["input"]
monthSelectProps: T["select"]
yearSelectProps: T["select"]
}
Expand Down
12 changes: 6 additions & 6 deletions packages/machines/date-picker/src/date-picker.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function sortDates(values: DateValue[]) {
return values.sort((a, b) => a.compare(b))
}

export function formatValue(ctx: Pick<MachineContext, "locale" | "timeZone" | "selectionMode" | "value">) {
export function formatValue(ctx: Pick<MachineContext, "locale" | "timeZone" | "selectionMode" | "value">): string[] {
const formatter = new DateFormatter(ctx.locale, {
timeZone: ctx.timeZone,
day: "2-digit",
Expand All @@ -28,17 +28,17 @@ export function formatValue(ctx: Pick<MachineContext, "locale" | "timeZone" | "s

if (ctx.selectionMode === "range") {
const [startDate, endDate] = ctx.value
if (!startDate || !endDate) return ""
return `${formatter.format(startDate.toDate(ctx.timeZone))} - ${formatter.format(endDate.toDate(ctx.timeZone))}`
if (!startDate || !endDate) return []
return [formatter.format(startDate.toDate(ctx.timeZone)), formatter.format(endDate.toDate(ctx.timeZone))]
}

if (ctx.selectionMode === "single") {
const [startValue] = ctx.value
if (!startValue) return ""
return formatter.format(startValue.toDate(ctx.timeZone))
if (!startValue) return []
return [formatter.format(startValue.toDate(ctx.timeZone))]
}

return ctx.value.map((date) => formatter.format(date.toDate(ctx.timeZone))).join(", ")
return ctx.value.map((date) => formatter.format(date.toDate(ctx.timeZone)))
}

export function getNextTriggerLabel(view: DateView) {
Expand Down

0 comments on commit e68f731

Please sign in to comment.