-
-
Notifications
You must be signed in to change notification settings - Fork 230
feat: date picker segments #2671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
88c58d4
c00b050
5738bac
53f04c7
f75ca3a
e7af282
aa7df7a
23ec242
1cd6fe2
bd18470
53215e9
ed687a3
1d1840c
cf81627
4111e34
6bb7dc7
3fb2682
41b7307
c622441
7ff3396
8730fd3
0333c8c
2c2a990
f553a3d
ca53ef0
5fb170c
9d4cc93
3b2138f
e693147
b2658c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
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(), | ||
locale: "en", | ||
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> | ||
</output> | ||
|
||
<div {...api.getControlProps()}> | ||
<div {...api.getSegmentInputProps()}> | ||
{api.getSegments().map((segment, i) => ( | ||
<span key={i} {...api.getSegmentProps({ segment })}> | ||
{segment.text} | ||
</span> | ||
))} | ||
</div> | ||
<button {...api.getClearTriggerProps()}>❌</button> | ||
<button {...api.getTriggerProps()}>🗓</button> | ||
</div> | ||
|
||
<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> | ||
</> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,10 +30,11 @@ import type { | |
TableCellProps, | ||
TableCellState, | ||
TableProps, | ||
SegmentProps, | ||
SegmentState, | ||
} from "./date-picker.types" | ||
import { | ||
adjustStartAndEndDate, | ||
defaultTranslations, | ||
ensureValidCharacters, | ||
getInputPlaceholder, | ||
getLocaleSeparator, | ||
|
@@ -79,7 +80,7 @@ export function connect<T extends PropTypes>( | |
}) | ||
|
||
const separator = getLocaleSeparator(locale) | ||
const translations = { ...defaultTranslations, ...prop("translations") } | ||
const translations = prop("translations") | ||
|
||
function getMonthWeeks(from = startValue) { | ||
const numOfWeeks = prop("fixedWeeks") ? 6 : undefined | ||
|
@@ -223,6 +224,11 @@ export function connect<T extends PropTypes>( | |
return [view, id].filter(Boolean).join(" ") | ||
} | ||
|
||
function getSegmentState(props: SegmentProps): SegmentState { | ||
const {} = props | ||
return {} | ||
} | ||
|
||
return { | ||
focused, | ||
open, | ||
|
@@ -703,6 +709,7 @@ export function connect<T extends PropTypes>( | |
"data-state": open ? "open" : "closed", | ||
"aria-haspopup": "grid", | ||
disabled, | ||
"data-readonly": dataAttr(readOnly), | ||
onClick(event) { | ||
if (event.defaultPrevented) return | ||
if (!interactive) return | ||
|
@@ -803,6 +810,138 @@ export function connect<T extends PropTypes>( | |
}) | ||
}, | ||
|
||
getSegmentInputProps() { | ||
|
||
return normalize.element({ | ||
...parts.segmentInput.attrs, | ||
id: dom.getInputId(scope, 0), // FIXIT: figure out the index | ||
dir: prop("dir"), | ||
"data-state": open ? "open" : "closed", | ||
role: "presentation", | ||
readOnly, | ||
disabled, | ||
style: { | ||
unicodeBidi: "isolate", | ||
}, | ||
}) | ||
}, | ||
|
||
getSegments(props = {}) { | ||
const { index = 0 } = props | ||
return computed("segments")[index] ?? [] | ||
}, | ||
|
||
getSegmentState, | ||
|
||
getSegmentProps(props) { | ||
const { segment } = props | ||
const isEditable = !disabled && !readOnly && segment.isEditable | ||
|
||
if (segment.type === "literal") { | ||
return normalize.element({ | ||
...parts.segment.attrs, | ||
dir: prop("dir"), | ||
"aria-hidden": true, | ||
"data-type": segment.type, | ||
"data-readonly": dataAttr(true), | ||
"data-disabled": dataAttr(true), | ||
}) | ||
} | ||
|
||
return normalize.element({ | ||
...parts.segment.attrs, | ||
dir: prop("dir"), | ||
role: "spinbutton", | ||
tabIndex: disabled ? undefined : 0, | ||
autoComplete: "off", | ||
spellCheck: isEditable ? "false" : undefined, | ||
autoCorrect: isEditable ? "off" : undefined, | ||
contentEditable: isEditable, | ||
suppressContentEditableWarning: isEditable, | ||
inputMode: | ||
disabled || segment.type === "dayPeriod" || segment.type === "era" || !isEditable ? undefined : "numeric", | ||
enterKeyHint: "next", | ||
"aria-labelledby": dom.getInputId(scope, 0), // FIXIT: figure out the index | ||
|
||
// "aria-label": translations.segmentLabel(segment), | ||
"aria-valuenow": segment.value, | ||
"aria-valuetext": segment.text, | ||
"aria-valuemin": segment.minValue, | ||
"aria-valuemax": segment.maxValue, | ||
"aria-readonly": ariaAttr(!segment.isEditable || readOnly), | ||
"aria-disabled": ariaAttr(disabled), | ||
"data-value": segment.value, | ||
"data-type": segment.type, | ||
"data-readonly": dataAttr(!segment.isEditable || readOnly), | ||
"data-disabled": dataAttr(disabled), | ||
"data-editable": dataAttr(segment.isEditable && !readOnly && !disabled), | ||
"data-placeholder": dataAttr(segment.isPlaceholder), | ||
style: { | ||
"caret-color": "transparent", | ||
|
||
}, | ||
onKeyDown(event) { | ||
if ( | ||
event.defaultPrevented || | ||
event.ctrlKey || | ||
event.metaKey || | ||
event.shiftKey || | ||
event.altKey || | ||
readOnly || | ||
event.nativeEvent.isComposing | ||
) { | ||
return | ||
} | ||
|
||
const keyMap: EventKeyMap = { | ||
Enter() { | ||
send({ type: "SEGMENT.ENTER", focus: true }) | ||
}, | ||
ArrowLeft() { | ||
send({ type: "SEGMENT.ARROW_LEFT", focus: true }) | ||
}, | ||
ArrowRight() { | ||
send({ type: "SEGMENT.ARROW_RIGHT", focus: true }) | ||
}, | ||
ArrowUp() { | ||
send({ type: "SEGMENT.ARROW_UP", segment, focus: true }) | ||
}, | ||
ArrowDown() { | ||
send({ type: "SEGMENT.ARROW_DOWN", segment, focus: true }) | ||
}, | ||
PageUp(event) { | ||
send({ type: "SEGMENT.PAGE_UP", larger: event.shiftKey, focus: true }) | ||
}, | ||
PageDown(event) { | ||
send({ type: "SEGMENT.PAGE_DOWN", larger: event.shiftKey, focus: true }) | ||
}, | ||
Home() { | ||
send({ type: "SEGMENT.HOME", focus: true }) | ||
}, | ||
End() { | ||
send({ type: "SEGMENT.END", focus: true }) | ||
}, | ||
Comment on lines
942
to
979
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After you get this working, I wonder if we can bundle these into a |
||
} | ||
|
||
const exec = | ||
keyMap[ | ||
getEventKey(event, { | ||
dir: prop("dir"), | ||
}) | ||
] | ||
|
||
if (exec) { | ||
exec(event) | ||
event.preventDefault() | ||
event.stopPropagation() | ||
} | ||
}, | ||
onPointerDown(event) { | ||
event.stopPropagation() | ||
}, | ||
onMouseDown(event) { | ||
event.stopPropagation() | ||
}, | ||
}) | ||
}, | ||
|
||
getMonthSelectProps() { | ||
return normalize.select({ | ||
...parts.monthSelect.attrs, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's rename this part to segmentGroup instead of segmentInput (since it's not technically an input)