Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 37 additions & 12 deletions packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
areAllSegmentsFilled,
createContent,
getDefaultHourCycle,
get24HourValueFromTypedHour,
getValueFromSegments,
inferGranularity,
initSegmentStates,
Expand Down Expand Up @@ -212,6 +213,7 @@ export class DateFieldRootState {
validationNode = $state<HTMLElement | null>(null);
states = initSegmentStates();
dayPeriodNode = $state<HTMLElement | null>(null);
hourInputDayPeriodHint: DateAndTimeSegmentObj["dayPeriod"] = null;
rangeRoot: DateRangeFieldRootState | undefined = undefined;
name = $state("");
domContext: DOMContext = new DOMContext(() => null);
Expand Down Expand Up @@ -617,12 +619,16 @@ export class DateFieldRootState {
const next = castCb(pVal) as DateAndTimeSegmentObj["hour"];
this.states.hour.updating = next;
if (next !== null && prev.dayPeriod !== null) {
const dayPeriod = this.formatter.dayPeriod(
toDate(dateRef.set({ hour: Number.parseInt(next) })),
this.hourCycle.current
);
if (dayPeriod === "AM" || dayPeriod === "PM") {
prev.dayPeriod = dayPeriod;
if (this.hourCycle.current !== 24 && this.hourInputDayPeriodHint !== null) {
prev.dayPeriod = this.hourInputDayPeriodHint;
} else {
const dayPeriod = this.formatter.dayPeriod(
toDate(dateRef.set({ hour: Number.parseInt(next) })),
this.hourCycle.current
);
if (dayPeriod === "AM" || dayPeriod === "PM") {
prev.dayPeriod = dayPeriod;
}
}
}
newSegmentValues = { ...prev, [part]: next };
Expand Down Expand Up @@ -672,9 +678,22 @@ export class DateFieldRootState {
}
this.segmentValues = newSegmentValues;
if (areAllSegmentsFilled(newSegmentValues, this.#fieldNode)) {
const segmentObjForValue =
"hour" in newSegmentValues &&
this.hourCycle.current !== 24 &&
this.hourInputDayPeriodHint !== null &&
newSegmentValues.hour !== null
? {
...newSegmentValues,
hour: get24HourValueFromTypedHour(
newSegmentValues.hour,
this.hourInputDayPeriodHint as "AM" | "PM"
),
}
: newSegmentValues;
this.setValue(
getValueFromSegments({
segmentObj: newSegmentValues,
segmentObj: segmentObjForValue,
fieldNode: this.#fieldNode,
dateRef: this.placeholder.current,
})
Expand Down Expand Up @@ -1322,12 +1341,19 @@ class DateFieldHourSegmentState extends BaseNumericSegmentState {

// Override to handle special hour logic
onkeydown(e: BitsKeyboardEvent) {
const oldUpdateSegment = this.root.updateSegment.bind(this.root);

// Add special handling for hour display with dayPeriod
if (isNumberString(e.key)) {
const oldUpdateSegment = this.root.updateSegment.bind(this.root);
this.root.hourInputDayPeriodHint =
this.root.hourCycle.current === 24
? null
: isDateAndTimeSegmentObj(this.root.segmentValues)
? this.root.segmentValues.dayPeriod
: null;
// oxlint-disable-next-line no-explicit-any
this.root.updateSegment = (part: any, cb: any) => {
const result = oldUpdateSegment(part, cb);
oldUpdateSegment(part, cb);

// After updating hour, check if we need to display "12" instead of "0"
if (part === "hour" && "hour" in this.root.segmentValues) {
Expand All @@ -1340,15 +1366,14 @@ class DateFieldHourSegmentState extends BaseNumericSegmentState {
this.root.segmentValues.hour = "12";
}
}

return result;
};
}

super.onkeydown(e);

// Restore original updateSegment
this.root.updateSegment = this.root.updateSegment.bind(this.root);
this.root.updateSegment = oldUpdateSegment;
this.root.hourInputDayPeriodHint = null;
}
}

Expand Down
24 changes: 24 additions & 0 deletions packages/bits-ui/src/lib/internal/date-time/field/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,21 @@ function getUsedSegments(fieldNode: HTMLElement | null) {
return usedSegments;
}

export function get24HourValueFromTypedHour(
hour: string,
dayPeriod: "AM" | "PM"
): string {
const parsedHour = Number.parseInt(hour);
if (Number.isNaN(parsedHour)) return hour;
if (dayPeriod === "AM") {
return parsedHour === 12 ? "0" : `${parsedHour}`;
}
if (dayPeriod === "PM") {
return parsedHour < 12 ? `${parsedHour + 12}` : `${parsedHour}`;
}
return hour;
}

type GetValueFromSegments = {
segmentObj: SegmentValueObj;
fieldNode: HTMLElement | null;
Expand All @@ -305,6 +320,15 @@ export function getValueFromSegments(props: GetValueFromSegments) {
if ("hour" in segmentObj) {
const value = segmentObj[part];
if (isNull(value)) continue;
if (part === "dayPeriod") continue;
if (part === "hour" && !isNull(segmentObj.dayPeriod)) {
const hour24 = get24HourValueFromTypedHour(
value,
segmentObj.dayPeriod as "AM" | "PM"
);
date = date.set({ hour: Number.parseInt(hour24) });
continue;
}
date = date.set({ [part]: segmentObj[part] });
} else if (isDateSegmentPart(part)) {
const value = segmentObj[part];
Expand Down
32 changes: 32 additions & 0 deletions tests/src/tests/date-field/date-field.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,38 @@ describe("date field", () => {
await expect.element(getDayPeriod()).toHaveTextContent("AM");
});

it("should preserve the PM day period when typing the hour in 12h mode", async () => {
setup({
value: new CalendarDateTime(2026, 3, 11, 14, 0, 0, 0),
});
const { getHour, getDayPeriod } = getTimeSegments(page.getByTestId);

await expect.element(getDayPeriod()).toHaveTextContent("PM");
await expect.element(page.getByTestId("value")).toHaveTextContent("2026-03-11T14:00");

await getHour().click();
await userEvent.keyboard(`{1}`);

await expect.element(getDayPeriod()).toHaveTextContent("PM");
await expect.element(page.getByTestId("value")).toHaveTextContent("2026-03-11T13:00");
});

it("should preserve the AM day period when typing the hour in 12h mode", async () => {
setup({
value: new CalendarDateTime(2026, 3, 11, 2, 0, 0, 0),
});
const { getHour, getDayPeriod } = getTimeSegments(page.getByTestId);

await expect.element(getDayPeriod()).toHaveTextContent("AM");
await expect.element(page.getByTestId("value")).toHaveTextContent("2026-03-11T02:00");

await getHour().click();
await userEvent.keyboard(`{1}`);

await expect.element(getDayPeriod()).toHaveTextContent("AM");
await expect.element(page.getByTestId("value")).toHaveTextContent("2026-03-11T01:00");
});

it("should never allow the hour to be 0 when in a 12 hour cycle", async () => {
setup({
value: new CalendarDateTime(2023, 10, 12, 12, 30, 0, 0),
Expand Down