Skip to content

Commit

Permalink
fix: date field 24 hour cycling (#945)
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Nov 18, 2024
1 parent 31d27bd commit fe4177f
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/early-mails-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bits-ui": patch
---

fix: DateField 24 hour cycling
35 changes: 30 additions & 5 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 @@ -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;
Expand Down Expand Up @@ -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}`;
});
Expand All @@ -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}`;
});
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions packages/bits-ui/src/lib/internal/date-time/field/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

/**
Expand Down Expand Up @@ -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,
};

Expand Down
4 changes: 3 additions & 1 deletion packages/bits-ui/src/lib/internal/date-time/formatter.ts
Original file line number Diff line number Diff line change
@@ -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<typeof createFormatter>;

Expand Down Expand Up @@ -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") {
Expand Down
63 changes: 63 additions & 0 deletions packages/tests/src/tests/date-field/date-field.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,69 @@ 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");
});

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");
});
});

/**
Expand Down

0 comments on commit fe4177f

Please sign in to comment.