From 9e9c411453a6666e1d47e01604469861746c2cc5 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Mon, 18 Nov 2024 14:17:00 -0500 Subject: [PATCH 1/2] fix: date field 24 hour cycling --- .changeset/early-mails-visit.md | 5 ++ .../lib/bits/date-field/date-field.svelte.ts | 35 +++++++++++-- .../lib/internal/date-time/field/helpers.ts | 4 +- .../src/lib/internal/date-time/formatter.ts | 4 +- .../src/tests/date-field/date-field.test.ts | 50 +++++++++++++++++++ 5 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 .changeset/early-mails-visit.md diff --git a/.changeset/early-mails-visit.md b/.changeset/early-mails-visit.md new file mode 100644 index 000000000..d07747169 --- /dev/null +++ b/.changeset/early-mails-visit.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix: DateField 24 hour cycling 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 index 199a0ae88..3be2fadba 100644 --- 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 @@ -486,7 +486,8 @@ export class DateFieldRootState { this.states.hour.updating = next; if (next !== null && prev.dayPeriod !== null) { const dayPeriod = this.formatter.dayPeriod( - toDate(dateRef.set({ hour: Number.parseInt(next) })) + toDate(dateRef.set({ hour: Number.parseInt(next) })), + this.hourCycle.current ); if (dayPeriod === "AM" || dayPeriod === "PM") { prev.dayPeriod = dayPeriod; @@ -1506,6 +1507,12 @@ class DateFieldHourSegmentState { this.#announcer.announce("12"); return "12"; } + + if (next === 0 && this.#root.hourCycle.current === 24) { + this.#announcer.announce("00"); + return "00"; + } + this.#announcer.announce(next); return `${next}`; }); @@ -1532,6 +1539,12 @@ class DateFieldHourSegmentState { this.#announcer.announce("12"); return "12"; } + + if (next === 0 && this.#root.hourCycle.current === 24) { + this.#announcer.announce("00"); + return "00"; + } + this.#announcer.announce(next); return `${next}`; }); @@ -1541,10 +1554,12 @@ class DateFieldHourSegmentState { if (isNumberString(e.key)) { const num = Number.parseInt(e.key); const max = - "dayPeriod" in this.#root.segmentValues && - this.#root.segmentValues.dayPeriod !== null - ? 12 - : 23; + this.#root.hourCycle.current === 24 + ? 23 + : "dayPeriod" in this.#root.segmentValues && + this.#root.segmentValues.dayPeriod !== null + ? 12 + : 23; const maxStart = Math.floor(max / 10); let moveToNext = false; const numIsZero = num === 0; @@ -1628,6 +1643,16 @@ class DateFieldHourSegmentState { return `0${num}`; } + /** + * If the new number is 0 and the hour cycle is set to 24, then we move + * to the next segment, returning the new number with a leading 0. + */ + if (num === 0 && this.#root.hourCycle.current === 24) { + moveToNext = true; + this.#root.states.hour.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. diff --git a/packages/bits-ui/src/lib/internal/date-time/field/helpers.ts b/packages/bits-ui/src/lib/internal/date-time/field/helpers.ts index dbf3978a2..bdc6ceefa 100644 --- a/packages/bits-ui/src/lib/internal/date-time/field/helpers.ts +++ b/packages/bits-ui/src/lib/internal/date-time/field/helpers.ts @@ -89,7 +89,7 @@ function createContentObj(props: CreateContentObjProps) { return "0"; } else if (!isNull(value) && !isNull(intValue)) { const formatted = formatter.part(dateRef.set({ [part]: value }), part, { - hourCycle: props.hourCycle === 24 ? "h24" : undefined, + hourCycle: props.hourCycle === 24 ? "h23" : undefined, }); /** @@ -220,7 +220,7 @@ function getOptsByGranularity(granularity: Granularity, hourCycle: HourCycle | u minute: "2-digit", second: "2-digit", timeZoneName: "short", - hourCycle: hourCycle === 24 ? "h24" : undefined, + hourCycle: hourCycle === 24 ? "h23" : undefined, hour12: hourCycle === 24 ? false : undefined, }; diff --git a/packages/bits-ui/src/lib/internal/date-time/formatter.ts b/packages/bits-ui/src/lib/internal/date-time/formatter.ts index 105e907fb..7b6846e33 100644 --- a/packages/bits-ui/src/lib/internal/date-time/formatter.ts +++ b/packages/bits-ui/src/lib/internal/date-time/formatter.ts @@ -1,5 +1,6 @@ import { DateFormatter, type DateValue } from "@internationalized/date"; import { hasTime, isZonedDateTime, toDate } from "./utils.js"; +import type { HourCycle } from "$lib/shared/date/types.js"; export type Formatter = ReturnType; @@ -75,10 +76,11 @@ export function createFormatter(initialLocale: string) { return new DateFormatter(locale, { weekday: length }).format(date); } - function dayPeriod(date: Date) { + function dayPeriod(date: Date, hourCycle: HourCycle | undefined = undefined) { const parts = new DateFormatter(locale, { hour: "numeric", minute: "numeric", + hourCycle: hourCycle === 24 ? "h23" : undefined, }).formatToParts(date); const value = parts.find((p) => p.type === "dayPeriod")?.value; if (value === "PM") { diff --git a/packages/tests/src/tests/date-field/date-field.test.ts b/packages/tests/src/tests/date-field/date-field.test.ts index f28a3d97a..b13aac945 100644 --- a/packages/tests/src/tests/date-field/date-field.test.ts +++ b/packages/tests/src/tests/date-field/date-field.test.ts @@ -891,6 +891,56 @@ describe("date field", () => { await user.keyboard(kbd.ARROW_UP); expect(input).toHaveValue(value.add({ years: 1 }).toString()); }); + + it("should handle 24 hour time appropriately", async () => { + const value = new CalendarDateTime(2023, 10, 12, 12, 30, 30, 0); + const { getByTestId, user } = setup({ + name: "hello", + value, + hourCycle: 24, + }); + + const { getHour } = getTimeSegments(getByTestId); + const hour = getHour(); + hour.focus(); + await user.keyboard("22"); + expect(hour).toHaveTextContent("22"); + }); + + it("should allow 00 to be entered when hourCycle is 24", async () => { + const value = new CalendarDateTime(2023, 10, 12, 12, 30, 30, 0); + const { getByTestId, user } = setup({ + name: "hello", + value, + hourCycle: 24, + }); + + const { getHour } = getTimeSegments(getByTestId); + const hour = getHour(); + hour.focus(); + await user.keyboard("00"); + expect(hour).toHaveTextContent("00"); + }); + + it("navigating to 00 with ArrowUp/Down when hourCycle is 24 should show 00 and not 0", async () => { + const value = new CalendarDateTime(2023, 10, 12, 1, 30, 30, 0); + const { getByTestId, user } = setup({ + name: "hello", + value, + hourCycle: 24, + }); + + const { getHour } = getTimeSegments(getByTestId); + const hour = getHour(); + hour.focus(); + await user.keyboard(kbd.ARROW_DOWN); + expect(hour).toHaveTextContent("00"); + expect(hour.textContent).not.toBe("0"); + await user.keyboard(kbd.ARROW_DOWN); + expect(hour).toHaveTextContent("23"); + await user.keyboard(kbd.ARROW_UP); + expect(hour).toHaveTextContent("00"); + }); }); /** From bff490b5a5e2ff0b8ff6bf00fe0924b27b093708 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Mon, 18 Nov 2024 14:19:12 -0500 Subject: [PATCH 2/2] another test --- .../tests/src/tests/date-field/date-field.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/tests/src/tests/date-field/date-field.test.ts b/packages/tests/src/tests/date-field/date-field.test.ts index b13aac945..9aeca79f6 100644 --- a/packages/tests/src/tests/date-field/date-field.test.ts +++ b/packages/tests/src/tests/date-field/date-field.test.ts @@ -941,6 +941,19 @@ describe("date field", () => { await user.keyboard(kbd.ARROW_UP); expect(hour).toHaveTextContent("00"); }); + + it("should display correct hour when prepopulated with value and hourCycle is 24", async () => { + const value = new CalendarDateTime(2023, 10, 12, 0, 30, 30, 0); + const { getByTestId } = setup({ + name: "hello", + value, + hourCycle: 24, + }); + + const { getHour } = getTimeSegments(getByTestId); + const hour = getHour(); + expect(hour).toHaveTextContent("00"); + }); }); /**