diff --git a/packages/core/src/clamp.ts b/packages/core/src/clamp.ts index d6b6b1c..9b2ff85 100644 --- a/packages/core/src/clamp.ts +++ b/packages/core/src/clamp.ts @@ -1,8 +1,21 @@ import { Temporal } from "@js-temporal/polyfill"; -export function clamp< - Value extends Temporal.PlainDate | Temporal.PlainDateTime ->(value: Value, minValue?: Value, maxValue?: Value) { +export function clamp( + value: Temporal.PlainDate, + minValue?: Temporal.PlainDate, + maxValue?: Temporal.PlainDate +): Temporal.PlainDate; +export function clamp( + value: Temporal.PlainDateTime, + minValue?: Temporal.PlainDateTime, + maxValue?: Temporal.PlainDateTime +): Temporal.PlainDateTime; + +export function clamp( + value: Temporal.PlainDate | Temporal.PlainDateTime, + minValue?: Temporal.PlainDate | Temporal.PlainDateTime, + maxValue?: Temporal.PlainDate | Temporal.PlainDateTime +): Temporal.PlainDate | Temporal.PlainDateTime { if ( minValue && Temporal[ diff --git a/packages/core/src/getMonths.ts b/packages/core/src/getMonths.ts index 722d6a7..8cd9524 100644 --- a/packages/core/src/getMonths.ts +++ b/packages/core/src/getMonths.ts @@ -1,11 +1,13 @@ import { Temporal } from "@js-temporal/polyfill"; +import { getNow } from "./getNow"; import { temporalToDate } from "./temporalToDate"; +const referenceValue = getNow(); + export function getMonths( - locale: Parameters[0], - referenceValue: Temporal.PlainDate, - minValue?: Temporal.PlainDate, - maxValue?: Temporal.PlainDate + locale: Intl.LocalesArgument, + minValue?: Temporal.PlainDate | Temporal.PlainDateTime, + maxValue?: Temporal.PlainDate | Temporal.PlainDateTime ) { const longMonthFormatter = new Intl.DateTimeFormat(locale, { month: "long", diff --git a/packages/core/src/getNow.ts b/packages/core/src/getNow.ts new file mode 100644 index 0000000..96eb325 --- /dev/null +++ b/packages/core/src/getNow.ts @@ -0,0 +1,5 @@ +import { Temporal } from "@js-temporal/polyfill"; + +export function getNow(calendar: Temporal.CalendarLike = "iso8601") { + return Temporal.Now.plainDateTime(calendar); +} diff --git a/packages/core/src/getWeekdays.ts b/packages/core/src/getWeekdays.ts index c774c9b..58fa57f 100644 --- a/packages/core/src/getWeekdays.ts +++ b/packages/core/src/getWeekdays.ts @@ -1,15 +1,9 @@ -import { Temporal } from "@js-temporal/polyfill"; import { getFirstDayOfWeek } from "./getFirstDayOfWeek"; +import { getNow } from "./getNow"; import { temporalToDate } from "./temporalToDate"; -export function getWeekdays( - locale: Parameters[0], - startOfWeek: number -) { - const firstDayOfWeek = getFirstDayOfWeek( - Temporal.Now.plainDate("iso8601"), - startOfWeek - ); +export function getWeekdays(locale: Intl.LocalesArgument, startOfWeek: number) { + const firstDayOfWeek = getFirstDayOfWeek(getNow(), startOfWeek); const longWeekdayFormatter = new Intl.DateTimeFormat(locale, { weekday: "long", diff --git a/packages/core/src/getYears.ts b/packages/core/src/getYears.ts index cf2cc84..95d67c5 100644 --- a/packages/core/src/getYears.ts +++ b/packages/core/src/getYears.ts @@ -1,8 +1,8 @@ import { Temporal } from "@js-temporal/polyfill"; export function getYears( - minValue: Temporal.PlainDate, - maxValue: Temporal.PlainDate + minValue: Temporal.PlainDate | Temporal.PlainDateTime, + maxValue: Temporal.PlainDate | Temporal.PlainDateTime ) { const years: number[] = []; diff --git a/packages/core/tempocal-core.ts b/packages/core/tempocal-core.ts index 9ef15df..d14d511 100644 --- a/packages/core/tempocal-core.ts +++ b/packages/core/tempocal-core.ts @@ -5,8 +5,9 @@ export * from "./src/getFirstDayOfWeek"; export * from "./src/getHours"; export * from "./src/getMinutes"; export * from "./src/getMonthEndDate"; -export * from "./src/getMonths"; export * from "./src/getMonthStartDate"; +export * from "./src/getMonths"; +export * from "./src/getNow"; export * from "./src/getWeekdays"; export * from "./src/getYears"; export * from "./src/temporalToDate"; diff --git a/packages/core/test/getNow.test.ts b/packages/core/test/getNow.test.ts new file mode 100644 index 0000000..644cf04 --- /dev/null +++ b/packages/core/test/getNow.test.ts @@ -0,0 +1,19 @@ +import { Temporal } from "@js-temporal/polyfill"; +import { expect, test } from "vitest"; +import { getNow } from "../src/getNow"; + +test("getNow (PlainDate, unbounded)", () => { + const result = getNow(); + + expect(result instanceof Temporal.PlainDate).toBe(true); + + expect( + result.equals( + Temporal.PlainDate.from({ + year: new Date().getFullYear(), + month: new Date().getMonth() + 1, + day: new Date().getDate(), + }) + ) + ).toBe(true); +}); diff --git a/packages/react/src/Calendar.tsx b/packages/react/src/Calendar.tsx index 76364f7..5ea646b 100644 --- a/packages/react/src/Calendar.tsx +++ b/packages/react/src/Calendar.tsx @@ -5,8 +5,7 @@ import { getWeekdays, } from "@tempocal/core"; import * as React from "react"; -import { CSSProperties } from "react"; -import { Locale } from "./useTempocal"; +import { Locale } from "./types"; type Value = Temporal.PlainDate | Temporal.PlainDateTime; @@ -260,7 +259,7 @@ function Day({ }: Pick & { date: Temporal.PlainDate; disabled: boolean; - style: CSSProperties | undefined; + style: React.CSSProperties | undefined; }) { const props = React.useMemo(() => { const plainDateLike: Temporal.PlainDateLike = { diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts new file mode 100644 index 0000000..071ee12 --- /dev/null +++ b/packages/react/src/types.ts @@ -0,0 +1,3 @@ +export type ClampMode = "always" | "value-change" | "never"; + +export type Locale = Intl.LocalesArgument; diff --git a/packages/react/src/useCalendarValue.ts b/packages/react/src/useCalendarValue.ts new file mode 100644 index 0000000..150e86b --- /dev/null +++ b/packages/react/src/useCalendarValue.ts @@ -0,0 +1,61 @@ +import { Temporal } from "@js-temporal/polyfill"; +import { clamp, getNow } from "@tempocal/core"; +import { useCallback } from "react"; + +export function useCalendarValue({ + clampCalendarValue, + maxValue, + minValue, + setCalendarValue, + calendarValue, +}: { + clampCalendarValue: boolean; + maxValue: Temporal.PlainDate | Temporal.PlainDateTime | undefined; + minValue: Temporal.PlainDate | Temporal.PlainDateTime | undefined; + setCalendarValue: (value: Temporal.PlainDate) => void; + calendarValue: Temporal.PlainDate; +}) { + const updateCalendarValue = useCallback( + (nextCalendarValue: Temporal.PlainDate) => { + if (!clampCalendarValue) { + setCalendarValue(nextCalendarValue); + + return nextCalendarValue; + } + + const clampedValue = clamp( + nextCalendarValue, + minValue instanceof Temporal.PlainDate + ? minValue + : minValue?.toPlainDate(), + maxValue instanceof Temporal.PlainDate + ? maxValue + : maxValue?.toPlainDate() + ); + + setCalendarValue(clampedValue); + + return clampedValue; + }, + [clampCalendarValue, maxValue, minValue, setCalendarValue] + ); + + const onChangeCalendarValue = useCallback( + (params?: Temporal.PlainDate | Temporal.PlainDateLike) => { + if (!params) { + return updateCalendarValue(getNow().toPlainDate()); + } + + if (params instanceof Temporal.PlainDate) { + return updateCalendarValue(params); + } + + return updateCalendarValue(calendarValue.with(params)); + }, + [calendarValue, updateCalendarValue] + ); + + return { + onChangeCalendarValue, + }; +} diff --git a/packages/react/src/useTempocal.ts b/packages/react/src/useTempocal.ts index ade1eb2..9c2f5c6 100644 --- a/packages/react/src/useTempocal.ts +++ b/packages/react/src/useTempocal.ts @@ -4,57 +4,16 @@ import { getHours, getMinutes, getMonths, + getNow, getYears, } from "@tempocal/core"; -import * as React from "react"; - -export type ClampMode = "always" | "value-change" | "never"; - -export type DateRange = - | [undefined, undefined] - | [Temporal.PlainDate, undefined] - | [Temporal.PlainDate, Temporal.PlainDate]; - -export type DateTimeRange = - | [undefined, undefined] - | [Temporal.PlainDateTime, undefined] - | [Temporal.PlainDateTime, Temporal.PlainDateTime]; - -type RequiredValue = Mode extends "date" - ? Temporal.PlainDate - : Mode extends "daterange" - ? DateRange - : Mode extends "datetime" - ? Temporal.PlainDateTime - : Mode extends "datetimerange" - ? DateTimeRange - : never; - -type ChangeValue = Mode extends "date" - ? Temporal.PlainDate | Temporal.PlainDateLike - : Mode extends "daterange" - ? - | Temporal.PlainDate - | Temporal.PlainDateLike - | [Temporal.PlainDate, Temporal.PlainDate] - | [undefined, undefined] - : Mode extends "datetime" - ? Temporal.PlainDateTime | Temporal.PlainDateTimeLike - : Mode extends "datetimerange" - ? - | Temporal.PlainDateTime - | Temporal.PlainDateTimeLike - | [Temporal.PlainDateTime, Temporal.PlainDateTime] - | [undefined, undefined] - : never; - -export type Locale = string; - -export function useTempocal< - Mode extends "date" | "daterange" | "datetime" | "datetimerange" ->({ - clampCalendarValue, - clampSelectedValue, +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { ClampMode, Locale } from "./types"; +import { useCalendarValue } from "./useCalendarValue"; + +export function useTempocal({ + clampCalendarValue = false, + clampSelectedValue = "never", locale, maxValue, minValue, @@ -63,232 +22,142 @@ export function useTempocal< value, }: { clampCalendarValue?: boolean; - clampSelectedValue?: Mode extends "date" - ? ClampMode - : Mode extends "datetime" - ? ClampMode - : never; + clampSelectedValue?: ClampMode; locale: Locale; - maxValue?: Temporal.PlainDate | Temporal.PlainDateTime; - minValue?: Temporal.PlainDate | Temporal.PlainDateTime; - mode: Mode; - setValue: (value: RequiredValue) => void; - value: RequiredValue | undefined; -}) { - const [calendarValue, setCalendarValue] = React.useState(() => { - if (!value || (Array.isArray(value) && !value[0])) { - return Temporal.Now.plainDate("iso8601"); +} & ( + | { + mode: "date"; + maxValue?: Temporal.PlainDate; + minValue?: Temporal.PlainDate; + setValue: (value: Temporal.PlainDate) => void; + value: Temporal.PlainDate | undefined; } - - if (Array.isArray(value)) { - return Temporal.PlainDate.from(value[0]); + | { + mode: "datetime"; + maxValue?: Temporal.PlainDateTime; + minValue?: Temporal.PlainDateTime; + setValue: (value: Temporal.PlainDateTime) => void; + value: Temporal.PlainDateTime | undefined; + } +)) { + const [calendarValue, setCalendarValue] = useState(() => { + if (!value) { + return getNow().toPlainDate(); } - return Temporal.PlainDate.from(value); + return value instanceof Temporal.PlainDate ? value : value.toPlainDate(); }); - const years = React.useMemo(() => { + const years = useMemo(() => { if (!minValue || !maxValue) { - return []; + return; } - return getYears( - minValue instanceof Temporal.PlainDateTime - ? minValue.toPlainDate() - : minValue, - maxValue instanceof Temporal.PlainDateTime - ? maxValue.toPlainDate() - : maxValue - ); + return getYears(minValue, maxValue); }, [maxValue, minValue]); - const months = React.useMemo(() => { - return getMonths( - locale, - calendarValue, - minValue instanceof Temporal.PlainDateTime - ? minValue.toPlainDate() - : minValue, - maxValue instanceof Temporal.PlainDateTime - ? maxValue.toPlainDate() - : maxValue - ); - }, [calendarValue, locale, maxValue, minValue]); - - const hours = React.useMemo(() => { - if ( - value instanceof Temporal.PlainDateTime && - (!minValue || minValue instanceof Temporal.PlainDateTime) && - (!maxValue || maxValue instanceof Temporal.PlainDateTime) - ) { - return getHours(value, minValue, maxValue); - } - - return getHours(); - }, [value, maxValue, minValue]); + const months = useMemo( + () => getMonths(locale, minValue, maxValue), + [locale, maxValue, minValue] + ); - const minutes = React.useMemo(() => { - if ( - value instanceof Temporal.PlainDateTime && - (!minValue || minValue instanceof Temporal.PlainDateTime) && - (!maxValue || maxValue instanceof Temporal.PlainDateTime) - ) { - return getMinutes(value, minValue, maxValue); + const hours = useMemo(() => { + if (mode !== "datetime") { + return; } - return getMinutes(); - }, [value, maxValue, minValue]); - - const updateCalendarValue = React.useCallback( - (nextCalendarValue: Temporal.PlainDate) => { - if (!clampCalendarValue) { - setCalendarValue(nextCalendarValue); - - return nextCalendarValue; - } - - const clampedValue = clamp( - nextCalendarValue, - minValue instanceof Temporal.PlainDateTime - ? minValue.toPlainDate() - : minValue, - maxValue instanceof Temporal.PlainDateTime - ? maxValue.toPlainDate() - : maxValue - ); - - setCalendarValue(clampedValue); + return getHours(value, minValue, maxValue); + }, [maxValue, minValue, mode, value]); - return clampedValue; - }, - [clampCalendarValue, maxValue, minValue] - ); + const minutes = useMemo(() => { + if (mode !== "datetime") { + return; + } - const onChangeCalendarValue = React.useCallback( - (params?: Temporal.PlainDate | Temporal.PlainDateLike) => { - if (!params) { - return updateCalendarValue(Temporal.Now.plainDate("iso8601")); - } + return getMinutes(value, minValue, maxValue); + }, [maxValue, minValue, mode, value]); - if (params instanceof Temporal.PlainDate) { - return updateCalendarValue(params); - } + const { onChangeCalendarValue } = useCalendarValue({ + clampCalendarValue, + maxValue, + minValue, + setCalendarValue, + calendarValue, + }); - return updateCalendarValue(calendarValue.with(params)); - }, - [calendarValue, updateCalendarValue] - ); + const updateSelectedValue = useCallback( + (nextSelectedValue: Temporal.PlainDate | Temporal.PlainDateTime) => { + if (nextSelectedValue instanceof Temporal.PlainDate) { + if (mode !== "date") { + throw new Error( + `Received a Temporal.PlainDate but expected a Temporal.PlainDateTime in updateSelectedValue` + ); + } - const updateSelectedValue = React.useCallback( - ( - nextSelectedValue: - | Temporal.PlainDate - | Temporal.PlainDateTime - | DateRange - | DateTimeRange - ) => { - if ( - !clampSelectedValue || - clampSelectedValue === "never" || - Array.isArray(nextSelectedValue) - ) { - // @ts-expect-error Help. - setValue(nextSelectedValue); - - return nextSelectedValue; - } + if (clampSelectedValue === "never") { + setValue(nextSelectedValue); - const clampedValue = clamp(nextSelectedValue, minValue, maxValue); + return nextSelectedValue; + } - // @ts-expect-error Help. - setValue(clampedValue); + const clampedValue = clamp(nextSelectedValue, minValue, maxValue); - return clampedValue; - }, - [clampSelectedValue, maxValue, minValue, setValue] - ); + setValue(clampedValue); - const onChangeSelectedValue = React.useCallback( - (params: ChangeValue): RequiredValue => { - if (Array.isArray(params)) { - if (!["daterange", "datetimerange"].includes(mode)) { + return clampedValue; + } else if (nextSelectedValue instanceof Temporal.PlainDateTime) { + if (mode !== "datetime") { throw new Error( - `Received an array in onChangeSelectedValue but mode is ${mode}` + `Received a Temporal.PlainDateTime but expected a Temporal.PlainDate in updateSelectedValue` ); } - if (!params[0] && !params[1]) { - // @ts-expect-error Help. - setValue(params); - } else if ( - params[0] instanceof Temporal.PlainDate && - params[1] instanceof Temporal.PlainDate && - mode === "daterange" - ) { - // @ts-expect-error Help. - setValue(params); - } else if ( - params[0] instanceof Temporal.PlainDateTime && - params[1] instanceof Temporal.PlainDateTime && - mode === "datetimerange" - ) { - // @ts-expect-error Help. - setValue(params); - } else { - throw new Error( - `Received an array of mixed values in onChangeSelectedValue but expected a pair of ${ - mode === "daterange" - ? "Temporal.PlainDate" - : "Temporal.PlainDateTime" - }` - ); + if (clampSelectedValue === "never") { + setValue(nextSelectedValue); + + return nextSelectedValue; } - return params as RequiredValue; + const clampedValue = clamp(nextSelectedValue, minValue, maxValue); + + setValue(clampedValue); + + return clampedValue; } + }, + [clampSelectedValue, maxValue, minValue, mode, setValue] + ); + const onChangeSelectedValue = useCallback( + ( + params: + | Temporal.PlainDate + | Temporal.PlainDateLike + | Temporal.PlainDateTime + | Temporal.PlainDateTimeLike + ) => { const nextValue = (() => { - if ( - params instanceof Temporal.PlainDate || - params instanceof Temporal.PlainDateTime - ) { + if (params instanceof Temporal.PlainDate) { return params; } - if (Array.isArray(value) && value[0]) { - return value[0].with(params); - } - - if (!Array.isArray(value) && value) { + if (value) { return value.with(params); } - return mode === "date" - ? Temporal.Now.plainDate("iso8601").with(params) - : Temporal.Now.plainDateTime("iso8601").with(params); + return getNow().toPlainDate().with(params); })(); - if (Array.isArray(value)) { - const range = ( - value[0] && !value[1] - ? [value[0], nextValue].sort(Temporal.PlainDate.compare) - : [nextValue, undefined] - ) as DateRange; - - return updateSelectedValue(range) as RequiredValue; - } - - return updateSelectedValue(nextValue) as RequiredValue; + return updateSelectedValue(nextValue); }, - [mode, setValue, updateSelectedValue, value] + [updateSelectedValue, value] ); - const previousClampSelectedValue = React.useRef(clampSelectedValue); - const previousMaxValue = React.useRef(maxValue); - const previousMinValue = React.useRef(minValue); + const previousMaxValue = useRef(maxValue); + const previousMinValue = useRef(minValue); - React.useEffect(() => { - if (!clampSelectedValue || !value) { + useEffect(() => { + if (!value) { return; } @@ -301,16 +170,14 @@ export function useTempocal< } if ( - clampSelectedValue !== previousClampSelectedValue.current || maxValue !== previousMaxValue.current || minValue !== previousMinValue.current ) { updateSelectedValue(value); } - }); + }, [clampSelectedValue, maxValue, minValue, updateSelectedValue, value]); - React.useEffect(() => { - previousClampSelectedValue.current = clampSelectedValue; + useEffect(() => { previousMaxValue.current = maxValue; previousMinValue.current = minValue; }); diff --git a/packages/react/src/useTempocalRange.ts b/packages/react/src/useTempocalRange.ts new file mode 100644 index 0000000..a0d4b4b --- /dev/null +++ b/packages/react/src/useTempocalRange.ts @@ -0,0 +1,313 @@ +import { Temporal } from "@js-temporal/polyfill"; +import { + clamp, + getHours, + getMinutes, + getMonths, + getNow, + getYears, +} from "@tempocal/core"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { ClampMode, Locale } from "./types"; +import { useCalendarValue } from "./useCalendarValue"; + +type EmptyRange = { start: undefined; end: undefined }; + +type Range = + | { start: T; end: T } + | { start: T; end: undefined } + | EmptyRange; + +export type DateRange = Range; +export type DateTimeRange = Range; + +function isDateRange(value: unknown): value is DateRange { + return Boolean( + value && + typeof value === "object" && + "start" in value && + "end" in value && + value.start instanceof Temporal.PlainDate && + value.end instanceof Temporal.PlainDate + ); +} + +function isDateTimeRange(value: unknown): value is DateTimeRange { + return Boolean( + value && + typeof value === "object" && + "start" in value && + "end" in value && + value.start instanceof Temporal.PlainDateTime && + value.end instanceof Temporal.PlainDateTime + ); +} + +function isEmptyRange(value: unknown): value is EmptyRange { + return Boolean( + value && + typeof value === "object" && + "start" in value && + "end" in value && + !value.start && + !value.end + ); +} + +export function useTempocalRange({ + clampCalendarValue = false, + clampSelectedValue = "never", + locale, + maxValue, + minValue, + mode, + setValue, + value, +}: { + clampCalendarValue?: boolean; + clampSelectedValue?: ClampMode; + locale: Locale; +} & ( + | { + mode: "daterange"; + maxValue?: Temporal.PlainDate; + minValue?: Temporal.PlainDate; + setValue: (value: DateRange) => void; + value: DateRange; + } + | { + mode: "datetimerange"; + maxValue?: Temporal.PlainDateTime; + minValue?: Temporal.PlainDateTime; + setValue: (value: DateTimeRange) => void; + value: DateTimeRange; + } +)) { + const [calendarValue, setCalendarValue] = useState(() => { + if (!value.start) { + return getNow().toPlainDate(); + } + + return value.start instanceof Temporal.PlainDate + ? value.start + : value.start.toPlainDate(); + }); + + const years = useMemo(() => { + if (!minValue || !maxValue) { + return; + } + + return getYears(minValue, maxValue); + }, [maxValue, minValue]); + + const months = useMemo( + () => getMonths(locale, minValue, maxValue), + [locale, maxValue, minValue] + ); + + const hours = useMemo< + [ReturnType, ReturnType] | undefined + >(() => { + if (mode !== "datetimerange") { + return; + } + + return [ + getHours(value.start, minValue, maxValue), + getHours(value.end, minValue, maxValue), + ]; + }, [maxValue, minValue, mode, value]); + + const minutes = useMemo< + [ReturnType, ReturnType] | undefined + >(() => { + if (mode !== "datetimerange") { + return; + } + + return [ + getMinutes(value.start, minValue, maxValue), + getMinutes(value.end, minValue, maxValue), + ]; + }, [maxValue, minValue, mode, value]); + + const { onChangeCalendarValue } = useCalendarValue({ + clampCalendarValue, + maxValue, + minValue, + setCalendarValue, + calendarValue, + }); + + const updateSelectedValue = useCallback( + (nextSelectedValue: DateRange | DateTimeRange) => { + if (isEmptyRange(nextSelectedValue)) { + setValue(nextSelectedValue); + + return nextSelectedValue; + } + + if (isDateRange(nextSelectedValue)) { + if (mode !== "daterange") { + throw new Error( + `Received a Temporal.PlainDate but expected a Temporal.PlainDateTime in updateSelectedValue` + ); + } + + if (clampSelectedValue === "never") { + setValue(nextSelectedValue); + + return nextSelectedValue; + } + + const clampedValue = { + start: clamp(nextSelectedValue.start, minValue, maxValue), + end: nextSelectedValue.end + ? clamp(nextSelectedValue.end, minValue, maxValue) + : undefined, + }; + + setValue(clampedValue); + + return clampedValue; + } else if (isDateTimeRange(nextSelectedValue)) { + if (mode !== "datetimerange") { + throw new Error( + `Received a Temporal.PlainDateTime but expected a Temporal.PlainDate in updateSelectedValue` + ); + } + + if (clampSelectedValue === "never") { + setValue(nextSelectedValue); + + return nextSelectedValue; + } + + const clampedValue = { + start: clamp(nextSelectedValue.start, minValue, maxValue), + end: nextSelectedValue.end + ? clamp(nextSelectedValue.end, minValue, maxValue) + : undefined, + }; + + setValue(clampedValue); + + return clampedValue; + } + }, + [clampSelectedValue, maxValue, minValue, mode, setValue] + ); + + const onChangeSelectedValue = useCallback( + ( + params: + | Temporal.PlainDate + | Temporal.PlainDateLike + | Temporal.PlainDateTime + | Temporal.PlainDateTimeLike + | DateRange + | DateTimeRange + | { start: undefined; end: undefined } + ) => { + if ( + isDateRange(params) || + isDateTimeRange(params) || + isEmptyRange(params) + ) { + return updateSelectedValue(params); + } + + const nextValue = (() => { + if ( + params instanceof Temporal.PlainDate || + params instanceof Temporal.PlainDateTime + ) { + return params; + } + + if (value.start) { + return value.start.with(params); + } + + return mode === "daterange" + ? getNow().toPlainDate().with(params) + : getNow().with(params); + })(); + + if (value.start && !value.end) { + const sortedRange = [value.start, nextValue].sort( + Temporal.PlainDate.compare + ) as [Temporal.PlainDate, Temporal.PlainDate]; + + return updateSelectedValue({ + start: sortedRange[0], + end: sortedRange[1], + }); + } + + if (nextValue instanceof Temporal.PlainDate) { + return updateSelectedValue({ + start: nextValue, + end: undefined, + }); + } else { + return updateSelectedValue({ + start: nextValue, + end: undefined, + }); + } + }, + [mode, updateSelectedValue, value.end, value.start] + ); + + const previousMaxValue = useRef(maxValue); + const previousMinValue = useRef(minValue); + + useEffect(() => { + if (isEmptyRange(value)) { + return; + } + + if (!maxValue && !minValue) { + return; + } + + if (clampSelectedValue !== "always") { + return; + } + + if ( + maxValue !== previousMaxValue.current || + minValue !== previousMinValue.current + ) { + updateSelectedValue(value); + } + }, [clampSelectedValue, maxValue, minValue, updateSelectedValue, value]); + + useEffect(() => { + previousMaxValue.current = maxValue; + previousMinValue.current = minValue; + }); + + return { + calendarProps: { + locale, + maxValue: + maxValue instanceof Temporal.PlainDateTime + ? maxValue.toPlainDate() + : maxValue, + minValue: + minValue instanceof Temporal.PlainDateTime + ? minValue.toPlainDate() + : minValue, + value: calendarValue, + }, + years, + months, + hours, + minutes, + onChangeCalendarValue, + onChangeSelectedValue, + }; +} diff --git a/packages/react/tempocal-react.ts b/packages/react/tempocal-react.ts index 5c33f32..b70d494 100644 --- a/packages/react/tempocal-react.ts +++ b/packages/react/tempocal-react.ts @@ -1,2 +1,4 @@ export * from "./src/Calendar"; +export * from "./src/types"; export * from "./src/useTempocal"; +export * from "./src/useTempocalRange"; diff --git a/packages/www/components/Sidebar.tsx b/packages/www/components/Sidebar.tsx index 7f189e0..b22f9c8 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: ["useTempocal", "useTempocalRange", "Calendar"], }, { section: "core", @@ -40,6 +40,7 @@ const documentation = [ "getMonthEndDate", "getMonths", "getMonthStartDate", + "getNow", "getWeekdays", "getYears", "temporalToDate", diff --git a/packages/www/examples/DatePicker.tsx b/packages/www/examples/DatePicker.tsx index ba3f2e8..f06016e 100644 --- a/packages/www/examples/DatePicker.tsx +++ b/packages/www/examples/DatePicker.tsx @@ -1,5 +1,5 @@ import { Temporal } from "@js-temporal/polyfill"; -import { temporalToDate } from "@tempocal/core"; +import { getNow, temporalToDate } from "@tempocal/core"; import { Calendar, Locale, useTempocal } from "@tempocal/react"; import classnames from "classnames"; import * as React from "react"; @@ -14,7 +14,7 @@ const getDayContent = ({ year, month, day }: Temporal.PlainDate) => { return "⭐️"; } - const now = Temporal.Now.plainDate("iso8601"); + const now = getNow(); if (year === now.year && month === now.month && day === now.day) { return "📅"; diff --git a/packages/www/examples/DateRangePicker.tsx b/packages/www/examples/DateRangePicker.tsx index bf70f7f..3580fb5 100644 --- a/packages/www/examples/DateRangePicker.tsx +++ b/packages/www/examples/DateRangePicker.tsx @@ -4,7 +4,7 @@ import { getMonthStartDate, temporalToDate, } from "@tempocal/core"; -import { Calendar, DateRange, useTempocal } from "@tempocal/react"; +import { Calendar, DateRange, useTempocalRange } from "@tempocal/react"; import classnames from "classnames"; import * as React from "react"; import { CalendarHeader } from "../recipes/CalendarHeader"; @@ -24,10 +24,10 @@ export function DateRangePicker({ monthsBefore: number; monthsFixedGrid: boolean; }) { - const [values, setValues] = React.useState([ - Temporal.Now.plainDate("iso8601").subtract({ days: 3 }), - Temporal.Now.plainDate("iso8601").add({ days: 3 }), - ]); + const [values, setValues] = React.useState({ + start: Temporal.Now.plainDate("iso8601").subtract({ days: 3 }), + end: Temporal.Now.plainDate("iso8601").add({ days: 3 }), + }); const [minValue] = React.useState( Temporal.Now.plainDate("iso8601").subtract({ years: 2 }) @@ -45,7 +45,7 @@ export function DateRangePicker({ onChangeCalendarValue, onChangeSelectedValue, years, - } = useTempocal({ + } = useTempocalRange({ clampCalendarValue: true, locale, maxValue, @@ -92,21 +92,22 @@ export function DateRangePicker({ weekdayProps={() => ({ className: "font-medium" })} renderDay={({ date, disabled, plainDateLike }) => { const isRangeSelected = - values[0] && - values[1] && - Temporal.PlainDate.compare(values[0], date) <= 0 && - Temporal.PlainDate.compare(values[1], date) >= 0; + values.start && + values.end && + Temporal.PlainDate.compare(values.start, date) <= 0 && + Temporal.PlainDate.compare(values.end, date) >= 0; - const isSelected = values[0] && !values[1] && values[0].equals(date); + const isSelected = + values.start && !values.end && values.start.equals(date); const isRangeHovered = - values[0] && - !values[1] && + values.start && + !values.end && hoverValue && - ((Temporal.PlainDate.compare(values[0], date) <= 0 && + ((Temporal.PlainDate.compare(values.start, date) <= 0 && Temporal.PlainDate.compare(hoverValue, date) >= 0) || (Temporal.PlainDate.compare(hoverValue, date) <= 0 && - Temporal.PlainDate.compare(values[0], date) >= 0)); + Temporal.PlainDate.compare(values.start, date) >= 0)); return ( {`Selected date range: ${ - values[0] ? dateFormatter.format(temporalToDate(values[0])) : "" + values.start + ? dateFormatter.format(temporalToDate(values.start)) + : "" } - ${ - values[1] ? dateFormatter.format(temporalToDate(values[1])) : "" + values.end + ? dateFormatter.format(temporalToDate(values.end)) + : "" }`} diff --git a/packages/www/examples/DateTimePicker.tsx b/packages/www/examples/DateTimePicker.tsx index e40ad25..227c163 100644 --- a/packages/www/examples/DateTimePicker.tsx +++ b/packages/www/examples/DateTimePicker.tsx @@ -94,7 +94,7 @@ export function DateTimePicker({ title="Hours" value={value.hour} > - {hours.map(({ disabled, hour }) => ( + {hours?.map(({ disabled, hour }) => ( @@ -108,7 +108,7 @@ export function DateTimePicker({ value={value.minute} > {minutes - .filter(({ minute }) => minute % 5 === 0) + ?.filter(({ minute }) => minute % 5 === 0) .map(({ disabled, minute }) => (