Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
88c58d4
feat: implement segmets getter
isBatak Aug 23, 2025
c00b050
feat: wip on segments
isBatak Aug 26, 2025
5738bac
Merge branch 'main' into feat/date-picker-segments
isBatak Aug 27, 2025
53f04c7
feat: implement placeholder
isBatak Aug 27, 2025
f75ca3a
feat: fill in segments on value select
isBatak Aug 28, 2025
e7af282
feat: working on key events
isBatak Aug 28, 2025
aa7df7a
feat: replace local date formatter with global reference
isBatak Aug 29, 2025
23ec242
Merge branch 'main' into feat/date-picker-segments
isBatak Aug 29, 2025
1cd6fe2
chore: fix comments
isBatak Aug 29, 2025
bd18470
feat: complete segment value increment/decrement functionality
isBatak Aug 30, 2025
53215e9
chore: some fixes
isBatak Aug 31, 2025
ed687a3
feat: implement placeholderValue and reset segments on value clear
isBatak Sep 2, 2025
1d1840c
feat: fix markValid
isBatak Sep 8, 2025
cf81627
feat: wip on backspace
isBatak Sep 8, 2025
4111e34
Merge branch 'main' into feat/date-picker-segments
isBatak Sep 10, 2025
6bb7dc7
chore: remove extra pages
isBatak Sep 10, 2025
3fb2682
feat: wip on segment delete
isBatak Sep 10, 2025
41b7307
chore: remove console log
isBatak Sep 10, 2025
c622441
Merge branch 'main' into feat/date-picker-segments
isBatak Sep 13, 2025
7ff3396
refactor(date-picker): simplify key handling and segment validation l…
isBatak Sep 13, 2025
8730fd3
feat: initial segment input support
isBatak Sep 13, 2025
0333c8c
Merge branch 'main' into feat/date-picker-segments
isBatak Sep 18, 2025
2c2a990
Merge branch 'main' into feat/date-picker-segments
isBatak Sep 20, 2025
f553a3d
feat: improve focus management
isBatak Sep 20, 2025
ca53ef0
chore: updates
isBatak Sep 20, 2025
5fb170c
feat: fix input issures
isBatak Sep 20, 2025
9d4cc93
feat: clear placeholder data
isBatak Sep 22, 2025
3b2138f
Merge branch 'main' into feat/date-picker-segments
isBatak Sep 27, 2025
e693147
feat: implement home and end key actions
isBatak Sep 27, 2025
b2658c6
feat: prevent default behavior for paste events in date picker input
isBatak Sep 27, 2025
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
154 changes: 154 additions & 0 deletions examples/next-ts/pages/date-picker-segment-single.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import * as datePicker from "@zag-js/date-picker"
import { normalizeProps, useMachine } from "@zag-js/react"
import { datePickerControls } from "@zag-js/shared"
import { useId } from "react"
import { StateVisualizer } from "../components/state-visualizer"
import { Toolbar } from "../components/toolbar"
import { useControls } from "../hooks/use-controls"

