diff --git a/package.json b/package.json
index e3e68255c..605761c4c 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,7 @@
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"prettier-plugin-tailwindcss": "0.5.13",
- "svelte": "^5.0.0-next.157",
+ "svelte": "5.0.0-next.164",
"svelte-eslint-parser": "^0.34.1",
"wrangler": "^3.44.0"
},
diff --git a/packages/bits-ui/other/setupTest.ts b/packages/bits-ui/other/setupTest.ts
index 9e565a609..e467f299b 100644
--- a/packages/bits-ui/other/setupTest.ts
+++ b/packages/bits-ui/other/setupTest.ts
@@ -90,5 +90,10 @@ vi.mock("$app/stores", (): typeof stores => {
// eslint-disable-next-line ts/no-require-imports
globalThis.ResizeObserver = require("resize-observer-polyfill");
Element.prototype.scrollIntoView = () => {};
-Element.prototype.hasPointerCapture = (() => {}) as any
+Element.prototype.hasPointerCapture = (() => {}) as any;
+// @ts-expect-error - shut it
+globalThis.window.CSS.supports = (property: string, value: string) => true;
+
+globalThis.document.elementsFromPoint = () => [];
+globalThis.document.elementFromPoint = () => null;
diff --git a/packages/bits-ui/package.json b/packages/bits-ui/package.json
index f0782f689..77523b51a 100644
--- a/packages/bits-ui/package.json
+++ b/packages/bits-ui/package.json
@@ -45,7 +45,7 @@
"jsdom": "^24.0.0",
"publint": "^0.2.7",
"resize-observer-polyfill": "^1.5.1",
- "svelte": "5.0.0-next.157",
+ "svelte": "5.0.0-next.164",
"svelte-check": "^3.6.9",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
diff --git a/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts b/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts
index 18251d3d4..16e2c3c5b 100644
--- a/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts
+++ b/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts
@@ -94,10 +94,6 @@ class CheckboxInputState {
constructor(root: CheckboxRootState) {
this.root = root;
-
- $effect(() => {
- console.log("shouldRender", this.shouldRender);
- });
}
props = $derived.by(
diff --git a/packages/bits-ui/src/lib/bits/date-field/components/date-field-input.svelte b/packages/bits-ui/src/lib/bits/date-field/components/date-field-input.svelte
index e7c224005..f3acadb42 100644
--- a/packages/bits-ui/src/lib/bits/date-field/components/date-field-input.svelte
+++ b/packages/bits-ui/src/lib/bits/date-field/components/date-field-input.svelte
@@ -1,35 +1,34 @@
{#if asChild}
-
+ {@render child?.({ props: mergedProps, segments: inputState.root.segmentContents })}
{:else}
-
-
+
+ {@render children?.({ segments: inputState.root.segmentContents })}
{/if}
diff --git a/packages/bits-ui/src/lib/bits/date-field/components/date-field-label.svelte b/packages/bits-ui/src/lib/bits/date-field/components/date-field-label.svelte
index fc5cca374..56999d294 100644
--- a/packages/bits-ui/src/lib/bits/date-field/components/date-field-label.svelte
+++ b/packages/bits-ui/src/lib/bits/date-field/components/date-field-label.svelte
@@ -1,34 +1,34 @@
{#if asChild}
-
+ {@render child?.({ props: mergedProps })}
{:else}
-
-
-
+
+ {@render children?.()}
+
{/if}
diff --git a/packages/bits-ui/src/lib/bits/date-field/components/date-field-segment.svelte b/packages/bits-ui/src/lib/bits/date-field/components/date-field-segment.svelte
index a033034b1..8744e1f13 100644
--- a/packages/bits-ui/src/lib/bits/date-field/components/date-field-segment.svelte
+++ b/packages/bits-ui/src/lib/bits/date-field/components/date-field-segment.svelte
@@ -1,45 +1,37 @@
{#if asChild}
-
+ {@render child?.({ props: mergedProps })}
{:else}
-
-
-
+
+ {@render children?.()}
+
{/if}
diff --git a/packages/bits-ui/src/lib/bits/date-field/components/date-field.svelte b/packages/bits-ui/src/lib/bits/date-field/components/date-field.svelte
index ddf24a8e2..cc8d18562 100644
--- a/packages/bits-ui/src/lib/bits/date-field/components/date-field.svelte
+++ b/packages/bits-ui/src/lib/bits/date-field/components/date-field.svelte
@@ -1,125 +1,71 @@
-
+{@render children?.()}
diff --git a/packages/bits-ui/src/lib/bits/date-field/ctx.ts b/packages/bits-ui/src/lib/bits/date-field/ctx.ts
deleted file mode 100644
index 53dfeffa8..000000000
--- a/packages/bits-ui/src/lib/bits/date-field/ctx.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { type CreateDateFieldProps, createDateField } from "@melt-ui/svelte";
-import { getContext, setContext } from "svelte";
-import { createBitAttrs, getOptionUpdater, removeUndefined } from "$lib/internal/index.js";
-
-export function getDateFieldData() {
- const NAME = "date-field" as const;
- const PARTS = ["label", "input", "segment"] as const;
-
- return {
- NAME,
- PARTS,
- };
-}
-
-type GetReturn = Omit
, "updateOption">;
-
-export function setCtx(props: CreateDateFieldProps) {
- const { NAME, PARTS } = getDateFieldData();
- const getAttrs = createBitAttrs(NAME, PARTS);
-
- const dateField = { ...createDateField(removeUndefined(props)), getAttrs };
- setContext(NAME, dateField);
-
- return {
- ...dateField,
- updateOption: getOptionUpdater(dateField.options),
- };
-}
-
-export function getCtx() {
- const { NAME } = getDateFieldData();
- return getContext(NAME);
-}
diff --git a/packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts b/packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts
new file mode 100644
index 000000000..98b6fb313
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts
@@ -0,0 +1,2158 @@
+import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js";
+import type { WithRefProps } from "$lib/internal/types.js";
+import { useRefById } from "$lib/internal/useRefById.svelte.js";
+import { getAnnouncer, type Announcer } from "$lib/shared/date/announcer.js";
+import {
+ areAllSegmentsFilled,
+ createContent,
+ getValueFromSegments,
+ inferGranularity,
+ initializeSegmentValues,
+ initSegmentStates,
+ isAcceptableSegmentKey,
+ isDateAndTimeSegmentObj,
+ isDateSegmentPart,
+ isFirstSegment,
+ removeDescriptionElement,
+ setDescription,
+} from "$lib/shared/date/field/helpers.js";
+import {
+ type DateAndTimeSegmentObj,
+ type DateSegmentObj,
+ type DateSegmentPart,
+ type SegmentValueObj,
+ type TimeSegmentObj,
+ type TimeSegmentPart,
+} from "$lib/shared/date/field/types.js";
+import { createFormatter, type Formatter } from "$lib/shared/date/formatter.js";
+import { getDaysInMonth, isBefore, toDate } from "$lib/shared/date/utils.js";
+import type { Updater } from "svelte/store";
+import type { DateValue } from "@internationalized/date";
+import type { WritableBox } from "svelte-toolbelt";
+import {
+ getAriaDisabled,
+ getAriaHidden,
+ getAriaInvalid,
+ getAriaReadonly,
+ getDataDisabled,
+ getDataInvalid,
+ getDataReadonly,
+} from "$lib/internal/attrs.js";
+import { isBrowser, isNumberString } from "$lib/internal/is.js";
+import { kbd } from "$lib/internal/kbd.js";
+import {
+ getFirstSegment,
+ handleSegmentNavigation,
+ isSegmentNavigationKey,
+ moveToNextSegment,
+ moveToPrevSegment,
+} from "$lib/shared/date/field.js";
+import type { SegmentPart } from "$lib/shared/index.js";
+import { DATE_SEGMENT_PARTS, TIME_SEGMENT_PARTS } from "$lib/shared/date/field/parts.js";
+import { untrack } from "svelte";
+import { createContext } from "$lib/internal/createContext.js";
+import { useId } from "$lib/internal/useId.svelte.js";
+import type { Granularity, Matcher } from "$lib/shared/date/types.js";
+
+type DateFieldRootStateProps = WritableBoxedValues<{
+ value: DateValue | undefined;
+ placeholder: DateValue;
+}> &
+ ReadableBoxedValues<{
+ readonlySegments: SegmentPart[];
+ isDateUnavailable: Matcher | undefined;
+ minValue: DateValue | undefined;
+ maxValue: DateValue | undefined;
+ disabled: boolean;
+ readonly: boolean;
+ granularity: Granularity | undefined;
+ hourCycle: 12 | 24 | undefined;
+ locale: string;
+ hideTimeZone: boolean;
+ name: string;
+ required: boolean;
+ }>;
+
+class DateFieldRootState {
+ value: DateFieldRootStateProps["value"];
+ placeholder: WritableBox;
+ isDateUnavailable: DateFieldRootStateProps["isDateUnavailable"];
+ minValue: DateFieldRootStateProps["minValue"];
+ maxValue: DateFieldRootStateProps["maxValue"];
+ disabled: DateFieldRootStateProps["disabled"];
+ readonly: DateFieldRootStateProps["readonly"];
+ granularity: DateFieldRootStateProps["granularity"];
+ readonlySegments: DateFieldRootStateProps["readonlySegments"];
+ hourCycle: DateFieldRootStateProps["hourCycle"];
+ locale: DateFieldRootStateProps["locale"];
+ hideTimeZone: DateFieldRootStateProps["hideTimeZone"];
+ name: DateFieldRootStateProps["name"];
+ required: DateFieldRootStateProps["required"];
+ descriptionId = useId();
+ formatter: Formatter;
+ initialSegments: SegmentValueObj;
+ segmentValues = $state() as SegmentValueObj;
+ announcer: Announcer;
+ readonlySegmentsSet = $derived.by(() => new Set(this.readonlySegments.value));
+ segmentStates = initSegmentStates();
+ fieldNode = $state(null);
+ labelNode = $state(null);
+ descriptionNode = $state(null);
+ validationNode = $state(null);
+ states = initSegmentStates();
+ dayPeriodNode = $state(null);
+
+ constructor(props: DateFieldRootStateProps) {
+ this.value = props.value;
+ this.placeholder = props.placeholder;
+ this.isDateUnavailable = props.isDateUnavailable;
+ this.minValue = props.minValue;
+ this.maxValue = props.maxValue;
+ this.disabled = props.disabled;
+ this.readonly = props.readonly;
+ this.granularity = props.granularity;
+ this.readonlySegments = props.readonlySegments;
+ this.hourCycle = props.hourCycle;
+ this.locale = props.locale;
+ this.hideTimeZone = props.hideTimeZone;
+ this.name = props.name;
+ this.required = props.required;
+ this.formatter = createFormatter(this.locale.value);
+ this.initialSegments = initializeSegmentValues(this.inferredGranularity);
+ this.segmentValues = this.initialSegments;
+
+ $effect(() => {
+ untrack(() => {
+ this.initialSegments = initializeSegmentValues(this.inferredGranularity);
+ });
+ });
+
+ $effect(() => {
+ this.announcer = getAnnouncer();
+ return () => {
+ removeDescriptionElement(this.descriptionId);
+ };
+ });
+ this.announcer = getAnnouncer();
+
+ $effect(() => {
+ if (this.formatter.getLocale() === this.locale.value) return;
+ this.formatter.setLocale(this.locale.value);
+ });
+
+ $effect(() => {
+ if (this.value.value) {
+ const descriptionId = untrack(() => this.descriptionId);
+ setDescription(descriptionId, this.formatter, this.value.value);
+ }
+ const placeholder = untrack(() => this.placeholder.value);
+ if (this.value.value && placeholder !== this.value.value) {
+ untrack(() => {
+ if (this.value.value) {
+ this.placeholder.value = this.value.value;
+ }
+ });
+ }
+ });
+
+ if (this.value.value) {
+ this.syncSegmentValues(this.value.value);
+ }
+
+ $effect(() => {
+ this.locale.value;
+ if (this.value.value) {
+ this.syncSegmentValues(this.value.value);
+ }
+
+ this.clearUpdating();
+ });
+ }
+
+ clearUpdating() {
+ this.states.day.updating = null;
+ this.states.month.updating = null;
+ this.states.year.updating = null;
+ this.states.hour.updating = null;
+ this.states.dayPeriod.updating = null;
+ }
+
+ setValue(value: DateValue | undefined) {
+ this.value.value = value;
+ }
+
+ syncSegmentValues(value: DateValue) {
+ const dateValues = DATE_SEGMENT_PARTS.map((part) => {
+ const partValue = value[part];
+
+ if (part === "month") {
+ if (this.states.month.updating) {
+ return [part, this.states.month.updating];
+ }
+ if (partValue < 10) {
+ return [part, `0${partValue}`];
+ }
+ }
+
+ if (part === "day") {
+ if (this.states.day.updating) {
+ return [part, this.states.day.updating];
+ }
+
+ if (partValue < 10) {
+ return [part, `0${partValue}`];
+ }
+ }
+
+ if (part === "year") {
+ if (this.states.year.updating) {
+ return [part, this.states.year.updating];
+ }
+ const valueDigits = `${partValue}`.length;
+ const diff = 4 - valueDigits;
+ if (diff > 0) {
+ return [part, `${"0".repeat(diff)}${partValue}`];
+ }
+ }
+
+ return [part, `${partValue}`];
+ });
+ if ("hour" in value) {
+ const timeValues = TIME_SEGMENT_PARTS.map((part) => {
+ if (part === "dayPeriod") {
+ if (this.states.dayPeriod.updating) {
+ return [part, this.states.dayPeriod.updating];
+ } else {
+ return [part, this.formatter.dayPeriod(toDate(value))];
+ }
+ } else if (part === "hour") {
+ if (value[part] === 0) {
+ /**
+ * If we're rendering a `dayPeriod` segment, we're operating in a
+ * 12-hour clock, so we never allow the displayed hour to be 0.
+ */
+ if (this.dayPeriodNode) {
+ return [part, "12"];
+ }
+ }
+ }
+ return [part, `${value[part]}`];
+ });
+
+ const mergedSegmentValues = [...dateValues, ...timeValues];
+ this.segmentValues = Object.fromEntries(mergedSegmentValues);
+ this.states.dayPeriod.updating = null;
+ return;
+ }
+
+ this.segmentValues = Object.fromEntries(dateValues);
+ }
+
+ isInvalid = $derived.by(() => {
+ const value = this.value.value;
+ if (!value) return false;
+ if (this.isDateUnavailable.value?.(value)) return true;
+ const minValue = this.minValue.value;
+ if (minValue && isBefore(value, minValue)) return true;
+ const maxValue = this.maxValue.value;
+ if (maxValue && isBefore(maxValue, value)) return true;
+ return false;
+ });
+
+ inferredGranularity = $derived.by(() => {
+ const granularity = this.granularity.value;
+ if (granularity) return granularity;
+ const inferred = inferGranularity(this.placeholder.value, this.granularity.value);
+ return inferred;
+ });
+
+ allSegmentContent = $derived.by(() =>
+ createContent({
+ segmentValues: this.segmentValues,
+ formatter: this.formatter,
+ locale: this.locale.value,
+ granularity: this.inferredGranularity,
+ dateRef: this.placeholder.value,
+ hideTimeZone: this.hideTimeZone.value,
+ hourCycle: this.hourCycle.value,
+ })
+ );
+
+ segmentContents = $derived.by(() => this.allSegmentContent.arr);
+
+ sharedSegmentAttrs = {
+ role: "spinbutton",
+ contenteditable: true,
+ tabindex: 0,
+ spellcheck: false,
+ inputmode: "numeric",
+ autocorrect: "off",
+ eterkeyhint: "next",
+ style: {
+ caretColor: "transparent",
+ },
+ };
+
+ getLabelledBy = (segmentId: string) => {
+ return `${segmentId} ${this.labelNode?.id ?? ""}`;
+ };
+
+ updateSegment = (
+ part: T,
+ cb: T extends DateSegmentPart
+ ? Updater
+ : T extends TimeSegmentPart
+ ? Updater
+ : Updater
+ ) => {
+ const disabled = this.disabled.value;
+ const readonly = this.readonly.value;
+ const readonlySegmentsSet = this.readonlySegmentsSet;
+ if (disabled || readonly || readonlySegmentsSet.has(part)) return;
+
+ const prev = this.segmentValues;
+
+ let newSegmentValues: SegmentValueObj = prev;
+
+ const dateRef = this.placeholder.value;
+ if (isDateAndTimeSegmentObj(prev)) {
+ const pVal = prev[part];
+ const castCb = cb as Updater;
+ if (part === "month") {
+ const next = castCb(pVal) as DateAndTimeSegmentObj["month"];
+ this.states.month.updating = next;
+ if (next !== null && prev.day !== null) {
+ const date = dateRef.set({ month: parseInt(next) });
+ const daysInMonth = getDaysInMonth(toDate(date));
+ const prevDay = parseInt(prev.day);
+ if (prevDay > daysInMonth) {
+ prev.day = `${daysInMonth}`;
+ }
+ }
+ newSegmentValues = { ...prev, [part]: next };
+ } else if (part === "dayPeriod") {
+ const next = castCb(pVal) as DateAndTimeSegmentObj["dayPeriod"];
+ this.states.dayPeriod.updating = next;
+ const date = this.value.value;
+ if (date && "hour" in date) {
+ const trueHour = date.hour;
+ if (next === "AM") {
+ if (trueHour >= 12) {
+ prev.hour = `${trueHour - 12}`;
+ }
+ } else if (next === "PM") {
+ if (trueHour < 12) {
+ prev.hour = `${trueHour + 12}`;
+ }
+ }
+ }
+ newSegmentValues = { ...prev, [part]: next };
+ } else if (part === "hour") {
+ const next = castCb(pVal) as DateAndTimeSegmentObj["hour"];
+ if (next !== null && prev.dayPeriod !== null) {
+ const dayPeriod = this.formatter.dayPeriod(
+ toDate(dateRef.set({ hour: parseInt(next) }))
+ );
+ if (dayPeriod === "AM" || dayPeriod === "PM") {
+ prev.dayPeriod = dayPeriod;
+ }
+ }
+ newSegmentValues = { ...prev, [part]: next };
+ } else if (part === "year") {
+ const next = castCb(pVal) as DateAndTimeSegmentObj["year"];
+ this.states.year.updating = next;
+ newSegmentValues = { ...prev, [part]: next };
+ } else if (part === "day") {
+ const next = castCb(pVal) as DateAndTimeSegmentObj["day"];
+ this.states.day.updating = next;
+ newSegmentValues = { ...prev, [part]: next };
+ } else {
+ const next = castCb(pVal);
+ newSegmentValues = { ...prev, [part]: next };
+ }
+ } else if (isDateSegmentPart(part)) {
+ const pVal = prev[part];
+ const castCb = cb as Updater;
+ const next = castCb(pVal);
+ if (part === "month" && next !== null && prev.day !== null) {
+ this.states.month.updating = next;
+ const date = dateRef.set({ month: parseInt(next) });
+ const daysInMonth = getDaysInMonth(toDate(date));
+ if (parseInt(prev.day) > daysInMonth) {
+ prev.day = `${daysInMonth}`;
+ }
+ newSegmentValues = { ...prev, [part]: next };
+ } else if (part === "year") {
+ const next = castCb(pVal) as DateAndTimeSegmentObj["year"];
+ this.states.year.updating = next;
+ newSegmentValues = { ...prev, [part]: next };
+ } else if (part === "day") {
+ const next = castCb(pVal) as DateAndTimeSegmentObj["day"];
+ this.states.day.updating = next;
+ newSegmentValues = { ...prev, [part]: next };
+ } else {
+ newSegmentValues = { ...prev, [part]: next };
+ }
+ }
+ this.segmentValues = newSegmentValues;
+ if (areAllSegmentsFilled(newSegmentValues, this.fieldNode)) {
+ this.setValue(
+ getValueFromSegments({
+ segmentObj: newSegmentValues,
+ fieldNode: this.fieldNode,
+ dateRef: this.placeholder.value,
+ })
+ );
+ } else {
+ this.setValue(undefined);
+ this.segmentValues = newSegmentValues;
+ }
+ };
+
+ handleSegmentClick = (e: MouseEvent) => {
+ if (this.disabled.value) {
+ e.preventDefault();
+ return;
+ }
+ };
+
+ getBaseSegmentAttrs = (part: SegmentPart, segmentId: string) => {
+ const inReadonlySegments = this.readonlySegmentsSet.has(part);
+ const defaultAttrs = {
+ "aria-invalid": getAriaInvalid(this.isInvalid),
+ "aria-disabled": getAriaDisabled(this.disabled.value),
+ "aria-readonly": getAriaReadonly(this.readonly.value || inReadonlySegments),
+ "data-invalid": getDataInvalid(this.isInvalid),
+ "data-disabled": getDataDisabled(this.disabled.value),
+ "data-segment": `${part}`,
+ };
+
+ if (part === "literal") return defaultAttrs;
+
+ const descriptionId = this.descriptionNode?.id;
+ const hasDescription = isFirstSegment(segmentId, this.fieldNode) && descriptionId;
+ const validationId = this.validationNode?.id;
+
+ const describedBy = hasDescription
+ ? `${descriptionId} ${this.isInvalid && validationId ? validationId : ""}`
+ : undefined;
+
+ const contenteditable =
+ this.readonly.value || inReadonlySegments || this.disabled.value ? false : undefined;
+
+ return {
+ ...defaultAttrs,
+ "aria-labelledby": this.getLabelledBy(segmentId),
+ contenteditable,
+ "aria-describedby": describedBy,
+ tabindex: this.disabled.value ? undefined : 0,
+ };
+ };
+
+ createInput(props: DateFieldInputStateProps) {
+ return new DateFieldInputState(props, this);
+ }
+
+ createLabel(props: DateFieldLabelStateProps) {
+ return new DateFieldLabelState(props, this);
+ }
+
+ createHiddenInput() {
+ return new DateFieldHiddenInputState(this);
+ }
+
+ createSegment(part: SegmentPart, props: WithRefProps) {
+ return segmentPartToInstance({
+ part,
+ segmentProps: props,
+ root: this,
+ });
+ }
+}
+
+type DateFieldInputStateProps = WithRefProps;
+
+class DateFieldInputState {
+ #id: DateFieldInputStateProps["id"];
+ #ref: DateFieldInputStateProps["ref"];
+ root: DateFieldRootState;
+
+ constructor(props: DateFieldInputStateProps, root: DateFieldRootState) {
+ this.#id = props.id;
+ this.#ref = props.ref;
+ this.root = root;
+
+ useRefById({
+ id: this.#id,
+ ref: this.#ref,
+ onRefChange: (node) => {
+ this.root.fieldNode = node;
+ },
+ });
+ }
+
+ #ariaDescribedBy = $derived.by(() => {
+ if (!isBrowser) return undefined;
+ const doesDescriptionExist = document.getElementById(this.root.descriptionId);
+ if (!doesDescriptionExist) return undefined;
+ return this.root.descriptionId;
+ });
+
+ props = $derived.by(
+ () =>
+ ({
+ id: this.#id.value,
+ role: "group",
+ "aria-labelledby": this.root.labelNode?.id ?? undefined,
+ "aria-describedby": this.#ariaDescribedBy,
+ "aria-disabled": getAriaDisabled(this.root.disabled.value),
+ "data-invalid": this.root.isInvalid ? "" : undefined,
+ "data-disabled": getDataDisabled(this.root.disabled.value),
+ }) as const
+ );
+}
+
+class DateFieldHiddenInputState {
+ #root: DateFieldRootState;
+ shouldRender = $derived.by(() => this.#root.name.value !== "");
+ isoValue = $derived.by(() => (this.#root.value.value ? this.#root.value.value.toString() : ""));
+
+ constructor(root: DateFieldRootState) {
+ this.#root = root;
+ }
+
+ props = $derived.by(() => {
+ return {
+ name: this.#root.name.value,
+ value: this.isoValue,
+ required: this.#root.required.value,
+ };
+ });
+}
+
+type DateFieldLabelStateProps = WithRefProps;
+
+class DateFieldLabelState {
+ #id: DateFieldLabelStateProps["id"];
+ #ref: DateFieldLabelStateProps["ref"];
+ #root: DateFieldRootState;
+
+ constructor(props: DateFieldLabelStateProps, root: DateFieldRootState) {
+ this.#id = props.id;
+ this.#ref = props.ref;
+ this.#root = root;
+
+ useRefById({
+ id: this.#id,
+ ref: this.#ref,
+ onRefChange: (node) => {
+ this.#root.labelNode = node;
+ },
+ });
+ }
+
+ #onclick = () => {
+ if (this.#root.disabled.value) return;
+ const firstSegment = getFirstSegment(this.#root.fieldNode);
+ if (!firstSegment) return;
+ firstSegment.focus();
+ };
+
+ props = $derived.by(
+ () =>
+ ({
+ id: this.#id.value,
+ "data-invalid": getDataInvalid(this.#root.isInvalid),
+ "data-disabled": getDataDisabled(this.#root.disabled.value),
+ onclick: this.#onclick,
+ }) as const
+ );
+}
+
+type DateFieldDaySegmentStateProps = WithRefProps;
+
+class DateFieldDaySegmentState {
+ #id: DateFieldDaySegmentStateProps["id"];
+ #ref: DateFieldDaySegmentStateProps["ref"];
+ #root: DateFieldRootState;
+ #announcer: Announcer;
+ #updateSegment: DateFieldRootState["updateSegment"];
+
+ constructor(props: DateFieldDaySegmentStateProps, root: DateFieldRootState) {
+ this.#id = props.id;
+ this.#ref = props.ref;
+ this.#root = root;
+ this.#announcer = this.#root.announcer;
+ this.#updateSegment = this.#root.updateSegment;
+
+ useRefById({
+ id: this.#id,
+ ref: this.#ref,
+ });
+ }
+
+ #onkeydown = (e: KeyboardEvent) => {
+ if (e.key !== kbd.TAB) e.preventDefault();
+ if (this.#root.disabled.value) return;
+ if (!isAcceptableSegmentKey(e.key)) return;
+
+ const segmentMonthValue = this.#root.segmentValues.month;
+ const placeholder = this.#root.placeholder.value;
+
+ const daysInMonth = segmentMonthValue
+ ? getDaysInMonth(placeholder.set({ month: parseInt(segmentMonthValue) }))
+ : getDaysInMonth(placeholder);
+
+ if (isArrowUp(e.key)) {
+ this.#updateSegment("day", (prev) => {
+ if (prev === null) {
+ const next = placeholder.day;
+ this.#announcer.announce(next);
+ return `${next}`;
+ }
+ const next = placeholder.set({ day: parseInt(prev) }).cycle("day", 1).day;
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+ return;
+ }
+ if (isArrowDown(e.key)) {
+ this.#updateSegment("day", (prev) => {
+ if (prev === null) {
+ const next = placeholder.day;
+ this.#announcer.announce(next);
+ return `${next}`;
+ }
+ const next = placeholder.set({ day: parseInt(prev) }).cycle("day", -1).day;
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+ return;
+ }
+
+ const fieldNode = this.#root.fieldNode;
+
+ if (isNumberString(e.key)) {
+ const num = parseInt(e.key);
+ let moveToNext = false;
+ this.#updateSegment("day", (prev) => {
+ const max = daysInMonth;
+ const maxStart = Math.floor(max / 10);
+ const numIsZero = num === 0;
+
+ /**
+ * If the user has left the segment, we want to reset the
+ * `prev` value so that we can start the segment over again
+ * when the user types a number.
+ */
+ if (this.#root.states.day.hasLeftFocus) {
+ prev = null;
+ this.#root.states.day.hasLeftFocus = false;
+ }
+
+ /**
+ * We are starting over in the segment if prev is null, which could
+ * happen in one of two scenarios:
+ * - the user has left the segment and then comes back to it
+ * - the segment was empty and the user begins typing a number
+ */
+ if (prev === null) {
+ /**
+ * If the user types a 0 as the first number, we want
+ * to keep track of that so that when they type the next
+ * number, we can move to the next segment.
+ */
+ if (numIsZero) {
+ this.#root.states.day.lastKeyZero = true;
+ this.#announcer.announce("0");
+ return "0";
+ }
+
+ ///////////////////////////
+
+ /**
+ * If the last key was a 0, or if the first number is
+ * greater than the max start digit (0-3 in most cases), then
+ * we want to move to the next segment, since it's not possible
+ * to continue typing a valid number in this segment.
+ */
+ if (this.#root.states.day.lastKeyZero || num > maxStart) {
+ moveToNext = true;
+ }
+
+ this.#root.states.day.lastKeyZero = false;
+
+ /**
+ * If we're moving to the next segment and the number is less than
+ * two digits, we want to announce the number and return it with a
+ * leading zero to follow the placeholder format of `MM/DD/YYYY`.
+ */
+ if (moveToNext && String(num).length === 1) {
+ this.#announcer.announce(num);
+ return `0${num}`;
+ }
+
+ /**
+ * If none of the above conditions are met, then we can just
+ * return the number as the segment value and continue typing
+ * in this segment.
+ */
+ return `${num}`;
+ }
+
+ /**
+ * If the number of digits is 2, or if the total with the existing digit
+ * and the pressed digit is greater than the maximum value for this
+ * month, then we will reset the segment as if the user had pressed the
+ * backspace key and then typed the number.
+ */
+ const total = parseInt(prev + num.toString());
+
+ if (this.#root.states.day.lastKeyZero) {
+ /**
+ * If the new number is not 0, then we reset the lastKeyZero state and
+ * move to the next segment, returning the new number with a leading 0.
+ */
+ if (num !== 0) {
+ moveToNext = true;
+ this.#root.states.day.lastKeyZero = false;
+ return `0${num}`;
+ }
+
+ /**
+ * If the new number is 0, then we simply return the previous value, since
+ * they didn't actually type a new number.
+ */
+ return prev;
+ }
+
+ /**
+ * If the total is greater than the max day value possible for this month, then
+ * we want to move to the next segment, trimming the first digit from the total,
+ * replacing it with a 0.
+ */
+ if (total > max) {
+ moveToNext = true;
+ return `0${num}`;
+ }
+
+ /**
+ * If the total has two digits and is less than or equal to the max day value,
+ * we will move to the next segment and return the total as the segment value.
+ */
+ moveToNext = true;
+ return `${total}`;
+ });
+
+ if (moveToNext) {
+ moveToNextSegment(e, fieldNode);
+ }
+ }
+
+ if (isBackspace(e.key)) {
+ let moveToPrev = false;
+ this.#updateSegment("day", (prev) => {
+ this.#root.states.day.hasLeftFocus = false;
+ if (prev === null) {
+ moveToPrev = true;
+ return null;
+ }
+ if (prev.length === 2 && prev.startsWith("0")) {
+ return null;
+ }
+ const str = prev.toString();
+ if (str.length === 1) return null;
+ return str.slice(0, -1);
+ });
+
+ if (moveToPrev) {
+ moveToPrevSegment(e, fieldNode);
+ }
+ }
+
+ if (isSegmentNavigationKey(e.key)) {
+ handleSegmentNavigation(e, fieldNode);
+ }
+ };
+
+ #onfocusout = () => {
+ this.#root.states.day.hasLeftFocus = true;
+ this.#updateSegment("month", (prev) => {
+ if (prev && prev.length === 1) {
+ return `0${prev}`;
+ }
+ return prev;
+ });
+ };
+
+ props = $derived.by(() => {
+ const segmentValues = this.#root.segmentValues;
+ const isEmpty = segmentValues.day === null;
+ const placeholder = this.#root.placeholder.value;
+ const date = segmentValues.day
+ ? placeholder.set({ day: parseInt(segmentValues.day) })
+ : placeholder;
+
+ const valueNow = date.day;
+ const valueMin = 1;
+ const valueMax = getDaysInMonth(toDate(date));
+ const valueText = isEmpty ? "Empty" : `${valueNow}`;
+
+ return {
+ ...this.#root.sharedSegmentAttrs,
+ id: this.#id.value,
+ "aria-label": "day,",
+ "aria-valuemin": valueMin,
+ "aria-valuemax": valueMax,
+ "aria-valuenow": valueNow,
+ "aria-valuetext": valueText,
+ onkeydown: this.#onkeydown,
+ onfocusout: this.#onfocusout,
+ onclick: this.#root.handleSegmentClick,
+ ...this.#root.getBaseSegmentAttrs("day", this.#id.value),
+ };
+ });
+}
+
+type DateFieldMonthSegmentStateProps = WithRefProps;
+
+class DateFieldMonthSegmentState {
+ #id: DateFieldMonthSegmentStateProps["id"];
+ #ref: DateFieldMonthSegmentStateProps["ref"];
+ #root: DateFieldRootState;
+ #announcer: Announcer;
+ #updateSegment: DateFieldRootState["updateSegment"];
+
+ constructor(props: DateFieldMonthSegmentStateProps, root: DateFieldRootState) {
+ this.#id = props.id;
+ this.#ref = props.ref;
+ this.#root = root;
+ this.#announcer = this.#root.announcer;
+ this.#updateSegment = this.#root.updateSegment;
+
+ useRefById({
+ id: this.#id,
+ ref: this.#ref,
+ });
+ }
+
+ getAnnouncement = (month: number) => {
+ return `${month} - ${this.#root.formatter.fullMonth(toDate(this.#root.placeholder.value.set({ month })))}`;
+ };
+
+ #onkeydown = (e: KeyboardEvent) => {
+ if (e.key !== kbd.TAB) e.preventDefault();
+ if (this.#root.disabled.value) return;
+ if (!isAcceptableSegmentKey(e.key)) return;
+
+ const placeholder = this.#root.placeholder.value;
+ const max = 12;
+
+ if (isArrowUp(e.key)) {
+ this.#updateSegment("month", (prev) => {
+ if (prev === null) {
+ const next = placeholder.month;
+ this.#announcer.announce(this.getAnnouncement(next));
+
+ if (String(next).length === 1) {
+ return `0${next}`;
+ }
+
+ return `${next}`;
+ }
+ const next = placeholder.set({ month: parseInt(prev) }).cycle("month", 1).month;
+ this.#announcer.announce(this.getAnnouncement(next));
+ if (String(next).length === 1) {
+ return `0${next}`;
+ }
+ return `${next}`;
+ });
+ return;
+ }
+
+ if (isArrowDown(e.key)) {
+ this.#updateSegment("month", (prev) => {
+ if (prev === null) {
+ const next = placeholder.month;
+ this.#announcer.announce(this.getAnnouncement(next));
+ if (String(next).length === 1) {
+ return `0${next}`;
+ }
+ return `${next}`;
+ }
+ const next = placeholder.set({ month: parseInt(prev) }).cycle("month", -1).month;
+ this.#announcer.announce(this.getAnnouncement(next));
+ if (String(next).length === 1) {
+ return `0${next}`;
+ }
+ return `${next}`;
+ });
+ return;
+ }
+
+ if (isNumberString(e.key)) {
+ const num = parseInt(e.key);
+ let moveToNext = false;
+
+ this.#updateSegment("month", (prev) => {
+ const maxStart = Math.floor(max / 10);
+ const numIsZero = num === 0;
+
+ /**
+ * If the user has left the segment, we want to reset the
+ * `prev` value so that we can start the segment over again
+ * when the user types a number.
+ */
+ if (this.#root.states.month.hasLeftFocus) {
+ prev = null;
+ this.#root.states.month.hasLeftFocus = false;
+ }
+
+ /**
+ * We are starting over in the segment if prev is null, which could
+ * happen in one of two scenarios:
+ * - the user has left the segment and then comes back to it
+ * - the segment was empty and the user begins typing a number
+ */
+ if (prev === null) {
+ /**
+ * If the user types a 0 as the first number, we want
+ * to keep track of that so that when they type the next
+ * number, we can move to the next segment.
+ */
+ if (numIsZero) {
+ this.#root.states.month.lastKeyZero = true;
+ this.#announcer.announce("0");
+ return "0";
+ }
+
+ ///////////////////////////
+
+ /**
+ * If the last key was a 0, or if the first number is
+ * greater than the max start digit (0-3 in most cases), then
+ * we want to move to the next segment, since it's not possible
+ * to continue typing a valid number in this segment.
+ */
+ if (this.#root.states.month.lastKeyZero || num > maxStart) {
+ moveToNext = true;
+ }
+
+ this.#root.states.month.lastKeyZero = false;
+
+ /**
+ * If we're moving to the next segment and the number is less than
+ * two digits, we want to announce the number and return it with a
+ * leading zero to follow the placeholder format of `MM/DD/YYYY`.
+ */
+ if (moveToNext && String(num).length === 1) {
+ this.#announcer.announce(num);
+ return `0${num}`;
+ }
+
+ /**
+ * If none of the above conditions are met, then we can just
+ * return the number as the segment value and continue typing
+ * in this segment.
+ */
+ return `${num}`;
+ }
+
+ /**
+ * If the number of digits is 2, or if the total with the existing digit
+ * and the pressed digit is greater than the maximum value for this
+ * month, then we will reset the segment as if the user had pressed the
+ * backspace key and then typed the number.
+ */
+ const total = parseInt(prev + num.toString());
+
+ if (this.#root.states.month.lastKeyZero) {
+ /**
+ * If the new number is not 0, then we reset the lastKeyZero state and
+ * move to the next segment, returning the new number with a leading 0.
+ */
+ if (num !== 0) {
+ moveToNext = true;
+ this.#root.states.month.lastKeyZero = false;
+ return `0${num}`;
+ }
+
+ /**
+ * If the new number is 0, then we simply return the previous value, since
+ * they didn't actually type a new number.
+ */
+ return prev;
+ }
+
+ /**
+ * If the total is greater than the max day value possible for this month, then
+ * we want to move to the next segment, trimming the first digit from the total,
+ * replacing it with a 0.
+ */
+ if (total > max) {
+ moveToNext = true;
+ return `0${num}`;
+ }
+
+ /**
+ * If the total has two digits and is less than or equal to the max day value,
+ * we will move to the next segment and return the total as the segment value.
+ */
+ moveToNext = true;
+ return `${total}`;
+ });
+
+ if (moveToNext) {
+ moveToNextSegment(e, this.#root.fieldNode);
+ }
+ }
+
+ if (isBackspace(e.key)) {
+ this.#root.states.month.hasLeftFocus = false;
+ let moveToPrev = false;
+ this.#updateSegment("month", (prev) => {
+ if (prev === null) {
+ this.#announcer.announce(null);
+ moveToPrev = true;
+ return null;
+ }
+
+ if (prev.length === 2 && prev.startsWith("0")) {
+ this.#announcer.announce(null);
+ return null;
+ }
+
+ const str = prev.toString();
+ if (str.length === 1) {
+ this.#announcer.announce(null);
+ return null;
+ }
+ const next = parseInt(str.slice(0, -1));
+ this.#announcer.announce(this.getAnnouncement(next));
+ return `${next}`;
+ });
+
+ if (moveToPrev) {
+ moveToPrevSegment(e, this.#root.fieldNode);
+ }
+ }
+
+ if (isSegmentNavigationKey(e.key)) {
+ handleSegmentNavigation(e, this.#root.fieldNode);
+ }
+ };
+
+ #onfocusout = () => {
+ this.#root.states.month.hasLeftFocus = true;
+ this.#updateSegment("month", (prev) => {
+ if (prev && prev.length === 1) {
+ return `0${prev}`;
+ }
+ return prev;
+ });
+ };
+
+ props = $derived.by(() => {
+ const segmentValues = this.#root.segmentValues;
+ const placeholder = this.#root.placeholder.value;
+ const isEmpty = segmentValues.month === null;
+ const date = segmentValues.month
+ ? placeholder.set({ month: parseInt(segmentValues.month) })
+ : placeholder;
+ const valueNow = date.month;
+ const valueMin = 1;
+ const valueMax = 12;
+ const valueText = isEmpty
+ ? "Empty"
+ : `${valueNow} - ${this.#root.formatter.fullMonth(toDate(date))}`;
+
+ return {
+ ...this.#root.sharedSegmentAttrs,
+ id: this.#id.value,
+ "aria-label": "month, ",
+ contenteditable: true,
+ "aria-valuemin": valueMin,
+ "aria-valuemax": valueMax,
+ "aria-valuenow": valueNow,
+ "aria-valuetext": valueText,
+ onkeydown: this.#onkeydown,
+ onfocusout: this.#onfocusout,
+ onclick: this.#root.handleSegmentClick,
+ ...this.#root.getBaseSegmentAttrs("month", this.#id.value),
+ } as const;
+ });
+}
+
+type DateFieldYearSegmentStateProps = WithRefProps;
+
+class DateFieldYearSegmentState {
+ #id: DateFieldYearSegmentStateProps["id"];
+ #ref: DateFieldYearSegmentStateProps["ref"];
+ #root: DateFieldRootState;
+ #announcer: Announcer;
+ #updateSegment: DateFieldRootState["updateSegment"];
+
+ /**
+ * When typing a year, a user may want to type `0090` to represent `90`.
+ * So we track the keys they've pressed in this specific interaction to
+ * determine once they've pressed four to move to the next segment.
+ *
+ * On `focusout` this is reset to an empty array.
+ */
+ #pressedKeys: string[] = [];
+
+ /**
+ * When a user re-enters a completed segment and backspaces, if they have
+ * leading zeroes on the year, they won't automatically be sent to the next
+ * segment even if they complete all 4 digits. This is because the leading zeroes
+ * get stripped out for the digit count.
+ *
+ * This lets us keep track of how many times the user has backspaced in a row
+ * to determine how many additional keypresses should move them to the next segment.
+ *
+ * For example, if the user has `0098` in the year segment and backspaces once,
+ * the segment will contain `009` and if the user types `7`, the segment should
+ * contain `0097` and move to the next segment.
+ *
+ * If the segment contains `0100` and the user backspaces twice, the segment will
+ * contain `01` and if the user types `2`, the segment should contain `012` and
+ * it should _not_ move to the next segment until the user types another digit.
+ */
+ #backspaceCount = 0;
+
+ constructor(props: DateFieldYearSegmentStateProps, root: DateFieldRootState) {
+ this.#id = props.id;
+ this.#ref = props.ref;
+ this.#root = root;
+ this.#announcer = this.#root.announcer;
+ this.#updateSegment = this.#root.updateSegment;
+
+ useRefById({
+ id: this.#id,
+ ref: this.#ref,
+ });
+ }
+
+ #resetBackspaceCount() {
+ this.#backspaceCount = 0;
+ }
+
+ #incrementBackspaceCount() {
+ this.#backspaceCount++;
+ }
+
+ #onkeydown = (e: KeyboardEvent) => {
+ if (e.key !== kbd.TAB) e.preventDefault();
+ if (this.#root.disabled.value || !isAcceptableSegmentKey(e.key)) return;
+
+ const placeholder = this.#root.placeholder.value;
+
+ if (isArrowUp(e.key)) {
+ this.#resetBackspaceCount();
+ this.#updateSegment("year", (prev) => {
+ if (prev === null) {
+ const next = placeholder.year;
+ this.#announcer.announce(next);
+ return `${next}`;
+ }
+ const next = placeholder.set({ year: parseInt(prev) }).cycle("year", 1).year;
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+ return;
+ }
+ if (isArrowDown(e.key)) {
+ this.#resetBackspaceCount();
+ this.#updateSegment("year", (prev) => {
+ if (prev === null) {
+ const next = placeholder.year;
+ this.#announcer.announce(next);
+ return `${next}`;
+ }
+ const next = placeholder.set({ year: parseInt(prev) }).cycle("year", -1).year;
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+ return;
+ }
+
+ if (isNumberString(e.key)) {
+ this.#pressedKeys.push(e.key);
+ let moveToNext = false;
+ const num = parseInt(e.key);
+ this.#updateSegment("year", (prev) => {
+ if (this.#root.states.year.hasLeftFocus) {
+ prev = null;
+ this.#root.states.year.hasLeftFocus = false;
+ }
+
+ if (prev === null) {
+ this.#announcer.announce(num);
+ return `000${num}`;
+ }
+
+ const str = prev.toString() + num.toString();
+ const mergedInt = parseInt(str);
+ const mergedIntDigits = String(mergedInt).length;
+
+ if (mergedIntDigits < 4) {
+ /**
+ * If the user has backspaced and hasn't typed enough digits to make up
+ * for the amount of backspaces, then we want to keep them in the segment
+ * and not prepend any zeroes to the number.
+ */
+ if (
+ this.#backspaceCount > 0 &&
+ this.#pressedKeys.length <= this.#backspaceCount &&
+ str.length <= 4
+ ) {
+ this.#announcer.announce(mergedInt);
+ return str;
+ }
+
+ /**
+ * If the mergedInt is less than 4 digits and we haven't backspaced,
+ * then we want to prepend zeroes to the number to keep the format
+ * of `YYYY`
+ */
+ this.#announcer.announce(mergedInt);
+ return prependYearZeros(mergedInt);
+ }
+
+ this.#announcer.announce(mergedInt);
+ moveToNext = true;
+ return `${mergedInt}`;
+ });
+
+ if (
+ this.#pressedKeys.length === 4 ||
+ this.#pressedKeys.length === this.#backspaceCount
+ ) {
+ moveToNext = true;
+ }
+
+ if (moveToNext) {
+ moveToNextSegment(e, this.#root.fieldNode);
+ }
+ }
+
+ if (isBackspace(e.key)) {
+ this.#pressedKeys = [];
+ this.#incrementBackspaceCount();
+ let moveToPrev = false;
+ this.#updateSegment("year", (prev) => {
+ this.#root.states.year.hasLeftFocus = false;
+ if (prev === null) {
+ moveToPrev = true;
+ this.#announcer.announce(null);
+ return null;
+ }
+ const str = prev.toString();
+ if (str.length === 1) {
+ this.#announcer.announce(null);
+ return null;
+ }
+ const next = str.slice(0, -1);
+ this.#announcer.announce(next);
+
+ return `${next}`;
+ });
+
+ if (moveToPrev) {
+ moveToPrevSegment(e, this.#root.fieldNode);
+ }
+ }
+
+ if (isSegmentNavigationKey(e.key)) {
+ handleSegmentNavigation(e, this.#root.fieldNode);
+ }
+ };
+
+ #onfocusout = () => {
+ this.#root.states.year.hasLeftFocus = true;
+ this.#pressedKeys = [];
+ this.#resetBackspaceCount();
+ this.#updateSegment("year", (prev) => {
+ if (prev && prev.length !== 4) {
+ return prependYearZeros(parseInt(prev));
+ }
+ return prev;
+ });
+ };
+
+ props = $derived.by(() => {
+ const segmentValues = this.#root.segmentValues;
+ const placeholder = this.#root.placeholder.value;
+ const isEmpty = segmentValues.year === null;
+ const date = segmentValues.year
+ ? placeholder.set({ year: parseInt(segmentValues.year) })
+ : placeholder;
+ const valueMin = 1;
+ const valueMax = 9999;
+ const valueNow = date.year;
+ const valueText = isEmpty ? "Empty" : `${valueNow}`;
+
+ return {
+ ...this.#root.sharedSegmentAttrs,
+ id: this.#id.value,
+ "aria-label": "year, ",
+ "aria-valuemin": valueMin,
+ "aria-valuemax": valueMax,
+ "aria-valuenow": valueNow,
+ "aria-valuetext": valueText,
+ onkeydown: this.#onkeydown,
+ onclick: this.#root.handleSegmentClick,
+ onfocusout: this.#onfocusout,
+ ...this.#root.getBaseSegmentAttrs("year", this.#id.value),
+ };
+ });
+}
+
+type DateFieldHourSegmentStateProps = WithRefProps;
+
+class DateFieldHourSegmentState {
+ #id: DateFieldHourSegmentStateProps["id"];
+ #ref: DateFieldHourSegmentStateProps["ref"];
+ #root: DateFieldRootState;
+ #announcer: Announcer;
+ #updateSegment: DateFieldRootState["updateSegment"];
+
+ constructor(props: DateFieldHourSegmentStateProps, root: DateFieldRootState) {
+ this.#id = props.id;
+ this.#ref = props.ref;
+ this.#root = root;
+ this.#announcer = this.#root.announcer;
+ this.#updateSegment = this.#root.updateSegment;
+
+ useRefById({
+ id: this.#id,
+ ref: this.#ref,
+ });
+ }
+
+ #onkeydown = (e: KeyboardEvent) => {
+ if (e.key !== kbd.TAB) e.preventDefault();
+ const placeholder = this.#root.placeholder.value;
+ if (
+ this.#root.disabled.value ||
+ !isAcceptableSegmentKey(e.key) ||
+ !("hour" in placeholder)
+ ) {
+ return;
+ }
+
+ const hourCycle = this.#root.hourCycle.value;
+
+ if (isArrowUp(e.key)) {
+ this.#updateSegment("hour", (prev) => {
+ if (prev === null) {
+ const next = placeholder.cycle("hour", 1, { hourCycle }).hour;
+ this.#announcer.announce(next);
+ return `${next}`;
+ }
+ const next = placeholder
+ .set({ hour: parseInt(prev) })
+ .cycle("hour", 1, { hourCycle }).hour;
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+ return;
+ }
+
+ if (isArrowDown(e.key)) {
+ this.#updateSegment("hour", (prev) => {
+ if (prev === null) {
+ const next = placeholder.cycle("hour", -1, { hourCycle }).hour;
+ this.#announcer.announce(next);
+ return `${next}`;
+ }
+ const next = placeholder
+ .set({ hour: parseInt(prev) })
+ .cycle("hour", -1, { hourCycle }).hour;
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+ return;
+ }
+
+ if (isNumberString(e.key)) {
+ const num = parseInt(e.key);
+ const max = 24;
+ const maxStart = Math.floor(max / 10);
+ let moveToNext = false;
+ this.#updateSegment("hour", (prev) => {
+ /**
+ * If the user has left the segment, we want to reset the
+ * `prev` value so that we can start the segment over again
+ * when the user types a number.
+ */
+ if (this.#root.states.hour.hasLeftFocus) {
+ prev = null;
+ this.#root.states.hour.hasLeftFocus = false;
+ }
+
+ if (prev === null) {
+ /**
+ * If the user types a 0 as the first number, we want
+ * to keep track of that so that when they type the next
+ * number, we can move to the next segment.
+ */
+ if (num === 0) {
+ this.#root.states.hour.lastKeyZero = true;
+ this.#announcer.announce(null);
+ return null;
+ }
+
+ /**
+ * If the last key was a 0, or if the first number is
+ * greater than the max start digit, then we want to move
+ * to the next segment, since it's not possible to continue
+ * typing a valid number in this segment.
+ */
+ if (this.#root.states.hour.lastKeyZero || num > maxStart) {
+ moveToNext = true;
+ }
+
+ this.#root.states.hour.lastKeyZero = false;
+ this.#announcer.announce(num);
+ /**
+ * If none of the above conditions are met, then we can just
+ * return the number as the segment value and continue typing
+ * in this segment.
+ */
+ return `${num}`;
+ }
+
+ const digits = prev.toString().length;
+ const total = parseInt(prev.toString() + num.toString());
+
+ /**
+ * If the number of digits is 2, or if the total with the existing digit
+ * and the pressed digit is greater than the maximum value, then we will
+ * reset the segment as if the user had pressed the backspace key and then
+ * typed a number.
+ */
+ if (digits === 2 || total > max) {
+ /**
+ * As we're doing elsewhere, we're checking if the number is greater
+ * than the max start digit, and if so, we're moving to the next segment.
+ */
+ if (num > maxStart) {
+ moveToNext = true;
+ }
+ this.#announcer.announce(num);
+ return `${num}`;
+ }
+ moveToNext = true;
+ this.#announcer.announce(total);
+ return `${total}`;
+ });
+
+ if (moveToNext) {
+ moveToNextSegment(e, this.#root.fieldNode);
+ }
+ }
+
+ if (isBackspace(e.key)) {
+ this.#root.states.hour.hasLeftFocus = false;
+ let moveToPrev = false;
+ this.#updateSegment("hour", (prev) => {
+ if (prev === null) {
+ this.#announcer.announce(null);
+ moveToPrev = true;
+ return null;
+ }
+ const str = prev.toString();
+ if (str.length === 1) {
+ this.#announcer.announce(null);
+ return null;
+ }
+ const next = parseInt(str.slice(0, -1));
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+
+ if (moveToPrev) {
+ moveToPrevSegment(e, this.#root.fieldNode);
+ }
+ }
+
+ if (isSegmentNavigationKey(e.key)) {
+ handleSegmentNavigation(e, this.#root.fieldNode);
+ }
+ };
+
+ #onfocusout = () => {
+ this.#root.states.hour.hasLeftFocus = true;
+ };
+
+ props = $derived.by(() => {
+ const segmentValues = this.#root.segmentValues;
+ const hourCycle = this.#root.hourCycle.value;
+ const placeholder = this.#root.placeholder.value;
+ if (!("hour" in segmentValues) || !("hour" in placeholder)) return {};
+ const isEmpty = segmentValues.hour === null;
+ const date = segmentValues.hour
+ ? placeholder.set({ hour: parseInt(segmentValues.hour) })
+ : placeholder;
+ const valueMin = hourCycle === 12 ? 1 : 0;
+ const valueMax = hourCycle === 12 ? 12 : 23;
+ const valueNow = date.hour;
+ const valueText = isEmpty ? "Empty" : `${valueNow} ${segmentValues.dayPeriod ?? ""}`;
+
+ return {
+ ...this.#root.sharedSegmentAttrs,
+ id: this.#id.value,
+ "aria-label": "hour, ",
+ "aria-valuemin": valueMin,
+ "aria-valuemax": valueMax,
+ "aria-valuenow": valueNow,
+ "aria-valuetext": valueText,
+ onkeydown: this.#onkeydown,
+ onfocusout: this.#onfocusout,
+ onclick: this.#root.handleSegmentClick,
+ ...this.#root.getBaseSegmentAttrs("hour", this.#id.value),
+ };
+ });
+}
+
+type DateFieldMinuteSegmentStateProps = WithRefProps;
+
+class DateFieldMinuteSegmentState {
+ #id: DateFieldMinuteSegmentStateProps["id"];
+ #ref: DateFieldMinuteSegmentStateProps["ref"];
+ #root: DateFieldRootState;
+ #announcer: Announcer;
+ #updateSegment: DateFieldRootState["updateSegment"];
+
+ constructor(props: DateFieldMinuteSegmentStateProps, root: DateFieldRootState) {
+ this.#id = props.id;
+ this.#ref = props.ref;
+ this.#root = root;
+ this.#announcer = this.#root.announcer;
+ this.#updateSegment = this.#root.updateSegment;
+
+ useRefById({
+ id: this.#id,
+ ref: this.#ref,
+ });
+ }
+
+ #onkeydown = (e: KeyboardEvent) => {
+ if (e.key !== kbd.TAB) e.preventDefault();
+ const placeholder = this.#root.placeholder.value;
+ if (
+ this.#root.disabled.value ||
+ !isAcceptableSegmentKey(e.key) ||
+ !("minute" in placeholder)
+ )
+ return;
+
+ const min = 0;
+ const max = 59;
+
+ if (isArrowUp(e.key)) {
+ this.#updateSegment("minute", (prev) => {
+ if (prev === null) {
+ this.#announcer.announce(min);
+ return `${min}`;
+ }
+ const next = placeholder.set({ minute: parseInt(prev) }).cycle("minute", 1).minute;
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+ return;
+ }
+
+ if (isArrowDown(e.key)) {
+ this.#updateSegment("minute", (prev) => {
+ if (prev === null) {
+ this.#announcer.announce(max);
+ return `${max}`;
+ }
+ const next = placeholder.set({ minute: parseInt(prev) }).cycle("minute", -1).minute;
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+ return;
+ }
+
+ if (isNumberString(e.key)) {
+ const num = parseInt(e.key);
+ let moveToNext = false;
+ this.#updateSegment("minute", (prev) => {
+ const maxStart = Math.floor(max / 10);
+
+ /**
+ * If the user has left the segment, we want to reset the
+ * `prev` value so that we can start the segment over again
+ * when the user types a number.
+ */
+ if (this.#root.states.minute.hasLeftFocus) {
+ prev = null;
+ this.#root.states.minute.hasLeftFocus = false;
+ }
+
+ if (prev === null) {
+ /**
+ * If the user types a 0 as the first number, we want
+ * to keep track of that so that when they type the next
+ * number, we can move to the next segment.
+ */
+ if (num === 0) {
+ this.#root.states.minute.lastKeyZero = true;
+ this.#announcer.announce(null);
+ return "0";
+ }
+
+ /**
+ * If the last key was a 0, or if the first number is
+ * greater than the max start digit, then we want to move
+ * to the next segment, since it's not possible to continue
+ * typing a valid number in this segment.
+ */
+ if (this.#root.states.minute.lastKeyZero || num > maxStart) {
+ moveToNext = true;
+ }
+
+ this.#root.states.minute.lastKeyZero = false;
+ this.#announcer.announce(num);
+ /**
+ * If none of the above conditions are met, then we can just
+ * return the number as the segment value and continue typing
+ * in this segment.
+ */
+ return `${num}`;
+ }
+
+ const digits = prev.length;
+ const total = parseInt(prev + num.toString());
+
+ /**
+ * If the number of digits is 2, or if the total with the existing digit
+ * and the pressed digit is greater than the maximum value, then we will
+ * reset the segment as if the user had pressed the backspace key and then
+ * typed a number.
+ */
+ if (digits === 2 || total > max) {
+ /**
+ * As we're doing elsewhere, we're checking if the number is greater
+ * than the max start digit, and if so, we're moving to the next segment.
+ */
+ if (num > maxStart) {
+ moveToNext = true;
+ }
+ this.#announcer.announce(num);
+ return `${num}`;
+ }
+ moveToNext = true;
+ this.#announcer.announce(total);
+ return `${total}`;
+ });
+
+ if (moveToNext) {
+ moveToNextSegment(e, this.#root.fieldNode);
+ }
+ return;
+ }
+
+ if (isBackspace(e.key)) {
+ this.#root.states.minute.hasLeftFocus = false;
+ let moveToPrev = false;
+ this.#updateSegment("minute", (prev) => {
+ if (prev === null) {
+ moveToPrev = true;
+ this.#announcer.announce("Empty");
+ return null;
+ }
+ const str = prev.toString();
+ if (str.length === 1) {
+ this.#announcer.announce("Empty");
+ return null;
+ }
+ const next = parseInt(str.slice(0, -1));
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+
+ if (moveToPrev) {
+ moveToPrevSegment(e, this.#root.fieldNode);
+ }
+ return;
+ }
+
+ if (isSegmentNavigationKey(e.key)) {
+ handleSegmentNavigation(e, this.#root.fieldNode);
+ }
+ };
+
+ #onfocusout = () => {
+ this.#root.states.minute.hasLeftFocus = true;
+ };
+
+ props = $derived.by(() => {
+ const segmentValues = this.#root.segmentValues;
+ const placeholder = this.#root.placeholder.value;
+
+ if (!("minute" in segmentValues) || !("minute" in placeholder)) return {};
+ const isEmpty = segmentValues.minute === null;
+ const date = segmentValues.minute
+ ? placeholder.set({ minute: parseInt(segmentValues.minute) })
+ : placeholder;
+ const valueNow = date.minute;
+ const valueMin = 0;
+ const valueMax = 59;
+ const valueText = isEmpty ? "Empty" : `${valueNow}`;
+
+ return {
+ ...this.#root.sharedSegmentAttrs,
+ id: this.#id.value,
+ "aria-label": "minute, ",
+ "aria-valuemin": valueMin,
+ "aria-valuemax": valueMax,
+ "aria-valuenow": valueNow,
+ "aria-valuetext": valueText,
+ onkeydown: this.#onkeydown,
+ onfocusout: this.#onfocusout,
+ onclick: this.#root.handleSegmentClick,
+ ...this.#root.getBaseSegmentAttrs("minute", this.#id.value),
+ };
+ });
+}
+
+type DateFieldSecondSegmentStateProps = WithRefProps;
+
+class DateFieldSecondSegmentState {
+ #id: DateFieldSecondSegmentStateProps["id"];
+ #ref: DateFieldSecondSegmentStateProps["ref"];
+ #root: DateFieldRootState;
+ #announcer: Announcer;
+ #updateSegment: DateFieldRootState["updateSegment"];
+
+ constructor(props: DateFieldSecondSegmentStateProps, root: DateFieldRootState) {
+ this.#id = props.id;
+ this.#ref = props.ref;
+ this.#root = root;
+ this.#announcer = this.#root.announcer;
+ this.#updateSegment = this.#root.updateSegment;
+
+ useRefById({
+ id: this.#id,
+ ref: this.#ref,
+ });
+ }
+
+ #onkeydown = (e: KeyboardEvent) => {
+ if (e.key !== kbd.TAB) e.preventDefault();
+ if (!isAcceptableSegmentKey(e.key) || this.#root.disabled.value) return;
+
+ const min = 0;
+ const max = 59;
+
+ const placeholder = this.#root.placeholder.value;
+
+ if (!("second" in placeholder)) return;
+
+ if (isArrowUp(e.key)) {
+ this.#updateSegment("second", (prev) => {
+ if (prev === null) {
+ this.#announcer.announce(min);
+ return `${min}`;
+ }
+ const next = placeholder.set({ second: parseInt(prev) }).cycle("second", 1).second;
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+ return;
+ }
+
+ if (isArrowDown(e.key)) {
+ this.#updateSegment("second", (prev) => {
+ if (prev === null) {
+ this.#announcer.announce(max);
+ return `${max}`;
+ }
+ const next = placeholder.set({ second: parseInt(prev) }).cycle("second", -1).second;
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+ return;
+ }
+
+ if (isNumberString(e.key)) {
+ const num = parseInt(e.key);
+ let moveToNext = false;
+ this.#updateSegment("second", (prev) => {
+ const maxStart = Math.floor(max / 10);
+
+ /**
+ * If the user has left the segment, we want to reset the
+ * `prev` value so that we can start the segment over again
+ * when the user types a number.
+ */
+ if (this.#root.states.second.hasLeftFocus) {
+ prev = null;
+ this.#root.states.second.hasLeftFocus = false;
+ }
+
+ if (prev === null) {
+ /**
+ * If the user types a 0 as the first number, we want
+ * to keep track of that so that when they type the next
+ * number, we can move to the next segment.
+ */
+ if (num === 0) {
+ this.#root.states.second.lastKeyZero = true;
+ this.#announcer.announce(null);
+ return "0";
+ }
+
+ /**
+ * If the last key was a 0, or if the first number is
+ * greater than the max start digit, then we want to move
+ * to the next segment, since it's not possible to continue
+ * typing a valid number in this segment.
+ */
+ if (this.#root.states.second.lastKeyZero || num > maxStart) {
+ moveToNext = true;
+ }
+ this.#root.states.second.lastKeyZero = false;
+
+ /**
+ * If none of the above conditions are met, then we can just
+ * return the number as the segment value and continue typing
+ * in this segment.
+ */
+ this.#announcer.announce(num);
+ return `${num}`;
+ }
+
+ const digits = prev.toString().length;
+ const total = parseInt(prev.toString() + num.toString());
+
+ /**
+ * If the number of digits is 2, or if the total with the existing digit
+ * and the pressed digit is greater than the maximum value, then we will
+ * reset the segment as if the user had pressed the backspace key and then
+ * typed a number.
+ */
+ if (digits === 2 || total > max) {
+ /**
+ * As we're doing elsewhere, we're checking if the number is greater
+ * than the max start digit, and if so, we're moving to the next segment.
+ */
+ if (num > maxStart) {
+ moveToNext = true;
+ }
+ this.#announcer.announce(num);
+ return `${num}`;
+ }
+ moveToNext = true;
+ this.#announcer.announce(total);
+ return `${total}`;
+ });
+
+ if (moveToNext) {
+ moveToNextSegment(e, this.#root.fieldNode);
+ }
+ }
+
+ if (isBackspace(e.key)) {
+ this.#root.states.second.hasLeftFocus = false;
+ let moveToPrev = false;
+ this.#updateSegment("second", (prev) => {
+ if (prev === null) {
+ moveToPrev = true;
+ this.#announcer.announce(null);
+ return null;
+ }
+ const str = prev.toString();
+ if (str.length === 1) {
+ this.#announcer.announce(null);
+ return null;
+ }
+ const next = parseInt(str.slice(0, -1));
+ this.#announcer.announce(next);
+ return `${next}`;
+ });
+
+ if (moveToPrev) {
+ moveToPrevSegment(e, this.#root.fieldNode);
+ }
+ }
+
+ if (isSegmentNavigationKey(e.key)) {
+ handleSegmentNavigation(e, this.#root.fieldNode);
+ }
+ };
+
+ #onfocusout = () => {
+ this.#root.states.second.hasLeftFocus = true;
+ };
+
+ props = $derived.by(() => {
+ const segmentValues = this.#root.segmentValues;
+ const placeholder = this.#root.placeholder.value;
+ if (!("second" in segmentValues) || !("second" in placeholder)) return {};
+ const isEmpty = segmentValues.second === null;
+ const date = segmentValues.second
+ ? placeholder.set({ second: parseInt(segmentValues.second) })
+ : placeholder;
+ const valueNow = date.second;
+ const valueMin = 0;
+ const valueMax = 59;
+ const valueText = isEmpty ? "Empty" : `${valueNow}`;
+
+ return {
+ ...this.#root.sharedSegmentAttrs,
+ id: this.#id.value,
+ "aria-label": "second, ",
+ "aria-valuemin": valueMin,
+ "aria-valuemax": valueMax,
+ "aria-valuenow": valueNow,
+ "aria-valuetext": valueText,
+ onkeydown: this.#onkeydown,
+ onfocusout: this.#onfocusout,
+ onclick: this.#root.handleSegmentClick,
+ ...this.#root.getBaseSegmentAttrs("second", this.#id.value),
+ };
+ });
+}
+
+type DateFieldDayPeriodSegmentStateProps = WithRefProps;
+
+class DateFieldDayPeriodSegmentState {
+ #id: DateFieldDayPeriodSegmentStateProps["id"];
+ #ref: DateFieldDayPeriodSegmentStateProps["ref"];
+ #root: DateFieldRootState;
+ #announcer: Announcer;
+ #updateSegment: DateFieldRootState["updateSegment"];
+
+ constructor(props: DateFieldMinuteSegmentStateProps, root: DateFieldRootState) {
+ this.#id = props.id;
+ this.#ref = props.ref;
+ this.#root = root;
+ this.#announcer = this.#root.announcer;
+ this.#updateSegment = this.#root.updateSegment;
+
+ useRefById({
+ id: this.#id,
+ ref: this.#ref,
+ onRefChange: (node) => {
+ this.#root.dayPeriodNode = node;
+ },
+ });
+ }
+
+ #onkeydown = (e: KeyboardEvent) => {
+ if (e.key !== kbd.TAB) e.preventDefault();
+ if (!isAcceptableDayPeriodKey(e.key) || this.#root.disabled.value) return;
+
+ if (isArrowUp(e.key) || isArrowDown(e.key)) {
+ this.#updateSegment("dayPeriod", (prev) => {
+ if (prev === "AM") {
+ const next = "PM";
+ this.#announcer.announce(next);
+ return next;
+ }
+ const next = "AM";
+ this.#announcer.announce(next);
+ return next;
+ });
+ return;
+ }
+
+ if (isBackspace(e.key)) {
+ this.#root.states.dayPeriod.hasLeftFocus = false;
+ this.#updateSegment("dayPeriod", () => {
+ const next = "AM";
+ this.#announcer.announce(next);
+ return next;
+ });
+ }
+
+ if (e.key === kbd.A || e.key === kbd.P) {
+ this.#updateSegment("dayPeriod", () => {
+ const next = e.key === kbd.A ? "AM" : "PM";
+ this.#announcer.announce(next);
+ return next;
+ });
+ }
+
+ if (isSegmentNavigationKey(e.key)) {
+ handleSegmentNavigation(e, this.#root.fieldNode);
+ }
+ };
+
+ props = $derived.by(() => {
+ const segmentValues = this.#root.segmentValues;
+ if (!("dayPeriod" in segmentValues)) return;
+
+ const valueMin = 0;
+ const valueMax = 12;
+ const valueNow = segmentValues.dayPeriod ?? 0;
+ const valueText = segmentValues.dayPeriod ?? "AM";
+
+ return {
+ ...this.#root.sharedSegmentAttrs,
+ id: this.#id.value,
+ inputmode: "text",
+ "aria-label": "AM/PM",
+ "aria-valuemin": valueMin,
+ "aria-valuemax": valueMax,
+ "aria-valuenow": valueNow,
+ "aria-valuetext": valueText,
+ onkeydown: this.#onkeydown,
+ onclick: this.#root.handleSegmentClick,
+ ...this.#root.getBaseSegmentAttrs("dayPeriod", this.#id.value),
+ };
+ });
+}
+
+type DateFieldDayLiteralSegmentStateProps = WithRefProps;
+
+class DateFieldDayLiteralSegmentState {
+ #id: DateFieldDayLiteralSegmentStateProps["id"];
+ #ref: DateFieldDayLiteralSegmentStateProps["ref"];
+ #root: DateFieldRootState;
+
+ constructor(props: DateFieldMinuteSegmentStateProps, root: DateFieldRootState) {
+ this.#id = props.id;
+ this.#ref = props.ref;
+ this.#root = root;
+
+ useRefById({
+ id: this.#id,
+ ref: this.#ref,
+ });
+ }
+
+ props = $derived.by(
+ () =>
+ ({
+ id: this.#id.value,
+ "aria-hidden": getAriaHidden(true),
+ ...this.#root.getBaseSegmentAttrs("literal", this.#id.value),
+ }) as const
+ );
+}
+
+type DateFieldTimeZoneSegmentStateProps = WithRefProps;
+
+class DateFieldTimeZoneSegmentState {
+ #id: DateFieldTimeZoneSegmentStateProps["id"];
+ #ref: DateFieldTimeZoneSegmentStateProps["ref"];
+ #root: DateFieldRootState;
+
+ constructor(props: DateFieldMinuteSegmentStateProps, root: DateFieldRootState) {
+ this.#id = props.id;
+ this.#ref = props.ref;
+ this.#root = root;
+
+ useRefById({
+ id: this.#id,
+ ref: this.#ref,
+ });
+ }
+
+ #onkeydown = (e: KeyboardEvent) => {
+ if (e.key !== kbd.TAB) e.preventDefault();
+ if (this.#root.disabled.value) return;
+ if (isSegmentNavigationKey(e.key)) {
+ handleSegmentNavigation(e, this.#root.fieldNode);
+ }
+ };
+
+ props = $derived.by(
+ () =>
+ ({
+ role: "textbox",
+ id: this.#id.value,
+ "aria-label": "timezone, ",
+ "data-readonly": getDataReadonly(true),
+ tabindex: 0,
+ style: {
+ caretColor: "transparent",
+ },
+ onkeydown: this.#onkeydown,
+ ...this.#root.getBaseSegmentAttrs("timeZoneName", this.#id.value),
+ }) as const
+ );
+}
+
+// Utils/helpers
+
+function isAcceptableDayPeriodKey(key: string) {
+ return isAcceptableSegmentKey(key) || key === kbd.A || key === kbd.P;
+}
+
+function isArrowUp(key: string) {
+ return key === kbd.ARROW_UP;
+}
+
+function isArrowDown(key: string) {
+ return key === kbd.ARROW_DOWN;
+}
+
+function isBackspace(key: string) {
+ return key === kbd.BACKSPACE;
+}
+
+const [setDateFieldRootContext, getDateFieldRootContext] =
+ createContext("DateField.Root");
+
+export function useDateFieldRoot(props: DateFieldRootStateProps) {
+ return setDateFieldRootContext(new DateFieldRootState(props));
+}
+
+export function useDateFieldInput(props: DateFieldInputStateProps) {
+ return getDateFieldRootContext().createInput(props);
+}
+
+export function useDateFieldHiddenInput() {
+ return getDateFieldRootContext().createHiddenInput();
+}
+
+export function useDateFieldSegment(part: SegmentPart, props: WithRefProps) {
+ return getDateFieldRootContext().createSegment(part, props);
+}
+
+export function useDateFieldLabel(props: DateFieldLabelStateProps) {
+ return getDateFieldRootContext().createLabel(props);
+}
+
+type SegmentPartToInstanceProps = {
+ part: SegmentPart;
+ root: DateFieldRootState;
+ segmentProps: WithRefProps;
+};
+
+function segmentPartToInstance(props: SegmentPartToInstanceProps) {
+ const { part, root, segmentProps } = props;
+ switch (part) {
+ case "day":
+ return new DateFieldDaySegmentState(segmentProps, root);
+ case "month":
+ return new DateFieldMonthSegmentState(segmentProps, root);
+ case "year":
+ return new DateFieldYearSegmentState(segmentProps, root);
+ case "hour":
+ return new DateFieldHourSegmentState(segmentProps, root);
+ case "minute":
+ return new DateFieldMinuteSegmentState(segmentProps, root);
+ case "second":
+ return new DateFieldSecondSegmentState(segmentProps, root);
+ case "dayPeriod":
+ return new DateFieldDayPeriodSegmentState(segmentProps, root);
+ case "literal":
+ return new DateFieldDayLiteralSegmentState(segmentProps, root);
+ case "timeZoneName":
+ return new DateFieldTimeZoneSegmentState(segmentProps, root);
+ }
+}
+
+function prependYearZeros(year: number) {
+ const digits = String(year).length;
+ const diff = 4 - digits;
+ return `${"0".repeat(diff)}${year}`;
+}
diff --git a/packages/bits-ui/src/lib/bits/date-field/index.ts b/packages/bits-ui/src/lib/bits/date-field/index.ts
index 48547f9f3..de30c1b86 100644
--- a/packages/bits-ui/src/lib/bits/date-field/index.ts
+++ b/packages/bits-ui/src/lib/bits/date-field/index.ts
@@ -4,10 +4,9 @@ export { default as Label } from "./components/date-field-label.svelte";
export { default as Segment } from "./components/date-field-segment.svelte";
export type {
- DateFieldProps as Props,
+ DateFieldRootProps as RootProps,
DateFieldInputProps as InputProps,
DateFieldLabelProps as LabelProps,
DateFieldSegmentProps as SegmentProps,
- DateFieldDescriptionProps as DescriptionProps,
- DateFieldSegmentEvents as SegmentEvents,
+ // DateFieldDescriptionProps as DescriptionProps,
} from "./types.js";
diff --git a/packages/bits-ui/src/lib/bits/date-field/types.ts b/packages/bits-ui/src/lib/bits/date-field/types.ts
index 4e9f3c118..bdd88beea 100644
--- a/packages/bits-ui/src/lib/bits/date-field/types.ts
+++ b/packages/bits-ui/src/lib/bits/date-field/types.ts
@@ -1,77 +1,164 @@
-import type { DateValue } from "@internationalized/date";
-import type { CreateDateFieldProps as MeltDateFieldProps } from "@melt-ui/svelte";
-import type { CustomEventHandler } from "$lib/index.js";
import type {
- DOMElement,
- Expand,
- HTMLDivAttributes,
- HTMLSpanAttributes,
- OmitDates,
OnChangeFn,
-} from "$lib/internal/index.js";
-import type { SegmentPart } from "$lib/shared/index.js";
-
-export type DateFieldPropsWithoutHTML = Expand<
- Omit, "required" | "name"> & {
- /**
- * The value of the date field.
- * You can bind this to a `DateValue` object to programmatically control the value.
- */
- value?: DateValue;
-
- /**
- * A callback function called when the value changes.
- */
- onValueChange?: OnChangeFn;
-
- /**
- * The placeholder date used to start the field.
- */
- placeholder?: DateValue;
-
- /**
- * A callback function called when the placeholder changes.
- */
- onPlaceholderChange?: OnChangeFn;
-
- /**
- * The id of the validation message element which is used to apply the
- * appropriate `aria-describedby` attribute to the input.
- */
- validationId?: string;
-
- /**
- * The id of the description element which is used to describe the input.
- * This is used to apply the appropriate `aria-describedby` attribute to the input.
- */
- descriptionId?: string;
- }
->;
-
-export type DateFieldInputPropsWithoutHTML = DOMElement;
-
-export type DateFieldDescriptionPropsWithoutHTML = DOMElement;
-
-export type DateFieldLabelPropsWithoutHTML = DOMElement;
+ PrimitiveDivAttributes,
+ PrimitiveSpanAttributes,
+ WithAsChild,
+ Without,
+} from "$lib/internal/types.js";
+import type { EditableSegmentPart } from "$lib/shared/date/field/types.js";
+import type { Granularity, Matcher } from "$lib/shared/date/types.js";
+import type { DateValue } from "@internationalized/date";
+import type { SegmentPart } from "@melt-ui/svelte";
+import type { Snippet } from "svelte";
+
+export type DateFieldRootPropsWithoutHTML = {
+ /**
+ * The value of the date field.
+ *
+ * @bindable
+ */
+ value?: DateValue | undefined;
+
+ /**
+ * A callback that is called when the date field value changes.
+ *
+ */
+ onValueChange?: OnChangeFn;
+
+ /**
+ * The placeholder value of the date field. This determines the format
+ * and what date the field starts at when it is empty.
+ *
+ * @bindable
+ */
+ placeholder?: DateValue | undefined;
+
+ /**
+ * A callback that is called when the date field's placeholder value changes.
+ */
+ onPlaceholderChange?: OnChangeFn;
+
+ /**
+ * A function that returns true if the given date is unavailable,
+ * where if selected, the date field will be marked as invalid.
+ */
+ isDateUnavailable?: Matcher;
+
+ /**
+ * The minimum acceptable date. When provided, the date field
+ * will be marked as invalid if the user enters a date before this date.
+ */
+ minValue?: DateValue | undefined;
+
+ /**
+ * The maximum acceptable date. When provided, the date field
+ * will be marked as invalid if the user enters a date after this date.
+ */
+ maxValue?: DateValue | undefined;
+
+ /**
+ * If true, the date field will be disabled and users will not be able
+ * to interact with it. This also disables the hidden input element if
+ * the date field is used in a form.
+ *
+ * @defaultValue false
+ */
+ disabled?: boolean;
+
+ /**
+ * If true, the date field will be readonly, and users will not be able to
+ * edit the values of any of the individual segments.
+ *
+ * @defaultValue false
+ */
+ readonly?: boolean;
+
+ /**
+ * An array of segment names that should be readonly. If provided, only the
+ * segments not in this array will be editable.
+ */
+ readonlySegments?: EditableSegmentPart[];
+
+ /**
+ * The format to use for displaying the time in the input.
+ * If using a 12 hour clock, ensure you also include the `dayPeriod`
+ * segment in your input to ensure the user can select AM/PM.
+ *
+ * @defaultValue the locale's default time format
+ */
+ hourCycle?: 12 | 24;
+
+ /**
+ * The locale to use for formatting the date field.
+ *
+ * @defaultValue 'en'
+ */
+ locale?: string;
+
+ /**
+ * The granularity of the date field. This determines which
+ * segments will be includes in the segments array used to
+ * build the date field.
+ *
+ * By default, when a `CalendarDate` value is used, the granularity
+ * will default to `'day'`, and when a `CalendarDateTime` or `ZonedDateTime`
+ * value is used, the granularity will default to `'minute'`.
+ *
+ * Granularity is only used for visual purposes, and does not impact
+ * the value of the date field. You can have the same value synced
+ * between multiple date fields with different granularities and they
+ * will all contain the same value.
+ *
+ * @defaultValue 'day'
+ */
+ granularity?: Granularity;
+
+ /**
+ * Whether or not to hide the timeZoneName segment from the date field.
+ *
+ * @defaultValue false;
+ */
+ hideTimeZone?: boolean;
+
+ /**
+ * The name to use for the hidden input element of the date field,
+ * which is used to submit the ISO string value of the date field
+ * to a server. If not provided, the hidden input element will not
+ * be rendered.
+ */
+ name?: string;
+
+ /**
+ * Whether or not the hidden input of the date field requires a value
+ * to be submitted.
+ *
+ * @defaultValue false
+ */
+ required?: boolean;
+
+ children?: Snippet;
+};
-export type DateFieldSegmentPropsWithoutHTML = Expand<
- {
- part: SegmentPart;
- } & DOMElement
->;
+export type DateFieldRootProps = DateFieldRootPropsWithoutHTML;
-export type DateFieldProps = DateFieldPropsWithoutHTML;
+export type DateFieldInputPropsWithoutHTML = Omit<
+ WithAsChild<{}, { segments: Array<{ part: SegmentPart; value: string }> }>,
+ "children"
+> & {
+ children?: Snippet<[{ segments: Array<{ part: SegmentPart; value: string }> }]>;
+};
-export type DateFieldLabelProps = DateFieldLabelPropsWithoutHTML & HTMLSpanAttributes;
+export type DateFieldInputProps = DateFieldInputPropsWithoutHTML &
+ Without;
-export type DateFieldSegmentProps = DateFieldSegmentPropsWithoutHTML & HTMLDivAttributes;
+export type DateFieldSegmentPropsWithoutHTML = WithAsChild<{
+ part: SegmentPart;
+}>;
-export type DateFieldInputProps = DateFieldInputPropsWithoutHTML & HTMLDivAttributes;
+export type DateFieldSegmentProps = DateFieldSegmentPropsWithoutHTML &
+ Without;
-export type DateFieldDescriptionProps = DateFieldDescriptionPropsWithoutHTML & HTMLDivAttributes;
+export type DateFieldLabelPropsWithoutHTML = WithAsChild<{}>;
-export type DateFieldSegmentEvents = {
- click: CustomEventHandler;
- focusout: CustomEventHandler;
- keydown: CustomEventHandler;
-};
+export type DateFieldLabelProps = DateFieldLabelPropsWithoutHTML &
+ Without;
diff --git a/packages/bits-ui/src/lib/bits/date-picker/ctx.ts b/packages/bits-ui/src/lib/bits/date-picker/ctx.ts
index 24a458808..e69de29bb 100644
--- a/packages/bits-ui/src/lib/bits/date-picker/ctx.ts
+++ b/packages/bits-ui/src/lib/bits/date-picker/ctx.ts
@@ -1,60 +0,0 @@
-import { type CreateDatePickerProps, createDatePicker } from "@melt-ui/svelte";
-import { getContext, setContext } from "svelte";
-import type { Writable } from "svelte/store";
-import { createBitAttrs, getOptionUpdater, removeUndefined } from "$lib/internal/index.js";
-import { getCalendarData } from "$lib/bits/calendar/ctx.js";
-import { getDateFieldData } from "$lib/bits/date-field/ctx.js";
-import { getPopoverData } from "$lib/bits/popover/ctx.js";
-import { getPositioningUpdater } from "$lib/bits/floating/helpers.js";
-import type { FloatingConfig } from "$lib/bits/floating/floating-config.js";
-import type { FloatingProps } from "$lib/bits/floating/_types.js";
-
-function getDatePickerData() {
- const NAME = "date-picker" as const;
- return {
- NAME,
- };
-}
-
-export function setCtx(props: CreateDatePickerProps) {
- const { NAME } = getDatePickerData();
- const { NAME: CALENDAR_NAME, PARTS: CALENDAR_PARTS } = getCalendarData();
- const getCalendarAttrs = createBitAttrs(CALENDAR_NAME, CALENDAR_PARTS);
- const { NAME: FIELD_NAME, PARTS: FIELD_PARTS } = getDateFieldData();
- const getFieldAttrs = createBitAttrs(FIELD_NAME, FIELD_PARTS);
- const { NAME: POPOVER_NAME, PARTS: POPOVER_PARTS } = getPopoverData();
- const getPopoverAttrs = createBitAttrs(POPOVER_NAME, POPOVER_PARTS);
-
- const datePicker = {
- ...createDatePicker({ ...removeUndefined(props), forceVisible: true }),
- getCalendarAttrs,
- getFieldAttrs,
- getPopoverAttrs,
- };
- const updateOption = getOptionUpdater(datePicker.options);
- setContext(NAME, { ...datePicker, updateOption });
-
- return {
- ...datePicker,
- updateOption,
- };
-}
-
-export function getCtx() {
- const { NAME } = getDatePickerData();
- return getContext>(NAME);
-}
-
-export function updatePositioning(props: FloatingProps) {
- const defaultPlacement = {
- side: "bottom",
- align: "center",
- } satisfies FloatingProps;
- const withDefaults = { ...defaultPlacement, ...props } satisfies FloatingProps;
- const {
- options: { positioning },
- } = getCtx();
-
- const updater = getPositioningUpdater(positioning as Writable);
- updater(withDefaults);
-}
diff --git a/packages/bits-ui/src/lib/bits/date-picker/index.ts b/packages/bits-ui/src/lib/bits/date-picker/index.ts
index fd071ff9e..f0c9409b7 100644
--- a/packages/bits-ui/src/lib/bits/date-picker/index.ts
+++ b/packages/bits-ui/src/lib/bits/date-picker/index.ts
@@ -1,52 +1,52 @@
-export { default as Arrow } from "./components/date-picker-arrow.svelte";
-export { default as Calendar } from "./components/date-picker-calendar.svelte";
-export { default as Close } from "./components/date-picker-close.svelte";
-export { default as Content } from "./components/date-picker-content.svelte";
-export { default as Field } from "./components/date-picker-field.svelte";
-export { default as Input } from "./components/date-picker-input.svelte";
-export { default as Label } from "./components/date-picker-label.svelte";
-export { default as Segment } from "./components/date-picker-segment.svelte";
-export { default as Trigger } from "./components/date-picker-trigger.svelte";
-export { default as Root } from "./components/date-picker.svelte";
-export { default as GridBody } from "./components/date-picker-grid-body.svelte";
-export { default as GridHead } from "./components/date-picker-grid-head.svelte";
-export { default as GridRow } from "./components/date-picker-grid-row.svelte";
-export { default as HeadCell } from "./components/date-picker-head-cell.svelte";
-export { default as Header } from "./components/date-picker-header.svelte";
-export { default as Cell } from "./components/date-picker-cell.svelte";
-export { default as Day } from "./components/date-picker-day.svelte";
-export { default as Grid } from "./components/date-picker-grid.svelte";
+// export { default as Arrow } from "./components/date-picker-arrow.svelte";
+// export { default as Calendar } from "./components/date-picker-calendar.svelte";
+// export { default as Close } from "./components/date-picker-close.svelte";
+// export { default as Content } from "./components/date-picker-content.svelte";
+// export { default as Field } from "./components/date-picker-field.svelte";
+// export { default as Input } from "./components/date-picker-input.svelte";
+// export { default as Label } from "./components/date-picker-label.svelte";
+// export { default as Segment } from "./components/date-picker-segment.svelte";
+// export { default as Trigger } from "./components/date-picker-trigger.svelte";
+// export { default as Root } from "./components/date-picker.svelte";
+// export { default as GridBody } from "./components/date-picker-grid-body.svelte";
+// export { default as GridHead } from "./components/date-picker-grid-head.svelte";
+// export { default as GridRow } from "./components/date-picker-grid-row.svelte";
+// export { default as HeadCell } from "./components/date-picker-head-cell.svelte";
+// export { default as Header } from "./components/date-picker-header.svelte";
+// export { default as Cell } from "./components/date-picker-cell.svelte";
+// export { default as Day } from "./components/date-picker-day.svelte";
+// export { default as Grid } from "./components/date-picker-grid.svelte";
-export { default as Heading } from "./components/date-picker-heading.svelte";
-export { default as NextButton } from "./components/date-picker-next-button.svelte";
-export { default as PrevButton } from "./components/date-picker-prev-button.svelte";
+// export { default as Heading } from "./components/date-picker-heading.svelte";
+// export { default as NextButton } from "./components/date-picker-next-button.svelte";
+// export { default as PrevButton } from "./components/date-picker-prev-button.svelte";
-export type {
- DatePickerProps as Props,
- DatePickerLabelProps as LabelProps,
- DatePickerInputProps as InputProps,
- DatePickerSegmentProps as SegmentProps,
- DatePickerArrowProps as ArrowProps,
- DatePickerCloseEvents as CloseEvents,
- DatePickerCloseProps as CloseProps,
- DatePickerContentProps as ContentProps,
- DatePickerTriggerEvents as TriggerEvents,
- DatePickerTriggerProps as TriggerProps,
- DatePickerCalendarEvents as CalendarEvents,
- DatePickerCalendarProps as CalendarProps,
- DatePickerCellProps as CellProps,
- DatePickerDayEvents as DayEvents,
- DatePickerDayProps as DayProps,
- DatePickerGridBodyProps as GridBodyProps,
- DatePickerGridHeadProps as GridHeadProps,
- DatePickerGridProps as GridProps,
- DatePickerGridRowProps as GridRowProps,
- DatePickerHeadCellProps as HeadCellProps,
- DatePickerHeaderProps as HeaderProps,
- DatePickerHeadingProps as HeadingProps,
- DatePickerNextButtonEvents as NextButtonEvents,
- DatePickerNextButtonProps as NextButtonProps,
- DatePickerPrevButtonEvents as PrevButtonEvents,
- DatePickerPrevButtonProps as PrevButtonProps,
- DatePickerSegmentEvents as SegmentEvents,
-} from "./types.js";
+// export type {
+// DatePickerProps as Props,
+// DatePickerLabelProps as LabelProps,
+// DatePickerInputProps as InputProps,
+// DatePickerSegmentProps as SegmentProps,
+// DatePickerArrowProps as ArrowProps,
+// DatePickerCloseEvents as CloseEvents,
+// DatePickerCloseProps as CloseProps,
+// DatePickerContentProps as ContentProps,
+// DatePickerTriggerEvents as TriggerEvents,
+// DatePickerTriggerProps as TriggerProps,
+// DatePickerCalendarEvents as CalendarEvents,
+// DatePickerCalendarProps as CalendarProps,
+// DatePickerCellProps as CellProps,
+// DatePickerDayEvents as DayEvents,
+// DatePickerDayProps as DayProps,
+// DatePickerGridBodyProps as GridBodyProps,
+// DatePickerGridHeadProps as GridHeadProps,
+// DatePickerGridProps as GridProps,
+// DatePickerGridRowProps as GridRowProps,
+// DatePickerHeadCellProps as HeadCellProps,
+// DatePickerHeaderProps as HeaderProps,
+// DatePickerHeadingProps as HeadingProps,
+// DatePickerNextButtonEvents as NextButtonEvents,
+// DatePickerNextButtonProps as NextButtonProps,
+// DatePickerPrevButtonEvents as PrevButtonEvents,
+// DatePickerPrevButtonProps as PrevButtonProps,
+// DatePickerSegmentEvents as SegmentEvents,
+// } from "./types.js";
diff --git a/packages/bits-ui/src/lib/bits/date-range-field/ctx.ts b/packages/bits-ui/src/lib/bits/date-range-field/ctx.ts
index 574516f4f..e69de29bb 100644
--- a/packages/bits-ui/src/lib/bits/date-range-field/ctx.ts
+++ b/packages/bits-ui/src/lib/bits/date-range-field/ctx.ts
@@ -1,24 +0,0 @@
-import { type CreateDateRangeFieldProps, createDateRangeField } from "@melt-ui/svelte";
-import { getContext, setContext } from "svelte";
-import { getDateFieldData } from "../date-field/ctx.js";
-import { createBitAttrs, getOptionUpdater, removeUndefined } from "$lib/internal/index.js";
-
-type GetReturn = Omit, "updateOption">;
-
-export function setCtx(props: CreateDateRangeFieldProps) {
- const { NAME, PARTS } = getDateFieldData();
- const getAttrs = createBitAttrs(NAME, PARTS);
- const dateRangeField = { ...createDateRangeField(removeUndefined(props)), getAttrs };
-
- setContext(NAME, dateRangeField);
-
- return {
- ...dateRangeField,
- updateOption: getOptionUpdater(dateRangeField.options),
- };
-}
-
-export function getCtx() {
- const { NAME } = getDateFieldData();
- return getContext(NAME);
-}
diff --git a/packages/bits-ui/src/lib/bits/date-range-field/index.ts b/packages/bits-ui/src/lib/bits/date-range-field/index.ts
index 80036ebd9..3d29d6682 100644
--- a/packages/bits-ui/src/lib/bits/date-range-field/index.ts
+++ b/packages/bits-ui/src/lib/bits/date-range-field/index.ts
@@ -1,13 +1,13 @@
-export { default as Root } from "./components/date-range-field.svelte";
-export { default as Input } from "./components/date-range-field-input.svelte";
-export { default as Label } from "./components/date-range-field-label.svelte";
-export { default as Segment } from "./components/date-range-field-segment.svelte";
+// export { default as Root } from "./components/date-range-field.svelte";
+// export { default as Input } from "./components/date-range-field-input.svelte";
+// export { default as Label } from "./components/date-range-field-label.svelte";
+// export { default as Segment } from "./components/date-range-field-segment.svelte";
-export type {
- DateRangeFieldProps as Props,
- DateRangeFieldLabelProps as LabelProps,
- DateRangeFieldInputProps as InputProps,
- DateRangeFieldSegmentProps as SegmentProps,
- //
- DateRangeFieldSegmentEvents as SegmentEvents,
-} from "./types.js";
+// export type {
+// DateRangeFieldProps as Props,
+// DateRangeFieldLabelProps as LabelProps,
+// DateRangeFieldInputProps as InputProps,
+// DateRangeFieldSegmentProps as SegmentProps,
+// //
+// DateRangeFieldSegmentEvents as SegmentEvents,
+// } from "./types.js";
diff --git a/packages/bits-ui/src/lib/bits/date-range-picker/ctx.ts b/packages/bits-ui/src/lib/bits/date-range-picker/ctx.ts
index a1595eb8c..e69de29bb 100644
--- a/packages/bits-ui/src/lib/bits/date-range-picker/ctx.ts
+++ b/packages/bits-ui/src/lib/bits/date-range-picker/ctx.ts
@@ -1,62 +0,0 @@
-import { type CreateDateRangePickerProps, createDateRangePicker } from "@melt-ui/svelte";
-import { getContext, setContext } from "svelte";
-import type { Writable } from "svelte/store";
-import { createBitAttrs, getOptionUpdater, removeUndefined } from "$lib/internal/index.js";
-import { getCalendarData } from "$lib/bits/calendar/ctx.js";
-import { getDateFieldData } from "$lib/bits/date-field/ctx.js";
-import { getPopoverData } from "$lib/bits/popover/ctx.js";
-import type { FloatingConfig } from "$lib/bits/floating/floating-config.js";
-import { getPositioningUpdater } from "$lib/bits/floating/helpers.js";
-import type { FloatingProps } from "$lib/bits/floating/_types.js";
-
-function getDateRangePickerData() {
- const NAME = "date-range-picker" as const;
- return {
- NAME,
- };
-}
-
-export function setCtx(props: CreateDateRangePickerProps) {
- const { NAME } = getDateRangePickerData();
-
- const { NAME: CALENDAR_NAME, PARTS: CALENDAR_PARTS } = getCalendarData();
- const getCalendarAttrs = createBitAttrs(CALENDAR_NAME, CALENDAR_PARTS);
- const { NAME: FIELD_NAME, PARTS: FIELD_PARTS } = getDateFieldData();
- const getFieldAttrs = createBitAttrs(FIELD_NAME, FIELD_PARTS);
- const { NAME: POPOVER_NAME, PARTS: POPOVER_PARTS } = getPopoverData();
- const getPopoverAttrs = createBitAttrs(POPOVER_NAME, POPOVER_PARTS);
-
- const dateRangePicker = {
- ...createDateRangePicker({ ...removeUndefined(props), forceVisible: true }),
- getCalendarAttrs,
- getFieldAttrs,
- getPopoverAttrs,
- };
- const updateOption = getOptionUpdater(dateRangePicker.options);
- setContext(NAME, { ...dateRangePicker, updateOption });
-
- return {
- ...dateRangePicker,
- updateOption,
- };
-}
-
-export function getCtx() {
- const { NAME } = getDateRangePickerData();
- return getContext>(NAME);
-}
-
-export function updatePositioning(props: FloatingProps) {
- const defaultPlacement = {
- side: "bottom",
- align: "center",
- } satisfies FloatingProps;
-
- const withDefaults = { ...defaultPlacement, ...props } satisfies FloatingProps;
- const {
- options: { positioning },
- } = getCtx();
-
- const updater = getPositioningUpdater(positioning as Writable);
- updater(withDefaults);
-}
diff --git a/packages/bits-ui/src/lib/bits/date-range-picker/index.ts b/packages/bits-ui/src/lib/bits/date-range-picker/index.ts
index 338eca2f1..937adca9c 100644
--- a/packages/bits-ui/src/lib/bits/date-range-picker/index.ts
+++ b/packages/bits-ui/src/lib/bits/date-range-picker/index.ts
@@ -1,55 +1,55 @@
-export { default as Arrow } from "./components/date-range-picker-arrow.svelte";
-export { default as Cell } from "./components/date-range-picker-cell.svelte";
-export { default as Day } from "./components/date-range-picker-day.svelte";
-export { default as Heading } from "./components/date-range-picker-heading.svelte";
-export { default as NextButton } from "./components/date-range-picker-next-button.svelte";
-export { default as PrevButton } from "./components/date-range-picker-prev-button.svelte";
-export { default as Calendar } from "./components/date-range-picker-calendar.svelte";
-export { default as Close } from "./components/date-range-picker-close.svelte";
-export { default as Content } from "./components/date-range-picker-content.svelte";
-export { default as Field } from "./components/date-range-picker-field.svelte";
-export { default as Input } from "./components/date-range-picker-input.svelte";
-export { default as Label } from "./components/date-range-picker-label.svelte";
-export { default as Segment } from "./components/date-range-picker-segment.svelte";
-export { default as Trigger } from "./components/date-range-picker-trigger.svelte";
-export { default as Root } from "./components/date-range-picker.svelte";
-export { default as Grid } from "./components/date-range-picker-grid.svelte";
-export { default as GridBody } from "./components/date-range-picker-grid-body.svelte";
-export { default as GridHead } from "./components/date-range-picker-grid-head.svelte";
-export { default as GridRow } from "./components/date-range-picker-grid-row.svelte";
-export { default as HeadCell } from "./components/date-range-picker-head-cell.svelte";
-export { default as Header } from "./components/date-range-picker-header.svelte";
+// export { default as Arrow } from "./components/date-range-picker-arrow.svelte";
+// export { default as Cell } from "./components/date-range-picker-cell.svelte";
+// export { default as Day } from "./components/date-range-picker-day.svelte";
+// export { default as Heading } from "./components/date-range-picker-heading.svelte";
+// export { default as NextButton } from "./components/date-range-picker-next-button.svelte";
+// export { default as PrevButton } from "./components/date-range-picker-prev-button.svelte";
+// export { default as Calendar } from "./components/date-range-picker-calendar.svelte";
+// export { default as Close } from "./components/date-range-picker-close.svelte";
+// export { default as Content } from "./components/date-range-picker-content.svelte";
+// export { default as Field } from "./components/date-range-picker-field.svelte";
+// export { default as Input } from "./components/date-range-picker-input.svelte";
+// export { default as Label } from "./components/date-range-picker-label.svelte";
+// export { default as Segment } from "./components/date-range-picker-segment.svelte";
+// export { default as Trigger } from "./components/date-range-picker-trigger.svelte";
+// export { default as Root } from "./components/date-range-picker.svelte";
+// export { default as Grid } from "./components/date-range-picker-grid.svelte";
+// export { default as GridBody } from "./components/date-range-picker-grid-body.svelte";
+// export { default as GridHead } from "./components/date-range-picker-grid-head.svelte";
+// export { default as GridRow } from "./components/date-range-picker-grid-row.svelte";
+// export { default as HeadCell } from "./components/date-range-picker-head-cell.svelte";
+// export { default as Header } from "./components/date-range-picker-header.svelte";
-export type {
- DateRangePickerProps as Props,
- DateRangePickerCalendarProps as CalendarProps,
- DateRangePickerLabelProps as LabelProps,
- DateRangePickerDescriptionProps as DescriptionProps,
- DateRangePickerInputProps as InputProps,
- DateRangePickerSegmentProps as SegmentProps,
- DateRangePickerCellProps as CellProps,
- DateRangePickerDayProps as DayProps,
- DateRangePickerGridBodyProps as GridBodyProps,
- DateRangePickerGridHeadProps as GridHeadProps,
- DateRangePickerGridRowProps as GridRowProps,
- DateRangePickerGridProps as GridProps,
- DateRangePickerHeadCellProps as HeadCellProps,
- DateRangePickerHeaderProps as HeaderProps,
- DateRangePickerHeadingProps as HeadingProps,
- DateRangePickerNextButtonProps as NextButtonProps,
- DateRangePickerPrevButtonProps as PrevButtonProps,
- DateRangePickerTriggerProps as TriggerProps,
- DateRangePickerContentProps as ContentProps,
- DateRangePickerArrowProps as ArrowProps,
- DateRangePickerCloseProps as CloseProps,
- //
- // Events
- //
- DateRangePickerCloseEvents as CloseEvents,
- DateRangePickerTriggerEvents as TriggerEvents,
- DateRangePickerCalendarEvents as CalendarEvents,
- DateRangePickerDayEvents as DayEvents,
- DateRangePickerPrevButtonEvents as PrevButtonEvents,
- DateRangePickerNextButtonEvents as NextButtonEvents,
- DateRangePickerSegmentEvents as SegmentEvents,
-} from "./types.js";
+// export type {
+// DateRangePickerProps as Props,
+// DateRangePickerCalendarProps as CalendarProps,
+// DateRangePickerLabelProps as LabelProps,
+// DateRangePickerDescriptionProps as DescriptionProps,
+// DateRangePickerInputProps as InputProps,
+// DateRangePickerSegmentProps as SegmentProps,
+// DateRangePickerCellProps as CellProps,
+// DateRangePickerDayProps as DayProps,
+// DateRangePickerGridBodyProps as GridBodyProps,
+// DateRangePickerGridHeadProps as GridHeadProps,
+// DateRangePickerGridRowProps as GridRowProps,
+// DateRangePickerGridProps as GridProps,
+// DateRangePickerHeadCellProps as HeadCellProps,
+// DateRangePickerHeaderProps as HeaderProps,
+// DateRangePickerHeadingProps as HeadingProps,
+// DateRangePickerNextButtonProps as NextButtonProps,
+// DateRangePickerPrevButtonProps as PrevButtonProps,
+// DateRangePickerTriggerProps as TriggerProps,
+// DateRangePickerContentProps as ContentProps,
+// DateRangePickerArrowProps as ArrowProps,
+// DateRangePickerCloseProps as CloseProps,
+// //
+// // Events
+// //
+// DateRangePickerCloseEvents as CloseEvents,
+// DateRangePickerTriggerEvents as TriggerEvents,
+// DateRangePickerCalendarEvents as CalendarEvents,
+// DateRangePickerDayEvents as DayEvents,
+// DateRangePickerPrevButtonEvents as PrevButtonEvents,
+// DateRangePickerNextButtonEvents as NextButtonEvents,
+// DateRangePickerSegmentEvents as SegmentEvents,
+// } from "./types.js";
diff --git a/packages/bits-ui/src/lib/bits/index.ts b/packages/bits-ui/src/lib/bits/index.ts
index 2905b49d7..ddf51b857 100644
--- a/packages/bits-ui/src/lib/bits/index.ts
+++ b/packages/bits-ui/src/lib/bits/index.ts
@@ -12,7 +12,7 @@ export * as Combobox from "./combobox/index.js";
export * as ContextMenu from "./context-menu/index.js";
export * as DateField from "./date-field/index.js";
// export * as DatePicker from "./date-picker/index.js";
-export * as DateRangeField from "./date-range-field/index.js";
+// export * as DateRangeField from "./date-range-field/index.js";
// export * as DateRangePicker from "./date-range-picker/index.js";
export * as Dialog from "./dialog/index.js";
export * as DropdownMenu from "./dropdown-menu/index.js";
diff --git a/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts b/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts
index 3837d6417..5fdff3b7b 100644
--- a/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts
+++ b/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts
@@ -339,7 +339,6 @@ class PinInputRootState {
};
#oninput = (e: Event & { currentTarget: HTMLInputElement }) => {
- console.log("new value", e.currentTarget.value);
const newValue = e.currentTarget.value.slice(0, this.#maxLength.value);
if (newValue.length > 0 && this.#regexPattern && !this.#regexPattern.test(newValue)) {
e.preventDefault();
@@ -369,7 +368,6 @@ class PinInputRootState {
this.#mirrorSelectionStart = start;
this.#mirrorSelectionEnd = end;
}
- console.log("input focus!");
this.#isFocused.value = true;
};
@@ -450,7 +448,6 @@ class PinInputRootState {
idx === this.#mirrorSelectionStart) ||
(idx >= this.#mirrorSelectionStart && idx < this.#mirrorSelectionEnd));
- console.log("isActive", isActive);
const char = this.value.value[idx] !== undefined ? this.value.value[idx] : null;
return {
diff --git a/packages/bits-ui/src/lib/bits/select/components/select.svelte b/packages/bits-ui/src/lib/bits/select/components/select.svelte
index bcb9c84f7..a4b20e2da 100644
--- a/packages/bits-ui/src/lib/bits/select/components/select.svelte
+++ b/packages/bits-ui/src/lib/bits/select/components/select.svelte
@@ -60,7 +60,7 @@
{#if value === ""}
{/if}
- {#each rootState.nativeOptionsArr as opt (opt.value.key)}
+ {#each rootState.nativeOptionsArr as opt, idx (opt.value.key + idx)}