diff --git a/packages/react/src/useTemporalState.ts b/packages/react/src/useTemporalState.ts new file mode 100644 index 0000000..4604b65 --- /dev/null +++ b/packages/react/src/useTemporalState.ts @@ -0,0 +1,54 @@ +import { Temporal } from "@js-temporal/polyfill"; +import { dateToTemporal } from "@tempocal/core"; +import * as React from "react"; + +export function useTemporalState( + mode: Mode, + initialState: Date | Temporal.PlainDateTime | Temporal.PlainDateTimeLike +): readonly [ + Mode extends "date" ? Temporal.PlainDate : Temporal.PlainDateTime, + ( + value: Mode extends "date" + ? Temporal.PlainDate | Temporal.PlainDateLike + : Temporal.PlainDateTime | Temporal.PlainDateTimeLike + ) => void +] { + const [temporalValue, setTemporalValue] = React.useState(() => { + if (mode === "date") { + return initialState instanceof Date + ? dateToTemporal(initialState).toPlainDate() + : initialState instanceof Temporal.PlainDate + ? initialState + : Temporal.PlainDate.from(initialState); + } + + return initialState instanceof Date + ? dateToTemporal(initialState) + : initialState instanceof Temporal.PlainDateTime + ? initialState + : Temporal.PlainDateTime.from(initialState); + }); + + const updateTemporalValue = React.useCallback( + ( + value: Mode extends "date" + ? Temporal.PlainDate | Temporal.PlainDateLike + : Temporal.PlainDateTime | Temporal.PlainDateTimeLike + ) => { + if ( + value instanceof Temporal.PlainDate || + value instanceof Temporal.PlainDateTime + ) { + setTemporalValue(value); + + return; + } + + setTemporalValue(temporalValue.with(value)); + }, + [temporalValue] + ); + + // @ts-expect-error Help. + return [temporalValue, updateTemporalValue]; +} diff --git a/packages/react/tempocal-react.ts b/packages/react/tempocal-react.ts index 5c33f32..9d6e206 100644 --- a/packages/react/tempocal-react.ts +++ b/packages/react/tempocal-react.ts @@ -1,2 +1,3 @@ export * from "./src/Calendar"; export * from "./src/useTempocal"; +export * from "./src/useTemporalState"; diff --git a/packages/react/test/useTempocal.test.ts b/packages/react/test/useTempocal.test.ts index 8c3fd42..fc2a15d 100644 --- a/packages/react/test/useTempocal.test.ts +++ b/packages/react/test/useTempocal.test.ts @@ -36,16 +36,6 @@ test("useTempocal", () => { ) ).toBe(true); - expect( - result.current.calendarProps.value.equals( - Temporal.PlainDate.from({ - year: 2022, - month: 4, - day: 15, - }) - ) - ).toBe(true); - expect(result.current.years).deep.equal([]); expect(result.current.months).toHaveLength(12); expect(result.current.hours).toHaveLength(24); diff --git a/packages/react/test/useTemporalState.test.ts b/packages/react/test/useTemporalState.test.ts new file mode 100644 index 0000000..d7c12aa --- /dev/null +++ b/packages/react/test/useTemporalState.test.ts @@ -0,0 +1,413 @@ +import { Temporal } from "@js-temporal/polyfill"; +import { act, renderHook } from "@testing-library/react-hooks"; +import { expect, test } from "vitest"; +import { useTemporalState } from "../tempocal-react"; + +test("useTemporalState (date, with Date)", () => { + const { result } = renderHook(() => + useTemporalState("date", new Date(2022, 3, 16)) + ); + + expect( + result.current[0].equals( + Temporal.PlainDate.from({ + year: 2022, + month: 4, + day: 16, + }) + ) + ).toBe(true); + + // Update with Temporal.PlainDate + act(() => { + result.current[1]( + Temporal.PlainDate.from({ + year: 2021, + month: 2, + day: 8, + }) + ); + }); + + expect( + result.current[0].equals( + Temporal.PlainDate.from({ + year: 2021, + month: 2, + day: 8, + }) + ) + ).toBe(true); + + // Update with PlainDateLike + act(() => { + result.current[1]({ + year: 2020, + month: 1, + day: 4, + }); + }); + + expect( + result.current[0].equals( + Temporal.PlainDate.from({ + year: 2020, + month: 1, + day: 4, + }) + ) + ).toBe(true); +}); + +test("useTemporalState (date, with PlainDate)", () => { + const { result } = renderHook(() => + useTemporalState( + "date", + Temporal.PlainDate.from({ + year: 2022, + month: 4, + day: 16, + }) + ) + ); + + expect( + result.current[0].equals( + Temporal.PlainDate.from({ + year: 2022, + month: 4, + day: 16, + }) + ) + ).toBe(true); + + // Update with Temporal.PlainDate + act(() => { + result.current[1]( + Temporal.PlainDate.from({ + year: 2021, + month: 2, + day: 8, + }) + ); + }); + + expect( + result.current[0].equals( + Temporal.PlainDate.from({ + year: 2021, + month: 2, + day: 8, + }) + ) + ).toBe(true); + + // Update with PlainDateLike + act(() => { + result.current[1]({ + year: 2020, + month: 1, + day: 4, + }); + }); + + expect( + result.current[0].equals( + Temporal.PlainDate.from({ + year: 2020, + month: 1, + day: 4, + }) + ) + ).toBe(true); +}); + +test("useTemporalState (date, with PlainDateLike)", () => { + const { result } = renderHook(() => + useTemporalState("date", { + year: 2022, + month: 4, + day: 16, + }) + ); + + expect( + result.current[0].equals( + Temporal.PlainDate.from({ + year: 2022, + month: 4, + day: 16, + }) + ) + ).toBe(true); + + // Update with Temporal.PlainDate + act(() => { + result.current[1]( + Temporal.PlainDate.from({ + year: 2021, + month: 2, + day: 8, + }) + ); + }); + + expect( + result.current[0].equals( + Temporal.PlainDate.from({ + year: 2021, + month: 2, + day: 8, + }) + ) + ).toBe(true); + + // Update with PlainDateLike + act(() => { + result.current[1]({ + year: 2020, + month: 1, + day: 4, + }); + }); + + expect( + result.current[0].equals( + Temporal.PlainDate.from({ + year: 2020, + month: 1, + day: 4, + }) + ) + ).toBe(true); +}); + +test("useTemporalState (datetime, with Date)", () => { + const { result } = renderHook(() => + useTemporalState("datetime", new Date(2022, 3, 16, 15, 30, 45)) + ); + + expect( + result.current[0].equals( + Temporal.PlainDateTime.from({ + year: 2022, + month: 4, + day: 16, + hour: 15, + minute: 30, + second: 45, + }) + ) + ).toBe(true); + + // Update with Temporal.PlainDateTime + act(() => { + result.current[1]( + Temporal.PlainDateTime.from({ + year: 2021, + month: 2, + day: 8, + hour: 10, + minute: 20, + second: 40, + }) + ); + }); + + expect( + result.current[0].equals( + Temporal.PlainDateTime.from({ + year: 2021, + month: 2, + day: 8, + hour: 10, + minute: 20, + second: 40, + }) + ) + ).toBe(true); + + // Update with PlainDateTimeLike + act(() => { + result.current[1]({ + year: 2020, + month: 1, + day: 4, + hour: 5, + minute: 10, + second: 20, + }); + }); + + expect( + result.current[0].equals( + Temporal.PlainDateTime.from({ + year: 2020, + month: 1, + day: 4, + hour: 5, + minute: 10, + second: 20, + }) + ) + ).toBe(true); +}); + +test("useTemporalState (datetime, with PlainDateTime)", () => { + const { result } = renderHook(() => + useTemporalState( + "datetime", + Temporal.PlainDateTime.from({ + year: 2022, + month: 4, + day: 16, + hour: 15, + minute: 30, + second: 45, + }) + ) + ); + + expect( + result.current[0].equals( + Temporal.PlainDateTime.from({ + year: 2022, + month: 4, + day: 16, + hour: 15, + minute: 30, + second: 45, + }) + ) + ).toBe(true); + + // Update with Temporal.PlainDateTime + act(() => { + result.current[1]( + Temporal.PlainDateTime.from({ + year: 2021, + month: 2, + day: 8, + hour: 10, + minute: 20, + second: 40, + }) + ); + }); + + expect( + result.current[0].equals( + Temporal.PlainDateTime.from({ + year: 2021, + month: 2, + day: 8, + hour: 10, + minute: 20, + second: 40, + }) + ) + ).toBe(true); + + // Update with PlainDateTimeLike + act(() => { + result.current[1]({ + year: 2020, + month: 1, + day: 4, + hour: 5, + minute: 10, + second: 20, + }); + }); + + expect( + result.current[0].equals( + Temporal.PlainDateTime.from({ + year: 2020, + month: 1, + day: 4, + hour: 5, + minute: 10, + second: 20, + }) + ) + ).toBe(true); +}); + +test("useTemporalState (datetime, with PlainDateTimeLike)", () => { + const { result } = renderHook(() => + useTemporalState("datetime", { + year: 2022, + month: 4, + day: 16, + hour: 15, + minute: 30, + second: 45, + }) + ); + + expect( + result.current[0].equals( + Temporal.PlainDateTime.from({ + year: 2022, + month: 4, + day: 16, + hour: 15, + minute: 30, + second: 45, + }) + ) + ).toBe(true); + + // Update with Temporal.PlainDateTime + act(() => { + result.current[1]( + Temporal.PlainDateTime.from({ + year: 2021, + month: 2, + day: 8, + hour: 10, + minute: 20, + second: 40, + }) + ); + }); + + expect( + result.current[0].equals( + Temporal.PlainDateTime.from({ + year: 2021, + month: 2, + day: 8, + hour: 10, + minute: 20, + second: 40, + }) + ) + ).toBe(true); + + // Update with PlainDateTimeLike + act(() => { + result.current[1]({ + year: 2020, + month: 1, + day: 4, + hour: 5, + minute: 10, + second: 20, + }); + }); + + expect( + result.current[0].equals( + Temporal.PlainDateTime.from({ + year: 2020, + month: 1, + day: 4, + hour: 5, + minute: 10, + second: 20, + }) + ) + ).toBe(true); +}); diff --git a/packages/www/components/Sidebar.tsx b/packages/www/components/Sidebar.tsx index 75244bb..e867364 100644 --- a/packages/www/components/Sidebar.tsx +++ b/packages/www/components/Sidebar.tsx @@ -26,7 +26,7 @@ const documentation = [ }, { section: "react", - pages: ["useTempocal", "Calendar"], + pages: ["useTemporalState", "useTempocal", "Calendar"], }, { section: "core", diff --git a/packages/www/examples/Basic.tsx b/packages/www/examples/Basic.tsx index 19cd6f2..c37ed7f 100644 --- a/packages/www/examples/Basic.tsx +++ b/packages/www/examples/Basic.tsx @@ -1,15 +1,11 @@ -import { Temporal } from "@js-temporal/polyfill"; -import { Calendar, useTempocal } from "@tempocal/react"; -import * as React from "react"; +import { Calendar, useTempocal, useTemporalState } from "@tempocal/react"; export function Basic() { - const [value, setValue] = React.useState( - Temporal.PlainDate.from({ - year: 2021, - month: 11, - day: 25, - }) - ); + const [value, setValue] = useTemporalState("date", { + year: 2021, + month: 11, + day: 25, + }); const { calendarProps } = useTempocal({ locale: "en-US", diff --git a/packages/www/examples/DateInput.tsx b/packages/www/examples/DateInput.tsx index 4567483..32c34cc 100644 --- a/packages/www/examples/DateInput.tsx +++ b/packages/www/examples/DateInput.tsx @@ -1,6 +1,5 @@ -import { Temporal } from "@js-temporal/polyfill"; import { temporalToDate } from "@tempocal/core"; -import { Calendar, useTempocal } from "@tempocal/react"; +import { Calendar, useTempocal, useTemporalState } from "@tempocal/react"; import classnames from "classnames"; import * as React from "react"; import { CalendarHeader } from "../recipes/CalendarHeader"; @@ -14,13 +13,11 @@ const dateFormatter = new Intl.DateTimeFormat(locale, { export function DateInput() { const [isOpen, setOpen] = React.useState(false); - const [value, setValue] = React.useState( - Temporal.PlainDate.from({ - year: 2021, - month: 11, - day: 25, - }) - ); + const [value, setValue] = useTemporalState("date", { + year: 2021, + month: 11, + day: 25, + }); const { calendarProps, diff --git a/packages/www/examples/DatePicker.tsx b/packages/www/examples/DatePicker.tsx index ba3f2e8..187b9f8 100644 --- a/packages/www/examples/DatePicker.tsx +++ b/packages/www/examples/DatePicker.tsx @@ -1,6 +1,11 @@ import { Temporal } from "@js-temporal/polyfill"; import { temporalToDate } from "@tempocal/core"; -import { Calendar, Locale, useTempocal } from "@tempocal/react"; +import { + Calendar, + Locale, + useTempocal, + useTemporalState, +} from "@tempocal/react"; import classnames from "classnames"; import * as React from "react"; import { CalendarHeader } from "../recipes/CalendarHeader"; @@ -34,13 +39,11 @@ export function DatePicker({ rollover: boolean; startOfWeek: number; }) { - const [value, setValue] = React.useState( - Temporal.PlainDate.from({ - year: 2021, - month: 11, - day: 25, - }) - ); + const [value, setValue] = useTemporalState("date", { + year: 2021, + month: 11, + day: 25, + }); const [minValue] = React.useState(value.subtract({ years: 2 })); const [maxValue] = React.useState(value.add({ years: 2 })); diff --git a/packages/www/examples/DateTimePicker.tsx b/packages/www/examples/DateTimePicker.tsx index e40ad25..a1eef41 100644 --- a/packages/www/examples/DateTimePicker.tsx +++ b/packages/www/examples/DateTimePicker.tsx @@ -1,6 +1,10 @@ -import { Temporal } from "@js-temporal/polyfill"; import { temporalToDate } from "@tempocal/core"; -import { Calendar, ClampMode, useTempocal } from "@tempocal/react"; +import { + Calendar, + ClampMode, + useTempocal, + useTemporalState, +} from "@tempocal/react"; import classnames from "classnames"; import * as React from "react"; import { CalendarHeader } from "../recipes/CalendarHeader"; @@ -17,16 +21,14 @@ export function DateTimePicker({ }: { clampSelectedValue: ClampMode; }) { - const [value, setValue] = React.useState( - Temporal.PlainDateTime.from({ - year: 2021, - month: 11, - day: 25, - hour: 8, - minute: 30, - second: 0, - }) - ); + const [value, setValue] = useTemporalState("datetime", { + year: 2021, + month: 11, + day: 25, + hour: 8, + minute: 30, + second: 0, + }); const [minValue] = React.useState(value.subtract({ years: 2 })); const [maxValue] = React.useState(value.add({ years: 2 })); diff --git a/packages/www/pages/react/useTemporalState.tsx b/packages/www/pages/react/useTemporalState.tsx new file mode 100644 index 0000000..806a567 --- /dev/null +++ b/packages/www/pages/react/useTemporalState.tsx @@ -0,0 +1,66 @@ +import { AnchorHeader } from "../../components/AnchorHeader"; +import { Code, CodeBlock } from "../../components/Code"; + +export default function UseTemporalStatePage() { + return ( +
+
+

useTemporalState

+
+
+

+ This hooks works like React.useState, except it's only + meant to hold Temporal values. +

+ {`import { useTemporalState } from "@tempocal/react"; + +const [value, setValue] = useTemporalState( + mode: "date" | "datetime", + initialState: Temporal.PlainDate | Temporal.PlainDateLike | Temporal.PlainDateTime | Temporal.PlainDateTimeLike +);`} +
+

Options

+
+
+
+ mode +

+ {`mode: "date" | "datetime"`} +
+
+ initialState +

+ {`// mode: "date" +initialState: Temporal.PlainDate | Temporal.PlainDateLike + +// mode: "datetime" +initialState: Temporal.PlainDateTime | Temporal.PlainDateTimeLike`} +
+
+
+

Returns

+
+
+
+ value +

+ {`// mode: "date" +value: Temporal.PlainDate + +// mode: "datetime" +value: Temporal.PlainDateTime`} +
+
+ setValue +

+ {`// mode: "date" +setValue: (value: Temporal.PlainDate | Temporal.PlainDateLike) => void + +// mode: "datetime" +setValue: (value: Temporal.PlainDateTime | Temporal.PlainDateTimeLike) => void`} +
+
+
+
+ ); +}