export default function Page() {
const controls = useControls(datePickerControls)
const service = useMachine(datePicker.machine, {
id: useId(),
selectionMode: "single",
...controls.context,
})

const api = datePicker.connect(service, normalizeProps)

return (
<>
<main className="date-picker">
<div>
<button>Outside Element</button>
</div>
<p>{`Visible range: ${api.visibleRangeText.formatted}`}</p>

<output className="date-output">
<div>Selected: {api.valueAsString ?? "-"}</div>
<div>Focused: {api.focusedValueAsString}</div>
<div>Placeholder: {api.placeholderValueAsString}</div>
</output>

<div {...api.getControlProps()}>
<div {...api.getSegmentGroupProps()}>
{api.getSegments().map((segment, i) => (
<span key={i} {...api.getSegmentProps({ segment, index: i })}>
{segment.text}
</span>
))}
</div>
<button {...api.getClearTriggerProps()}>❌</button>
<button {...api.getTriggerProps()}>🗓</button>
</div>

<input type="datetime-local" placeholder="mm/dd/yyyy" />

<div {...api.getPositionerProps()}>
<div {...api.getContentProps()}>
<div style={{ marginBottom: "20px" }}>
<select {...api.getMonthSelectProps()}>
{api.getMonths().map((month, i) => (
<option key={i} value={month.value} disabled={month.disabled}>
{month.label}
</option>
))}
</select>

<select {...api.getYearSelectProps()}>
{api.getYears().map((year, i) => (
<option key={i} value={year.value} disabled={year.disabled}>
{year.label}
</option>
))}
</select>
</div>

<div hidden={api.view !== "day"}>
<div {...api.getViewControlProps({ view: "year" })}>
<button {...api.getPrevTriggerProps()}>Prev</button>
<button {...api.getViewTriggerProps()}>{api.visibleRangeText.start}</button>
<button {...api.getNextTriggerProps()}>Next</button>
</div>

<table {...api.getTableProps({ view: "day" })}>
<thead {...api.getTableHeaderProps({ view: "day" })}>
<tr {...api.getTableRowProps({ view: "day" })}>
{api.weekDays.map((day, i) => (
<th scope="col" key={i} aria-label={day.long}>
{day.narrow}
</th>
))}
</tr>
</thead>
<tbody {...api.getTableBodyProps({ view: "day" })}>
{api.weeks.map((week, i) => (
<tr key={i} {...api.getTableRowProps({ view: "day" })}>
{week.map((value, i) => (
<td key={i} {...api.getDayTableCellProps({ value })}>
<div {...api.getDayTableCellTriggerProps({ value })}>{value.day}</div>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>

<div style={{ display: "flex", gap: "40px" }}>
<div hidden={api.view !== "month"} style={{ width: "100%" }}>
<div {...api.getViewControlProps({ view: "month" })}>
<button {...api.getPrevTriggerProps({ view: "month" })}>Prev</button>
<button {...api.getViewTriggerProps({ view: "month" })}>{api.visibleRange.start.year}</button>
<button {...api.getNextTriggerProps({ view: "month" })}>Next</button>
</div>

<table {...api.getTableProps({ view: "month", columns: 4 })}>
<tbody {...api.getTableBodyProps({ view: "month" })}>
{api.getMonthsGrid({ columns: 4, format: "short" }).map((months, row) => (
<tr key={row} {...api.getTableRowProps()}>
{months.map((month, index) => (
<td key={index} {...api.getMonthTableCellProps({ ...month, columns: 4 })}>
<div {...api.getMonthTableCellTriggerProps({ ...month, columns: 4 })}>{month.label}</div>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>

<div hidden={api.view !== "year"} style={{ width: "100%" }}>
<div {...api.getViewControlProps({ view: "year" })}>
<button {...api.getPrevTriggerProps({ view: "year" })}>Prev</button>
<span>
{api.getDecade().start} - {api.getDecade().end}
</span>
<button {...api.getNextTriggerProps({ view: "year" })}>Next</button>
</div>

<table {...api.getTableProps({ view: "year", columns: 4 })}>
<tbody {...api.getTableBodyProps()}>
{api.getYearsGrid({ columns: 4 }).map((years, row) => (
<tr key={row} {...api.getTableRowProps({ view: "year" })}>
{years.map((year, index) => (
<td key={index} {...api.getYearTableCellProps({ ...year, columns: 4 })}>
<div {...api.getYearTableCellTriggerProps({ ...year, columns: 4 })}>{year.label}</div>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>

<Toolbar viz controls={controls.ui}>
<StateVisualizer state={service} omit={["weeks"]} />
</Toolbar>
</>
)
}
68 changes: 68 additions & 0 deletions packages/machines/date-picker/LOGIC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
## DatePicker Segment Focus Logic

- The date-picker component has three states: `IDLE`, `FOCUSED`, and `OPEN`

- In the `IDLE` state:

- When a segment group is focused:
- Transition to the `FOCUSED` state
- Set the active index and active segment index
- Focus the first editable segment

- In the `FOCUSED` state:

- When a segment group is blurred:
- Transition to the `IDLE` state
- Reset the active segment index to -1
- Reset the active index to start

- When arrow right key is pressed:
- Move to the next editable segment
- If at the last segment, move to the first segment of next input (range mode)

- When arrow left key is pressed:
- Move to the previous editable segment
- If at the first segment, move to the last segment of previous input (range mode)

- When arrow up key is pressed:
- Increment the current segment value (day, month, year)
- Constrain the value within valid bounds
- Update the date value

- When arrow down key is pressed:
- Decrement the current segment value (day, month, year)
- Constrain the value within valid bounds
- Update the date value

- When a digit is typed:
- Update the current segment value
- If segment is complete, move to next segment
- Parse and validate the date

- When backspace is pressed:
- Clear the current segment value
- Move to previous segment if current is empty

- When delete is pressed:
- Clear the current segment value
- Stay on the current segment

- When enter is pressed:
- Parse the complete input value
- Set the focused date if valid
- Select the focused date

- When escape is pressed:
- Revert to the last valid value
- Blur the segment group

- When text is pasted:
- Parse the pasted value as a complete date
- Update all segments if valid
- Focus the last segment

- In the `OPEN` state:

- Segment interactions are disabled
- Arrow keys control calendar navigation instead
- Typing characters may trigger date search/filter
2 changes: 2 additions & 0 deletions packages/machines/date-picker/src/date-picker.anatomy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export const anatomy = createAnatomy("date-picker").parts(
"content",
"control",
"input",
"segmentGroup",
"segment",
"label",
"monthSelect",
"nextTrigger",
Expand Down
Loading
Loading