diff --git a/packages/core/package.json b/packages/core/package.json index a4f9f29e..94a1f606 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -42,10 +42,10 @@ ], "dependencies": { "@formwerk/devtools": "workspace:*", - "@internationalized/date": "^3.7.0", "@standard-schema/spec": "1.0.0", "@standard-schema/utils": "^0.3.0", "klona": "^2.0.6", + "temporal-polyfill": "^0.3.0", "type-fest": "^4.37.0" }, "peerDependencies": { diff --git a/packages/core/src/i18n/useDateFormatter.ts b/packages/core/src/i18n/useDateFormatter.ts index 734354fc..ce027286 100644 --- a/packages/core/src/i18n/useDateFormatter.ts +++ b/packages/core/src/i18n/useDateFormatter.ts @@ -1,16 +1,16 @@ import { MaybeRefOrGetter, shallowRef, toValue, watch } from 'vue'; -import { DateFormatter } from '@internationalized/date'; import { getUserLocale } from './getUserLocale'; import { isEqual } from '../utils/common'; +import { Intl as TemporalIntl } from 'temporal-polyfill'; // TODO: May memory leak in SSR -const dateFormatterCache = new Map(); +const dateFormatterCache = new Map(); function getFormatter(locale: string, options: Intl.DateTimeFormatOptions = {}) { const cacheKey = locale + JSON.stringify(options); let formatter = dateFormatterCache.get(cacheKey); if (!formatter) { - formatter = new DateFormatter(locale, options); + formatter = new TemporalIntl.DateTimeFormat(locale, options); dateFormatterCache.set(cacheKey, formatter); } diff --git a/packages/core/src/i18n/useLocale.ts b/packages/core/src/i18n/useLocale.ts index 5bc58b7b..6551411f 100644 --- a/packages/core/src/i18n/useLocale.ts +++ b/packages/core/src/i18n/useLocale.ts @@ -3,14 +3,14 @@ import { getConfig } from '../config'; import { getDirection } from './getDirection'; import { getWeekInfo } from './getWeekInfo'; import { Maybe, Reactivify } from '../types'; -import { Calendar, GregorianCalendar } from '@internationalized/date'; import { getTimeZone } from './getTimezone'; +import { getCalendar } from './getCalendar'; export type NumberLocaleExtension = `nu-${string}`; export interface LocaleExtension { number: Maybe; - calendar: Maybe; + calendar: Maybe; timeZone: Maybe; } @@ -37,8 +37,8 @@ export function useLocale( } // Add the calendar locale extension if it's not already present - if (!code.includes('-ca-') && calExt?.identifier) { - code += `-ca-${calExt.identifier}`; + if (!code.includes('-ca-') && calExt) { + code += `-ca-${calExt}`; } code = code.replaceAll('--', '-'); @@ -49,7 +49,7 @@ export function useLocale( const localeInstance = computed(() => new Intl.Locale(localeString.value)); const direction = computed(() => getDirection(localeInstance.value)); const weekInfo = computed(() => getWeekInfo(localeInstance.value)); - const calendar = computed(() => toValue(extensions.calendar) ?? (new GregorianCalendar() as Calendar)); + const calendar = computed(() => toValue(extensions.calendar) ?? getCalendar(localeInstance.value)); const timeZone = computed(() => toValue(extensions.timeZone) ?? getTimeZone(localeInstance.value)); const locale = computed(() => localeInstance.value.toString()); diff --git a/packages/core/src/useCalendar/types.ts b/packages/core/src/useCalendar/types.ts index 6dc7082b..54e94ac3 100644 --- a/packages/core/src/useCalendar/types.ts +++ b/packages/core/src/useCalendar/types.ts @@ -1,11 +1,11 @@ import { WeekInfo } from '../i18n/getWeekInfo'; import { Ref } from 'vue'; import { Maybe } from '../types'; -import type { ZonedDateTime, Calendar } from '@internationalized/date'; +import { Temporal } from 'temporal-polyfill'; export interface CalendarDayCell { type: 'day'; - value: ZonedDateTime; + value: Temporal.ZonedDateTime; dayOfMonth: number; label: string; isToday: boolean; @@ -18,7 +18,7 @@ export interface CalendarDayCell { export interface CalendarMonthCell { type: 'month'; label: string; - value: ZonedDateTime; + value: Temporal.ZonedDateTime; monthOfYear: number; selected: boolean; disabled: boolean; @@ -28,7 +28,7 @@ export interface CalendarMonthCell { export interface CalendarYearCell { type: 'year'; label: string; - value: ZonedDateTime; + value: Temporal.ZonedDateTime; year: number; selected: boolean; disabled: boolean; @@ -42,12 +42,12 @@ export type CalendarViewType = 'weeks' | 'months' | 'years'; export interface CalendarContext { locale: Ref; weekInfo: Ref; - calendar: Ref; + calendar: Ref; timeZone: Ref; - getSelectedDate: () => ZonedDateTime; - getMinDate: () => Maybe; - getMaxDate: () => Maybe; - getFocusedDate: () => ZonedDateTime; - setFocusedDate: (date: ZonedDateTime) => void; - setDate: (date: ZonedDateTime, view?: CalendarViewType) => void; + getSelectedDate: () => Temporal.ZonedDateTime; + getMinDate: () => Maybe; + getMaxDate: () => Maybe; + getFocusedDate: () => Temporal.ZonedDateTime; + setFocusedDate: (date: Temporal.ZonedDateTime) => void; + setDate: (date: Temporal.ZonedDateTime, view?: CalendarViewType) => void; } diff --git a/packages/core/src/useCalendar/useCalendar.spec.ts b/packages/core/src/useCalendar/useCalendar.spec.ts index d33bde1f..71cc5e67 100644 --- a/packages/core/src/useCalendar/useCalendar.spec.ts +++ b/packages/core/src/useCalendar/useCalendar.spec.ts @@ -2,7 +2,8 @@ import { fireEvent, render, screen } from '@testing-library/vue'; import { axe } from 'vitest-axe'; import { useCalendar, CalendarCell } from './index'; import { flush } from '@test-utils/flush'; -import { createCalendar, fromDate } from '@internationalized/date'; +import { Temporal } from 'temporal-polyfill'; +import { fromZonedDateTimeToDate } from '../useDateTime/useTemporalStore'; describe('useCalendar', () => { describe('a11y', () => { @@ -44,7 +45,13 @@ describe('useCalendar', () => { describe('date selection', () => { test('calls onUpdateModelValue when a date is selected', async () => { - const currentDate = fromDate(new Date(2025, 2, 11), 'UTC'); + const currentDate = Temporal.ZonedDateTime.from({ + year: 2025, + month: 3, + day: 11, + timeZone: 'UTC', + calendar: 'gregory', + }); const vm = await render({ components: { @@ -54,7 +61,7 @@ describe('useCalendar', () => { const { calendarProps } = useCalendar({ label: 'Calendar', timeZone: 'UTC', - modelValue: currentDate.toDate(), + modelValue: new Date(currentDate.epochMilliseconds), }); return { @@ -74,11 +81,11 @@ describe('useCalendar', () => { await flush(); await fireEvent.click(screen.getByText('Select Date')); await flush(); - expect(vm.emitted('update:modelValue')[0]).toEqual([currentDate.toDate()]); + expect(vm.emitted('update:modelValue')[0]).toEqual([fromZonedDateTimeToDate(currentDate)]); }); test('uses provided calendar type', async () => { - const calendar = createCalendar('islamic-umalqura'); + const calendar = 'islamic-umalqura'; await render({ setup() { @@ -93,7 +100,7 @@ describe('useCalendar', () => { }, template: `
-
{{ selectedDate.calendar.identifier }}
+
{{ selectedDate.calendarId }}
`, }); @@ -103,7 +110,13 @@ describe('useCalendar', () => { }); test('handles Enter key on calendar cell', async () => { - const currentDate = fromDate(new Date(2025, 2, 11), 'UTC'); + const currentDate = Temporal.ZonedDateTime.from({ + year: 2025, + month: 3, + day: 11, + timeZone: 'UTC', + calendar: 'gregory', + }); const vm = await render({ components: { @@ -112,7 +125,7 @@ describe('useCalendar', () => { setup() { const { calendarProps, focusedDate } = useCalendar({ label: 'Calendar', - modelValue: currentDate.toDate(), + modelValue: new Date(currentDate.epochMilliseconds), timeZone: 'UTC', }); @@ -142,17 +155,23 @@ describe('useCalendar', () => { // Test Enter key selects the date await fireEvent.keyDown(cell, { code: 'Enter' }); - expect(vm.emitted('update:modelValue')[0]).toEqual([currentDate.toDate()]); + expect(vm.emitted('update:modelValue')[0]).toEqual([fromZonedDateTimeToDate(currentDate)]); }); test('handles Enter key in different panels', async () => { - const currentDate = fromDate(new Date(2025, 2, 11), 'UTC'); + const currentDate = Temporal.ZonedDateTime.from({ + year: 2025, + month: 3, + day: 11, + timeZone: 'UTC', + calendar: 'gregory', + }); const vm = await render({ setup() { const { calendarProps, focusedDate, gridLabelProps, currentView } = useCalendar({ label: 'Calendar', - modelValue: currentDate.toDate(), + modelValue: new Date(currentDate.epochMilliseconds), timeZone: 'UTC', }); @@ -180,7 +199,7 @@ describe('useCalendar', () => { // Test Enter in day panel await fireEvent.keyDown(calendar, { code: 'Enter' }); - expect(vm.emitted('update:modelValue')[0]).toEqual([currentDate.toDate()]); + expect(vm.emitted('update:modelValue')[0]).toEqual([fromZonedDateTimeToDate(currentDate)]); // Switch to month panel await fireEvent.click(panelLabel); @@ -255,13 +274,19 @@ describe('useCalendar', () => { }); test('navigates months using next/previous buttons in month panel', async () => { - const currentDate = fromDate(new Date(2025, 2, 11), 'UTC'); + const currentDate = Temporal.ZonedDateTime.from({ + year: 2025, + month: 3, + day: 11, + timeZone: 'UTC', + calendar: 'gregory', + }); await render({ setup() { const { nextButtonProps, previousButtonProps, gridLabelProps, focusedDate, calendarProps } = useCalendar({ label: 'Calendar', - modelValue: currentDate.toDate(), + modelValue: new Date(currentDate.epochMilliseconds), timeZone: 'UTC', }); @@ -309,13 +334,19 @@ describe('useCalendar', () => { }); test('navigates years using next/previous buttons in year panel', async () => { - const currentDate = fromDate(new Date(2025, 2, 11), 'UTC'); + const currentDate = Temporal.ZonedDateTime.from({ + year: 2025, + month: 3, + day: 11, + timeZone: 'UTC', + calendar: 'gregory', + }); await render({ setup() { const { nextButtonProps, previousButtonProps, gridLabelProps, focusedDate, calendarProps } = useCalendar({ label: 'Calendar', - modelValue: currentDate.toDate(), + modelValue: new Date(currentDate.epochMilliseconds), timeZone: 'UTC', }); @@ -352,7 +383,7 @@ describe('useCalendar', () => { screen.getByText( currentDate .add({ years: 9 }) - .set({ month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }) + .with({ month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }) .toString(), ), ).toBeInTheDocument(); @@ -363,7 +394,7 @@ describe('useCalendar', () => { screen.getByText( currentDate .add({ years: 8 }) - .set({ month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }) + .with({ month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }) .toString(), ), ).toBeInTheDocument(); @@ -375,7 +406,7 @@ describe('useCalendar', () => { screen.getByText( currentDate .subtract({ years: 10 }) - .set({ month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }) + .with({ month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }) .toString(), ), ).toBeInTheDocument(); @@ -385,7 +416,7 @@ describe('useCalendar', () => { screen.getByText( currentDate .subtract({ years: 9 }) - .set({ month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }) + .with({ month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }) .toString(), ), ).toBeInTheDocument(); @@ -394,13 +425,19 @@ describe('useCalendar', () => { describe('keyboard navigation', () => { test('handles arrow key navigation in day panel', async () => { - const currentDate = fromDate(new Date(2025, 2, 11), 'UTC'); + const currentDate = Temporal.ZonedDateTime.from({ + year: 2025, + month: 3, + day: 11, + timeZone: 'UTC', + calendar: 'gregory', + }); await render({ setup() { const { calendarProps, selectedDate, focusedDate } = useCalendar({ label: 'Calendar', - modelValue: currentDate.toDate(), + modelValue: new Date(currentDate.epochMilliseconds), timeZone: 'UTC', }); @@ -448,23 +485,27 @@ describe('useCalendar', () => { // Test Home (start of month) await fireEvent.keyDown(calendar, { code: 'Home' }); - expect(screen.getByText(currentDate.set({ day: 1 }).toString())).toBeInTheDocument(); + expect(screen.getByText(currentDate.with({ day: 1 }).toString())).toBeInTheDocument(); // Test End (end of month) await fireEvent.keyDown(calendar, { code: 'End' }); - expect( - screen.getByText(currentDate.set({ day: currentDate.calendar.getDaysInMonth(currentDate) }).toString()), - ).toBeInTheDocument(); + expect(screen.getByText(currentDate.with({ day: currentDate.daysInMonth }).toString())).toBeInTheDocument(); }); test('handles arrow key navigation in month panel', async () => { - const currentDate = fromDate(new Date(2025, 2, 11), 'UTC'); + const currentDate = Temporal.ZonedDateTime.from({ + year: 2025, + month: 3, + day: 11, + timeZone: 'UTC', + calendar: 'gregory', + }); await render({ setup() { const { calendarProps, selectedDate, focusedDate, gridLabelProps } = useCalendar({ label: 'Calendar', - modelValue: currentDate.toDate(), + modelValue: new Date(currentDate.epochMilliseconds), timeZone: 'UTC', }); @@ -518,23 +559,27 @@ describe('useCalendar', () => { // Test Home (start of year) await fireEvent.keyDown(calendar, { code: 'Home' }); - expect(screen.getByText(currentDate.set({ month: 1 }).toString())).toBeInTheDocument(); + expect(screen.getByText(currentDate.with({ month: 1 }).toString())).toBeInTheDocument(); // Test End (end of year) await fireEvent.keyDown(calendar, { code: 'End' }); - expect( - screen.getByText(currentDate.set({ month: currentDate.calendar.getMonthsInYear(currentDate) }).toString()), - ).toBeInTheDocument(); + expect(screen.getByText(currentDate.with({ month: currentDate.monthsInYear }).toString())).toBeInTheDocument(); }); test('handles arrow key navigation in year panel', async () => { - const currentDate = fromDate(new Date(2025, 2, 11), 'UTC'); + const currentDate = Temporal.ZonedDateTime.from({ + year: 2025, + month: 3, + day: 11, + timeZone: 'UTC', + calendar: 'gregory', + }); await render({ setup() { const { calendarProps, selectedDate, focusedDate, gridLabelProps } = useCalendar({ label: 'Calendar', - modelValue: currentDate.toDate(), + modelValue: new Date(currentDate.epochMilliseconds), timeZone: 'UTC', }); @@ -597,7 +642,14 @@ describe('useCalendar', () => { }); test('respects min and max date boundaries', async () => { - const currentDate = fromDate(new Date(2025, 2, 11), 'UTC'); + const currentDate = Temporal.ZonedDateTime.from({ + year: 2025, + month: 3, + day: 11, + timeZone: 'UTC', + calendar: 'gregory', + }); + const minDate = currentDate.subtract({ days: 1 }); const maxDate = currentDate.add({ days: 1 }); @@ -605,10 +657,10 @@ describe('useCalendar', () => { setup() { const { calendarProps, selectedDate, focusedDate } = useCalendar({ label: 'Calendar', - modelValue: currentDate.toDate(), + modelValue: new Date(currentDate.epochMilliseconds), timeZone: 'UTC', - min: minDate.toDate(), - max: maxDate.toDate(), + min: new Date(minDate.epochMilliseconds), + max: new Date(maxDate.epochMilliseconds), }); return { @@ -642,7 +694,13 @@ describe('useCalendar', () => { describe('disabled state', () => { test('prevents all interactions when disabled', async () => { - const currentDate = fromDate(new Date(2025, 2, 11), 'UTC'); + const currentDate = Temporal.ZonedDateTime.from({ + year: 2025, + month: 3, + day: 11, + timeZone: 'UTC', + calendar: 'gregory', + }); await render({ components: { @@ -652,7 +710,7 @@ describe('useCalendar', () => { const { calendarProps, gridLabelProps, nextButtonProps, previousButtonProps, focusedDate, currentView } = useCalendar({ label: 'Calendar', - modelValue: currentDate.toDate(), + modelValue: new Date(currentDate.epochMilliseconds), timeZone: 'UTC', disabled: true, }); @@ -722,7 +780,13 @@ describe('useCalendar', () => { describe('readonly state', () => { test('prevents all interactions when readonly', async () => { - const currentDate = fromDate(new Date(2025, 2, 11), 'UTC'); + const currentDate = Temporal.ZonedDateTime.from({ + year: 2025, + month: 3, + day: 11, + timeZone: 'UTC', + calendar: 'gregory', + }); await render({ components: { @@ -732,7 +796,7 @@ describe('useCalendar', () => { const { calendarProps, gridLabelProps, nextButtonProps, previousButtonProps, focusedDate, currentView } = useCalendar({ label: 'Calendar', - modelValue: currentDate.toDate(), + modelValue: new Date(currentDate.epochMilliseconds), timeZone: 'UTC', readonly: true, }); diff --git a/packages/core/src/useCalendar/useCalendar.ts b/packages/core/src/useCalendar/useCalendar.ts index 151b26a7..ebe1638d 100644 --- a/packages/core/src/useCalendar/useCalendar.ts +++ b/packages/core/src/useCalendar/useCalendar.ts @@ -9,7 +9,6 @@ import { useLabel } from '../a11y'; import { useControlButtonProps } from '../helpers/useControlButtonProps'; import { CalendarContextKey, YEAR_CELLS_COUNT } from './constants'; import { CalendarView, useCalendarView } from './useCalendarView'; -import { Calendar, ZonedDateTime, now, toCalendar } from '@internationalized/date'; import { exposeField, FormField, useFormField } from '../useFormField'; import { useInputValidity } from '../validation'; import { fromDateToCalendarZonedDateTime, useTemporalStore } from '../useDateTime/useTemporalStore'; @@ -17,6 +16,7 @@ import { PickerContextKey } from '../usePicker'; import { registerField } from '@formwerk/devtools'; import { useConstraintsValidator } from '../validation/useConstraintsValidator'; import { createDisabledContext } from '../helpers/createDisabledContext'; +import { Temporal } from 'temporal-polyfill'; export interface CalendarProps { /** @@ -52,7 +52,7 @@ export interface CalendarProps { /** * The calendar type to use for the calendar, e.g. `gregory`, `islamic-umalqura`, etc. */ - calendar?: Calendar; + calendar?: string; /** * The time zone to use for the calendar. @@ -165,8 +165,16 @@ export function useCalendar(_props: Reactivify temporalValue.value ?? toCalendar(now(toValue(timeZone)), calendar.value)); - const focusedDay = shallowRef(); + const selectedDate = computed( + () => + temporalValue.value ?? + Temporal.ZonedDateTime.from({ + epochMilliseconds: Temporal.Now.instant().epochMilliseconds, + timeZone: timeZone.value, + }).withCalendar(calendar.value), + ); + + const focusedDay = shallowRef(); function getFocusedOrSelected() { if (focusedDay.value) { @@ -187,7 +195,7 @@ export function useCalendar(_props: Reactivify selectedDate.value, getFocusedDate: getFocusedOrSelected, setDate, - setFocusedDate: async (date: ZonedDateTime) => { + setFocusedDate: async (date: Temporal.ZonedDateTime) => { if (isDisabled.value || toValue(props.readonly)) { return; } @@ -215,7 +223,7 @@ export function useCalendar(_props: Reactivify ZonedDateTime | undefined; + fn: () => Temporal.ZonedDateTime | undefined; type: 'focus' | 'select'; } export function useCalendarKeyboard(context: CalendarContext, currentPanel: Ref) { - function withCheckedBounds(fn: () => ZonedDateTime | undefined) { + function withCheckedBounds(fn: () => Temporal.ZonedDateTime | undefined) { const date = fn(); if (!date) { return undefined; @@ -450,7 +458,11 @@ export function useCalendarKeyboard(context: CalendarContext, currentPanel: Ref< const minDate = context.getMinDate(); const maxDate = context.getMaxDate(); - if (date && ((minDate && date.compare(minDate) < 0) || (maxDate && date.compare(maxDate) > 0))) { + if ( + date && + ((minDate && Temporal.ZonedDateTime.compare(date, minDate) < 0) || + (maxDate && Temporal.ZonedDateTime.compare(date, maxDate) > 0)) + ) { return undefined; } @@ -539,21 +551,21 @@ export function useCalendarKeyboard(context: CalendarContext, currentPanel: Ref< const type = currentPanel.value.type; if (type === 'weeks') { if (current.day === 1) { - return current.subtract({ months: 1 }).set({ day: 1 }); + return current.subtract({ months: 1 }).with({ day: 1 }); } - return current.set({ day: 1 }); + return current.with({ day: 1 }); } if (type === 'months') { if (current.month === 1) { - return current.subtract({ years: 1 }).set({ month: 1 }); + return current.subtract({ years: 1 }).with({ month: 1 }); } - return current.set({ month: 1 }); + return current.with({ month: 1 }); } - return current.set({ year: current.year - YEAR_CELLS_COUNT }); + return current.with({ year: current.year - YEAR_CELLS_COUNT }); }, type: 'focus', }, @@ -563,22 +575,22 @@ export function useCalendarKeyboard(context: CalendarContext, currentPanel: Ref< const type = currentPanel.value.type; const current = context.getFocusedDate(); if (type === 'weeks') { - if (current.day === current.calendar.getDaysInMonth(current)) { - return current.add({ months: 1 }).set({ day: 1 }); + if (current.day === current.daysInMonth) { + return current.add({ months: 1 }).with({ day: 1 }); } - return current.set({ day: current.calendar.getDaysInMonth(current) }); + return current.with({ day: current.daysInMonth }); } if (type === 'months') { - if (current.month === current.calendar.getMonthsInYear(current)) { - return current.add({ years: 1 }).set({ month: 1 }); + if (current.month === current.monthsInYear) { + return current.add({ years: 1 }).with({ month: 1 }); } - return current.set({ month: current.calendar.getMonthsInYear(current) }); + return current.with({ month: current.monthsInYear }); } - return current.set({ year: current.year + YEAR_CELLS_COUNT }); + return current.with({ year: current.year + YEAR_CELLS_COUNT }); }, }, Escape: { @@ -586,7 +598,7 @@ export function useCalendarKeyboard(context: CalendarContext, currentPanel: Ref< fn: () => { const selected = context.getSelectedDate(); const focused = context.getFocusedDate(); - if (selected.compare(focused) !== 0) { + if (Temporal.ZonedDateTime.compare(selected, focused) !== 0) { return context.getSelectedDate(); } diff --git a/packages/core/src/useCalendar/useCalendarView.ts b/packages/core/src/useCalendar/useCalendarView.ts index d7ea605f..0cdf1197 100644 --- a/packages/core/src/useCalendar/useCalendarView.ts +++ b/packages/core/src/useCalendar/useCalendarView.ts @@ -4,8 +4,7 @@ import { useDateFormatter } from '../i18n'; import { Reactivify } from '../types'; import { normalizeProps } from '../utils/common'; import { YEAR_CELLS_COUNT } from './constants'; -import { now, toCalendar, toCalendarDate } from '@internationalized/date'; - +import { Temporal } from 'temporal-polyfill'; export interface CalendarWeeksView { type: 'weeks'; days: CalendarDayCell[]; @@ -76,14 +75,14 @@ export function useCalendarView(_props: Reactivify, context: const viewLabel = computed(() => { if (viewType.value === 'weeks') { - return `${monthFormatter.value.format(context.getFocusedDate().toDate())} ${yearFormatter.value.format(context.getFocusedDate().toDate())}`; + return `${monthFormatter.value.format(context.getFocusedDate().toPlainDateTime())} ${yearFormatter.value.format(context.getFocusedDate().toPlainDateTime())}`; } if (viewType.value === 'months') { - return yearFormatter.value.format(context.getFocusedDate().toDate()); + return yearFormatter.value.format(context.getFocusedDate().toPlainDateTime()); } - return `${yearFormatter.value.format(years.value[0].value.toDate())} - ${yearFormatter.value.format(years.value[years.value.length - 1].value.toDate())}`; + return `${yearFormatter.value.format(years.value[0].value.toPlainDateTime())} - ${yearFormatter.value.format(years.value[years.value.length - 1].value.toPlainDateTime())}`; }); return { currentView, setView, viewLabel }; @@ -99,10 +98,10 @@ function useCalendarDaysView( const days = computed(() => { const current = getSelectedDate(); const focused = getFocusedDate(); - const startOfMonth = focused.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }); + const startOfMonth = focused.with({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }); const firstDayOfWeek = weekInfo.value.firstDay; - const startDayOfWeek = startOfMonth.toDate().getDay(); + const startDayOfWeek = startOfMonth.day; const daysToSubtract = (startDayOfWeek - firstDayOfWeek + 7) % 7; // Move to first day of week @@ -110,36 +109,36 @@ function useCalendarDaysView( // Always use 6 weeks (42 days) for consistent layout const gridDays = 42; - const rightNow = toCalendar(now(timeZone.value), calendar.value); + const rightNow = Temporal.Now.zonedDateTimeISO(timeZone.value).withCalendar(calendar.value); + const minDate = getMinDate(); const maxDate = getMaxDate(); - - const rightNowDate = toCalendarDate(rightNow); - const focusedDate = toCalendarDate(focused); - const currentDate = toCalendarDate(current); + const currentPlain = current.toPlainDate(); + const focusedPlain = focused.toPlainDate(); + const rightNowPlain = rightNow.toPlainDate(); return Array.from({ length: gridDays }, (_, i) => { const dayOfMonth = firstDay.add({ days: i }); let disabled = false; - if (minDate && dayOfMonth.compare(minDate) < 0) { + if (minDate && Temporal.ZonedDateTime.compare(dayOfMonth, minDate) < 0) { disabled = true; } - if (maxDate && dayOfMonth.compare(maxDate) > 0) { + if (maxDate && Temporal.ZonedDateTime.compare(dayOfMonth, maxDate) > 0) { disabled = true; } - const domDate = toCalendarDate(dayOfMonth); + const domDate = dayOfMonth.toPlainDate(); return { value: dayOfMonth, - label: dayNumberFormatter.value.format(dayOfMonth.toDate()), + label: dayNumberFormatter.value.format(dayOfMonth.toPlainDateTime()), dayOfMonth: dayOfMonth.day, - isToday: rightNowDate.compare(domDate) === 0, - selected: currentDate.compare(domDate) === 0, - isOutsideMonth: domDate.month !== focusedDate.month, - focused: focusedDate.compare(domDate) === 0, + isToday: rightNowPlain.equals(domDate), + selected: currentPlain.equals(domDate), + isOutsideMonth: domDate.month !== focusedPlain.month, + focused: focusedPlain.equals(domDate), disabled, type: 'day', } as CalendarDayCell; @@ -151,7 +150,7 @@ function useCalendarDaysView( const daysPerWeek = 7; const firstDayOfWeek = weekInfo.value.firstDay; // Get the current date's day of week (0-6) - const currentDayOfWeek = focused.toDate().getDay(); + const currentDayOfWeek = focused.day; // Calculate how many days to go back to reach first day of week const daysToSubtract = (currentDayOfWeek - firstDayOfWeek + 7) % 7; @@ -161,7 +160,7 @@ function useCalendarDaysView( const days: string[] = []; for (let i = 0; i < daysPerWeek; i++) { - days.push(dayFormatter.value.format(focused.add({ days: i }).toDate())); + days.push(dayFormatter.value.format(focused.add({ days: i }).toPlainDateTime())); } return days; @@ -182,8 +181,8 @@ function useCalendarMonthsView( const minDate = getMinDate(); const maxDate = getMaxDate(); - return Array.from({ length: focused.calendar.getMonthsInYear(focused) }, (_, i) => { - const date = focused.set({ month: i + 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }); + return Array.from({ length: focused.monthsInYear }, (_, i) => { + const date = focused.with({ month: i + 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }); let disabled = false; if (minDate && minDate.month < date.month) { @@ -196,7 +195,7 @@ function useCalendarMonthsView( const cell: CalendarMonthCell = { type: 'month', - label: monthFormatter.value.format(date.toDate()), + label: monthFormatter.value.format(date.toPlainDateTime()), value: date, monthOfYear: date.month, selected: date.month === current.month && date.year === current.year, @@ -225,7 +224,7 @@ function useCalendarYearsView( return Array.from({ length: YEAR_CELLS_COUNT }, (_, i) => { const startYear = Math.floor(focused.year / YEAR_CELLS_COUNT) * YEAR_CELLS_COUNT; - const date = focused.set({ + const date = focused.with({ year: startYear + i, month: 1, day: 1, @@ -247,7 +246,7 @@ function useCalendarYearsView( const cell: CalendarYearCell = { type: 'year', - label: yearFormatter.value.format(date.toDate()), + label: yearFormatter.value.format(date.toPlainDateTime()), value: date, year: date.year, selected: date.year === current.year, diff --git a/packages/core/src/useDateTime/constants.ts b/packages/core/src/useDateTime/constants.ts index daf9d729..84f690c8 100644 --- a/packages/core/src/useDateTime/constants.ts +++ b/packages/core/src/useDateTime/constants.ts @@ -1,5 +1,5 @@ import { DateTimeSegmentType } from './types'; -import type { DateTimeDuration, ZonedDateTime } from '@internationalized/date'; +import type { Temporal } from 'temporal-polyfill'; export function isEditableSegmentType(type: DateTimeSegmentType) { return !['era', 'timeZoneName', 'literal'].includes(type); @@ -11,8 +11,8 @@ export function isOptionalSegmentType(type: DateTimeSegmentType) { return optionalTypes.includes(type); } -export function segmentTypeToDurationLike(type: DateTimeSegmentType): keyof DateTimeDuration | undefined { - const map: Partial> = { +export function segmentTypeToDurationLike(type: DateTimeSegmentType): keyof Temporal.DurationLike | undefined { + const map: Partial> = { year: 'years', month: 'months', day: 'days', @@ -60,7 +60,7 @@ export function getOrderedSegmentTypes(): EditableSegmentType[] { return ['year', 'month', 'day', 'hour', 'minute', 'second']; } -export function isEqualPart(min: ZonedDateTime, max: ZonedDateTime, part: DateTimeSegmentType) { +export function isEqualPart(min: Temporal.ZonedDateTime, max: Temporal.ZonedDateTime, part: DateTimeSegmentType) { const editablePart = part as EditableSegmentType; const parts = getOrderedSegmentTypes(); const idx = parts.indexOf(editablePart); diff --git a/packages/core/src/useDateTime/temporalPartial.spec.ts b/packages/core/src/useDateTime/temporalPartial.spec.ts index f25a5edc..a111c1ba 100644 --- a/packages/core/src/useDateTime/temporalPartial.spec.ts +++ b/packages/core/src/useDateTime/temporalPartial.spec.ts @@ -1,11 +1,11 @@ -import { createCalendar, now } from '@internationalized/date'; +import { Temporal } from 'temporal-polyfill'; import { createTemporalPartial, isTemporalPartial, isTemporalPartSet, toTemporalPartial } from './temporalPartial'; import { DateTimeSegmentType } from './types'; describe('Temporal Partial', () => { describe('createTemporalPartial', () => { test('creates a temporal partial with empty set parts', () => { - const calendar = createCalendar('gregory'); + const calendar = 'gregory'; const partial = createTemporalPartial(calendar, 'UTC'); expect(partial['~fw_temporal_partial']).toEqual({}); @@ -13,17 +13,17 @@ describe('Temporal Partial', () => { }); test('creates temporal partial with different calendar systems', () => { - const islamicCalendar = createCalendar('islamic-umalqura'); + const islamicCalendar = 'islamic-umalqura'; const partial = createTemporalPartial(islamicCalendar, 'UTC'); - expect(partial.calendar.identifier).toBe('islamic-umalqura'); + expect(partial.calendarId).toBe('islamic-umalqura'); expect(isTemporalPartial(partial)).toBe(true); }); }); describe('toTemporalPartial', () => { test('converts ZonedDateTime to temporal partial', () => { - const date = now('UTC'); + const date = Temporal.Now.zonedDateTimeISO('UTC'); const partial = toTemporalPartial(date); expect(isTemporalPartial(partial)).toBe(true); @@ -31,7 +31,7 @@ describe('Temporal Partial', () => { }); test('clones existing temporal partial', () => { - const date = now('UTC'); + const date = Temporal.Now.zonedDateTimeISO('UTC'); const partial1 = toTemporalPartial(date, ['day']); const partial2 = toTemporalPartial(partial1); @@ -40,7 +40,7 @@ describe('Temporal Partial', () => { }); test('sets specified parts as true', () => { - const date = now('UTC'); + const date = Temporal.Now.zonedDateTimeISO('UTC'); const parts: DateTimeSegmentType[] = ['year', 'month', 'day']; const partial = toTemporalPartial(date, parts); @@ -50,7 +50,7 @@ describe('Temporal Partial', () => { }); test('preserves existing set parts when adding new ones', () => { - const date = now('UTC'); + const date = Temporal.Now.zonedDateTimeISO('UTC'); const partial1 = toTemporalPartial(date, ['year']); const partial2 = toTemporalPartial(partial1, ['month']); @@ -63,14 +63,14 @@ describe('Temporal Partial', () => { describe('isTemporalPartial', () => { test('returns true for temporal partials', () => { - const calendar = createCalendar('gregory'); + const calendar = 'gregory'; const partial = createTemporalPartial(calendar, 'UTC'); expect(isTemporalPartial(partial)).toBe(true); }); test('returns false for regular ZonedDateTime', () => { - const date = now('UTC'); + const date = Temporal.Now.zonedDateTimeISO('UTC'); expect(isTemporalPartial(date)).toBe(false); }); @@ -78,7 +78,7 @@ describe('Temporal Partial', () => { describe('isTemporalPartSet', () => { test('returns true for set parts', () => { - const date = now('UTC'); + const date = Temporal.Now.zonedDateTimeISO('UTC'); const partial = toTemporalPartial(date, ['year', 'month']); expect(isTemporalPartSet(partial, 'year')).toBe(true); @@ -86,7 +86,7 @@ describe('Temporal Partial', () => { }); test('returns false for unset parts', () => { - const date = now('UTC'); + const date = Temporal.Now.zonedDateTimeISO('UTC'); const partial = toTemporalPartial(date, ['year']); expect(isTemporalPartSet(partial, 'month')).toBe(false); @@ -94,7 +94,7 @@ describe('Temporal Partial', () => { }); test('handles multiple operations on the same partial', () => { - const date = now('UTC'); + const date = Temporal.Now.zonedDateTimeISO('UTC'); let partial = toTemporalPartial(date, ['year']); partial = toTemporalPartial(partial, ['month']); partial = toTemporalPartial(partial, ['day']); @@ -107,7 +107,7 @@ describe('Temporal Partial', () => { }); test('temporal partial maintains date values', () => { - const date = now('UTC'); + const date = Temporal.Now.zonedDateTimeISO('UTC'); const partial = toTemporalPartial(date, ['year', 'month', 'day']); expect(partial.year).toBe(date.year); diff --git a/packages/core/src/useDateTime/temporalPartial.ts b/packages/core/src/useDateTime/temporalPartial.ts index ee779b86..073a633e 100644 --- a/packages/core/src/useDateTime/temporalPartial.ts +++ b/packages/core/src/useDateTime/temporalPartial.ts @@ -1,23 +1,23 @@ -import { DateTimeSegmentType, TemporalPartial } from './types'; +import { DateTimeSegmentType, TemporalPartial, TemporalType } from './types'; import { isObject } from '../../../shared/src'; -import { Calendar, ZonedDateTime, now, toCalendar } from '@internationalized/date'; import { Maybe } from '../types'; import { getOrderedSegmentTypes, isEqualPart } from './constants'; +import { Temporal } from 'temporal-polyfill'; export function createTemporalPartial( - calendar: Calendar, + calendar: string, timeZone: string, - min?: Maybe, - max?: Maybe, + min?: Maybe, + max?: Maybe, ) { if (min && max) { // Get the middle of the min and max - const diff = Math.round(max.compare(min) / 2); + const diff = Math.round(max.since(min, { largestUnit: 'milliseconds' }).milliseconds / 2); const zonedDateTime = min .add({ milliseconds: diff, }) - .set({ + .with({ hour: 0, minute: 0, second: 0, @@ -34,7 +34,7 @@ export function createTemporalPartial( return zonedDateTime; } - const zonedDateTime = toCalendar(now(timeZone), calendar).set({ + const zonedDateTime = Temporal.Now.zonedDateTimeISO(timeZone).withCalendar(calendar).with({ hour: 0, minute: 0, second: 0, @@ -46,10 +46,10 @@ export function createTemporalPartial( } export function toTemporalPartial( - value: ZonedDateTime | TemporalPartial, + value: Temporal.ZonedDateTime | TemporalPartial, setParts?: DateTimeSegmentType[], ): TemporalPartial { - const clone = value.copy() as TemporalPartial; + const clone = Temporal.ZonedDateTime.from(value) as TemporalPartial; clone['~fw_temporal_partial'] = isTemporalPartial(value) ? value['~fw_temporal_partial'] : {}; if (setParts) { setParts.forEach(part => { @@ -60,7 +60,7 @@ export function toTemporalPartial( return clone as TemporalPartial; } -export function isTemporalPartial(value: ZonedDateTime): value is TemporalPartial { +export function isTemporalPartial(value: TemporalType | TemporalPartial): value is TemporalPartial { return isObject((value as TemporalPartial)['~fw_temporal_partial']); } diff --git a/packages/core/src/useDateTime/types.ts b/packages/core/src/useDateTime/types.ts index a35a1ccc..5d2cb6ee 100644 --- a/packages/core/src/useDateTime/types.ts +++ b/packages/core/src/useDateTime/types.ts @@ -1,4 +1,4 @@ -import { ZonedDateTime } from '@internationalized/date'; +import { Temporal } from 'temporal-polyfill'; /** * lib.es2017.intl.d.ts @@ -16,9 +16,11 @@ export type DateTimeSegmentType = | 'weekday' | 'year'; -export type DateValue = Date | ZonedDateTime; +export type DateValue = Date | Temporal.ZonedDateTime; -export type TemporalPartial = ZonedDateTime & { +export type TemporalType = Temporal.ZonedDateTime | Temporal.PlainDateTime | Temporal.PlainDate | Temporal.PlainTime; + +export type TemporalPartial = TTemp & { [`~fw_temporal_partial`]: { [key: string]: boolean | undefined; }; diff --git a/packages/core/src/useDateTime/useDateField.spec.ts b/packages/core/src/useDateTime/useDateField.spec.ts index 02436ee9..1c4b9720 100644 --- a/packages/core/src/useDateTime/useDateField.spec.ts +++ b/packages/core/src/useDateTime/useDateField.spec.ts @@ -2,11 +2,11 @@ import { render, screen } from '@testing-library/vue'; import { axe } from 'vitest-axe'; import { useDateField } from '.'; import { flush } from '@test-utils/flush'; -import { createCalendar, now, toCalendar } from '@internationalized/date'; import { DateTimeSegment } from './useDateTimeSegment'; import { ref, toValue } from 'vue'; import { StandardSchema } from '../types'; import { fireEvent } from '@testing-library/vue'; +import { Temporal } from 'temporal-polyfill'; describe('useDateField', () => { const currentDate = new Date('2024-03-15T12:00:00Z'); @@ -96,8 +96,8 @@ describe('useDateField', () => { describe('calendar systems', () => { test('supports different calendar systems', async () => { - const calendar = createCalendar('islamic-umalqura'); - const date = toCalendar(now('UTC'), calendar).set({ year: 1445, month: 9, day: 5 }); // Islamic date + const calendar = 'islamic-umalqura'; + const date = Temporal.Now.zonedDateTimeISO('UTC').withCalendar(calendar).with({ year: 1445, month: 9, day: 5 }); // Islamic date await render({ components: { DateTimeSegment }, @@ -106,7 +106,7 @@ describe('useDateField', () => { label: 'Date', name: 'date', calendar, - value: date.toDate(), + value: new Date(date.epochMilliseconds), }); return { @@ -370,8 +370,8 @@ describe('useDateField', () => { describe('constraints', () => { test('respects min and max date constraints', async () => { - const minDate = now('UTC'); - const maxDate = now('UTC').add({ days: 1 }); + const minDate = Temporal.Now.zonedDateTimeISO('UTC'); + const maxDate = Temporal.Now.zonedDateTimeISO('UTC').add({ days: 1 }); await render({ components: { DateTimeSegment }, @@ -380,15 +380,15 @@ describe('useDateField', () => { label: 'Date', name: 'date', timeZone: 'UTC', - min: minDate.toDate(), - max: maxDate.toDate(), + min: new Date(minDate.epochMilliseconds), + max: new Date(maxDate.epochMilliseconds), value: currentDate, }); const { segments, controlProps, labelProps } = props; - expect(toValue(props.calendarProps.value.min)).toEqual(minDate.toDate()); - expect(toValue(props.calendarProps.value.max)).toEqual(maxDate.toDate()); + expect(toValue(props.calendarProps.value.min?.getTime())).toEqual(minDate.epochMilliseconds); + expect(toValue(props.calendarProps.value.max?.getTime())).toEqual(maxDate.epochMilliseconds); return { segments, diff --git a/packages/core/src/useDateTime/useDateField.ts b/packages/core/src/useDateTime/useDateField.ts index 35fd1fd0..c9baad9a 100644 --- a/packages/core/src/useDateTime/useDateField.ts +++ b/packages/core/src/useDateTime/useDateField.ts @@ -8,10 +8,10 @@ import { FieldTypePrefixes } from '../constants'; import { useDateFormatter, useLocale } from '../i18n'; import { useErrorMessage, useLabel } from '../a11y'; import { fromDateToCalendarZonedDateTime, useTemporalStore } from './useTemporalStore'; -import { ZonedDateTime, Calendar } from '@internationalized/date'; import { useInputValidity } from '../validation'; import { registerField } from '@formwerk/devtools'; import { useConstraintsValidator } from '../validation/useConstraintsValidator'; +import { Temporal } from 'temporal-polyfill'; export interface DateFieldProps { /** @@ -37,7 +37,7 @@ export interface DateFieldProps { /** * The calendar type to use for the field, e.g. `gregory`, `islamic-umalqura`, etc. */ - calendar?: Calendar; + calendar?: string; /** * The time zone to use for the field, e.g. `UTC`, `America/New_York`, etc. @@ -139,7 +139,7 @@ export function useDateField(_props: Reactivify) { max, }); - function onValueChange(value: ZonedDateTime) { + function onValueChange(value: Temporal.ZonedDateTime) { temporalValue.value = value; } diff --git a/packages/core/src/useDateTime/useDateTimeSegmentGroup.spec.ts b/packages/core/src/useDateTime/useDateTimeSegmentGroup.spec.ts index a4d24a81..2c453560 100644 --- a/packages/core/src/useDateTime/useDateTimeSegmentGroup.spec.ts +++ b/packages/core/src/useDateTime/useDateTimeSegmentGroup.spec.ts @@ -1,11 +1,10 @@ -import { DateFormatter, fromDate } from '@internationalized/date'; import { useDateTimeSegmentGroup } from './useDateTimeSegmentGroup'; -import { Ref, ref } from 'vue'; +import { ref } from 'vue'; import { fireEvent, render, screen } from '@testing-library/vue'; import { flush } from '@test-utils/flush'; import { DateTimeSegment } from './useDateTimeSegment'; import { createTemporalPartial, isTemporalPartial } from './temporalPartial'; -import { TemporalPartial } from './types'; +import { Temporal, Intl as TemporalIntl } from 'temporal-polyfill'; function dispatchEvent() { // NOOP @@ -14,10 +13,15 @@ function dispatchEvent() { describe('useDateTimeSegmentGroup', () => { const timeZone = 'UTC'; const locale = 'en-US'; - const currentDate = fromDate(new Date('2025-02-11'), timeZone); + const currentDate = Temporal.ZonedDateTime.from({ + timeZone, + year: 2025, + month: 2, + day: 11, + }); function createFormatter() { - return new DateFormatter(locale, { + return new TemporalIntl.DateTimeFormat(locale, { day: 'numeric', month: 'numeric', year: 'numeric', @@ -284,7 +288,7 @@ describe('useDateTimeSegmentGroup', () => { }); monthRegistration.setValue(6); - expect(onValueChange).toHaveBeenCalledWith(currentDate.set({ month: 6 })); + expect(onValueChange).toHaveBeenCalledWith(currentDate.with({ month: 6 })); }); test('clears segment values', async () => { @@ -542,7 +546,7 @@ describe('useDateTimeSegmentGroup', () => { test('handles non-numeric segments (dayPeriod)', async () => { const formatter = ref( - new DateFormatter(locale, { + new TemporalIntl.DateTimeFormat(locale, { hour: 'numeric', hour12: true, dayPeriod: 'short', @@ -598,11 +602,11 @@ describe('useDateTimeSegmentGroup', () => { // Test arrow up changes period (AM -> PM) await fireEvent.keyDown(dayPeriodSegment, { code: 'ArrowUp' }); - expect(onValueChange).toHaveBeenCalledWith(currentDate.add({ hours: 12 }).set({ day: currentDate.day })); + expect(onValueChange).toHaveBeenCalledWith(currentDate.add({ hours: 12 }).with({ day: currentDate.day })); // Test arrow down changes period (PM -> AM) await fireEvent.keyDown(dayPeriodSegment, { code: 'ArrowDown' }); - expect(onValueChange).toHaveBeenCalledWith(currentDate.subtract({ hours: 12 }).set({ day: currentDate.day })); + expect(onValueChange).toHaveBeenCalledWith(currentDate.subtract({ hours: 12 }).with({ day: currentDate.day })); // Test clearing with backspace await fireEvent.keyDown(dayPeriodSegment, { code: 'Backspace' }); @@ -619,7 +623,7 @@ describe('useDateTimeSegmentGroup', () => { const formatter = ref(createFormatter()); const controlEl = ref(); const onValueChange = vi.fn(); - const initialDate = currentDate.set({ year: 2024, month: 1, day: 1 }); + const initialDate = currentDate.with({ year: 2024, month: 1, day: 1 }); await render({ components: { @@ -628,7 +632,7 @@ describe('useDateTimeSegmentGroup', () => { setup() { const { segments } = useDateTimeSegmentGroup({ formatter, - temporalValue: createTemporalPartial(initialDate.calendar, initialDate.timeZone), + temporalValue: createTemporalPartial(initialDate.calendarId, initialDate.timeZoneId), formatOptions: { day: 'numeric', month: 'numeric', @@ -710,16 +714,14 @@ describe('useDateTimeSegmentGroup', () => { // Verify final call is not a partial const finalCall = onValueChange.mock.lastCall?.[0]; expect(isTemporalPartial(finalCall)).toBe(false); - expect(finalCall.toString()).toBe(initialDate.set({ month: 3, day: 5 }).toString()); + expect(finalCall.toString()).toBe(initialDate.with({ month: 3, day: 5 }).toString()); }); test('preserves partial state when not all segments are filled', async () => { const formatter = ref(createFormatter()); const controlEl = ref(); - const initialDate = currentDate.set({ year: 2024, month: 1, day: 1 }); - const temporalValue = ref( - createTemporalPartial(initialDate.calendar, initialDate.timeZone), - ) as Ref; + const initialDate = currentDate.with({ year: 2024, month: 1, day: 1 }); + const temporalValue = ref(createTemporalPartial(initialDate.calendarId, initialDate.timeZoneId)); const onValueChange = vi.fn(v => { temporalValue.value = v; }); @@ -775,10 +777,12 @@ describe('useDateTimeSegmentGroup', () => { daySegment.focus(); const dayInput = new InputEvent('beforeinput', { data: '5', cancelable: true }); daySegment.dispatchEvent(dayInput); + screen.debug(); // Verify the value is still a partial since year is not set const lastCall = onValueChange.mock.lastCall?.[0]; expect(isTemporalPartial(lastCall)).toBe(true); + expect(lastCall['~fw_temporal_partial']).toEqual({ day: true, month: true, diff --git a/packages/core/src/useDateTime/useDateTimeSegmentGroup.ts b/packages/core/src/useDateTime/useDateTimeSegmentGroup.ts index 5696925c..ab667f7d 100644 --- a/packages/core/src/useDateTime/useDateTimeSegmentGroup.ts +++ b/packages/core/src/useDateTime/useDateTimeSegmentGroup.ts @@ -14,7 +14,7 @@ import { } from './constants'; import { NumberParserContext, useNumberParser } from '../i18n'; import { isTemporalPartial, isTemporalPartSet, toTemporalPartial } from './temporalPartial'; -import { ZonedDateTime, DateFormatter } from '@internationalized/date'; +import { Temporal, Intl as TemporalIntl } from 'temporal-polyfill'; export interface DateTimeSegmentRegistration { id: string; @@ -43,16 +43,16 @@ export interface DateTimeSegmentGroupContext { export const DateTimeSegmentGroupKey: InjectionKey = Symbol('DateTimeSegmentGroupKey'); export interface DateTimeSegmentGroupProps { - formatter: Ref; + formatter: Ref; locale: MaybeRefOrGetter; formatOptions: MaybeRefOrGetter>; - temporalValue: MaybeRefOrGetter; + temporalValue: MaybeRefOrGetter; direction?: MaybeRefOrGetter; controlEl: Ref; readonly?: MaybeRefOrGetter; - min?: MaybeRefOrGetter>; - max?: MaybeRefOrGetter>; - onValueChange: (value: ZonedDateTime) => void; + min?: MaybeRefOrGetter>; + max?: MaybeRefOrGetter>; + onValueChange: (value: Temporal.ZonedDateTime) => void; onTouched: () => void; dispatchEvent: (type: string) => void; } @@ -85,7 +85,7 @@ export function useDateTimeSegmentGroup({ const segments = computed(() => { const date = toValue(temporalValue); - let parts = formatter.value.formatToParts(date.toDate()) as { + let parts = formatter.value.formatToParts(date.toPlainDateTime()) as { type: DateTimeSegmentType; value: string; }[]; @@ -132,9 +132,10 @@ export function useDateTimeSegmentGroup({ }); } - function withAllPartsSet(value: ZonedDateTime) { + function withAllPartsSet(value: Temporal.ZonedDateTime) { if (isTemporalPartial(value) && isAllPartsSet(value)) { - return value.copy(); // clones the value and drops the partial flag + // clones the value and drops the partial flag + return Temporal.ZonedDateTime.from(value); } return value; @@ -187,8 +188,8 @@ export function useDateTimeSegmentGroup({ const type = segment.getType(); const date = toValue(temporalValue); const maxPartsRecord: Partial> = { - day: date.calendar.getDaysInMonth(date), - month: date.calendar.getMonthsInYear(date), + day: date.daysInMonth, + month: date.monthsInYear, year: 9999, hour: toValue(formatOptions)?.hour12 ? 12 : 23, minute: 59, @@ -350,21 +351,21 @@ export function useDateTimeSegmentGroup({ } interface ArithmeticInit { - currentDate: MaybeRefOrGetter; - min?: MaybeRefOrGetter>; - max?: MaybeRefOrGetter>; + currentDate: MaybeRefOrGetter; + min?: MaybeRefOrGetter>; + max?: MaybeRefOrGetter>; } function useDateArithmetic({ currentDate, min, max }: ArithmeticInit) { - function clampDate(date: ZonedDateTime) { + function clampDate(date: Temporal.ZonedDateTime) { const minDate = toValue(min); const maxDate = toValue(max); - if (minDate && date.compare(minDate) < 0) { + if (minDate && Temporal.ZonedDateTime.compare(minDate, date) < 0) { return toValue(currentDate); } - if (maxDate && date.compare(maxDate) > 0) { + if (maxDate && Temporal.ZonedDateTime.compare(date, maxDate) > 0) { return toValue(currentDate); } @@ -381,7 +382,7 @@ function useDateArithmetic({ currentDate, min, max }: ArithmeticInit) { return date; } - const newDate = date.set({ + const newDate = date.with({ [part]: value, }); @@ -411,7 +412,7 @@ function useDateArithmetic({ currentDate, min, max }: ArithmeticInit) { } if (isTemporalPartial(date)) { - let newDate: ZonedDateTime | TemporalPartial = date; + let newDate: Temporal.ZonedDateTime | TemporalPartial = date; if (isTemporalPartSet(date, part)) { newDate = date.add({ [durationPart]: diff, @@ -420,7 +421,7 @@ function useDateArithmetic({ currentDate, min, max }: ArithmeticInit) { newDate = part === 'dayPeriod' ? date - : date.set({ + : date.with({ [part]: part === 'year' ? date.year : 1, }); } @@ -443,7 +444,7 @@ function useDateArithmetic({ currentDate, min, max }: ArithmeticInit) { .add({ [durationPart]: diff, }) - .set({ + .with({ day: part !== 'day' && part !== 'weekday' ? day : undefined, month: part !== 'month' ? month : undefined, year: part !== 'year' ? year : undefined, diff --git a/packages/core/src/useDateTime/useTemporalStore.spec.ts b/packages/core/src/useDateTime/useTemporalStore.spec.ts index 13451692..10f7d2ff 100644 --- a/packages/core/src/useDateTime/useTemporalStore.spec.ts +++ b/packages/core/src/useDateTime/useTemporalStore.spec.ts @@ -1,13 +1,11 @@ -import { createCalendar, fromDate, now } from '@internationalized/date'; import { useTemporalStore } from './useTemporalStore'; import { createTemporalPartial, isTemporalPartial } from './temporalPartial'; import { ref } from 'vue'; import { Maybe } from '../types'; import { flush } from '@test-utils/flush'; -import { vi } from 'vitest'; +import { Temporal } from 'temporal-polyfill'; describe('useTemporalStore', () => { - const calendar = createCalendar('gregory'); const timeZone = 'UTC'; const locale = 'en-US'; @@ -18,27 +16,27 @@ describe('useTemporalStore', () => { model: { get: () => date, }, - calendar, + calendar: 'gregory', timeZone, locale, }); - expect(store.value.toDate()).toEqual(date); + expect(store.value.epochMilliseconds).toBe(date.getTime()); expect(isTemporalPartial(store.value)).toBe(false); }); test('initializes with ZonedDateTime value', () => { - const date = now(timeZone); + const date = Temporal.Now.zonedDateTimeISO(timeZone); const store = useTemporalStore({ model: { - get: () => date.toDate(), + get: () => new Date(date.epochMilliseconds), }, - calendar, + calendar: 'gregory', timeZone, locale, }); - expect(store.value.toString()).toBe(date.toString()); + expect(store.value.epochMilliseconds).toBe(date.epochMilliseconds); expect(isTemporalPartial(store.value)).toBe(false); }); @@ -47,14 +45,14 @@ describe('useTemporalStore', () => { model: { get: () => null, }, - calendar, + calendar: 'gregory', timeZone, locale, }); expect(isTemporalPartial(store.value)).toBe(true); - expect(store.value.timeZone).toBe(timeZone); - expect(store.value.calendar.identifier).toBe(calendar.identifier); + expect(store.value.timeZoneId).toBe(timeZone); + expect(store.value.calendarId).toBe('gregory'); }); }); @@ -66,7 +64,7 @@ describe('useTemporalStore', () => { get: () => modelValue.value, set: value => (modelValue.value = value), }, - calendar, + calendar: 'gregory', timeZone, locale, }); @@ -75,7 +73,7 @@ describe('useTemporalStore', () => { modelValue.value = newDate; await flush(); - expect(store.value.toDate()).toEqual(newDate); + expect(store.value.epochMilliseconds).toBe(newDate.getTime()); expect(isTemporalPartial(store.value)).toBe(false); }); @@ -86,7 +84,7 @@ describe('useTemporalStore', () => { get: () => modelValue.value, set: value => (modelValue.value = value), }, - calendar, + calendar: 'gregory', timeZone, locale, }); @@ -109,15 +107,15 @@ describe('useTemporalStore', () => { get: () => modelValue.value, set: value => (modelValue.value = value), }, - calendar, + calendar: 'gregory', timeZone, locale, }); - const newDate = now(timeZone); + const newDate = Temporal.Now.zonedDateTimeISO(timeZone); store.value = newDate; - expect(modelValue.value).toEqual(newDate.toDate()); + expect(modelValue.value).toEqual(new Date(newDate.epochMilliseconds)); }); test('sets model to undefined when store value is temporal partial', () => { @@ -127,13 +125,13 @@ describe('useTemporalStore', () => { get: () => modelValue.value, set: value => (modelValue.value = value), }, - calendar, + calendar: 'gregory', timeZone, locale, }); // Change to temporal partial - store.value = createTemporalPartial(calendar, timeZone); + store.value = createTemporalPartial('gregory', timeZone); expect(modelValue.value).toBeUndefined(); }); @@ -146,17 +144,17 @@ describe('useTemporalStore', () => { model: { get: () => date, }, - calendar, + calendar: 'gregory', timeZone, locale, }); - const expectedZonedDateTime = fromDate(date, timeZone); - expect(store.value.toString()).toBe(expectedZonedDateTime.toString()); + const expectedZonedDateTime = Temporal.Now.zonedDateTimeISO(timeZone); + expect(store.value.epochMilliseconds).toBe(expectedZonedDateTime.epochMilliseconds); }); test('handles different calendar systems', () => { - const islamicCalendar = createCalendar('islamic-umalqura'); + const islamicCalendar = 'islamic-umalqura'; const date = new Date(); const store = useTemporalStore({ model: { @@ -167,8 +165,8 @@ describe('useTemporalStore', () => { locale, }); - expect(store.value.calendar.identifier).toBe('islamic-umalqura'); - expect(store.value.toDate()).toEqual(date); + expect(store.value.calendarId).toBe('islamic-umalqura'); + expect(store.value.epochMilliseconds).toBe(date.getTime()); }); test('updates model with correct date when temporal value changes', () => { @@ -183,21 +181,21 @@ describe('useTemporalStore', () => { onModelSet(value); }, }, - calendar, + calendar: 'gregory', timeZone, locale, }); // Change year - store.value = store.value.set({ year: 2025 }); + store.value = store.value.with({ year: 2025 }); expect(onModelSet).toHaveBeenLastCalledWith(new Date('2025-01-01T00:00:00Z')); // Change month - store.value = store.value.set({ month: 6 }); + store.value = store.value.with({ month: 6 }); expect(onModelSet).toHaveBeenLastCalledWith(new Date('2025-06-01T00:00:00Z')); // Change day - store.value = store.value.set({ day: 15 }); + store.value = store.value.with({ day: 15 }); expect(onModelSet).toHaveBeenLastCalledWith(new Date('2025-06-15T00:00:00Z')); }); @@ -213,13 +211,13 @@ describe('useTemporalStore', () => { onModelSet(value); }, }, - calendar, + calendar: 'gregory', timeZone, locale, }); // Change date parts - store.value = store.value.set({ year: 2025, month: 6, day: 15 }); + store.value = store.value.with({ year: 2025, month: 6, day: 15 }); // Verify time components are preserved const expectedDate = new Date('2025-06-15T14:30:45Z'); @@ -242,14 +240,14 @@ describe('useTemporalStore', () => { onModelSet(value); }, }, - calendar, + calendar: 'gregory', timeZone: timeZoneRef, locale, }); // Change timezone timeZoneRef.value = 'America/New_York'; - store.value = store.value.set({ hour: 12 }); // Set to noon NY time + store.value = store.value.with({ hour: 12 }); // Set to noon NY time // Verify the UTC time in the model is correctly adjusted const lastSetDate = onModelSet.mock.lastCall?.[0] as Date; diff --git a/packages/core/src/useDateTime/useTemporalStore.ts b/packages/core/src/useDateTime/useTemporalStore.ts index ddc5d2b9..e7a72d98 100644 --- a/packages/core/src/useDateTime/useTemporalStore.ts +++ b/packages/core/src/useDateTime/useTemporalStore.ts @@ -3,8 +3,7 @@ import { DateValue, TemporalPartial } from './types'; import { Maybe } from '../types'; import { isNullOrUndefined } from '../utils/common'; import { createTemporalPartial, isTemporalPartial } from './temporalPartial'; -import { Calendar, fromDate, toCalendar, toTimeZone, type ZonedDateTime } from '@internationalized/date'; - +import { Temporal, toTemporalInstant } from 'temporal-polyfill'; interface TemporalValueStoreInit { model: { get: () => Maybe; @@ -12,16 +11,16 @@ interface TemporalValueStoreInit { }; locale: MaybeRefOrGetter; timeZone: MaybeRefOrGetter; - calendar: MaybeRefOrGetter; + calendar: MaybeRefOrGetter; allowPartial?: boolean; - min?: MaybeRefOrGetter>; - max?: MaybeRefOrGetter>; + min?: MaybeRefOrGetter>; + max?: MaybeRefOrGetter>; } export function useTemporalStore(init: TemporalValueStoreInit) { const model = init.model; - function normalizeNullish(value: Maybe): ZonedDateTime | TemporalPartial { + function normalizeNullish(value: Maybe): Temporal.ZonedDateTime | TemporalPartial { if (isNullOrUndefined(value)) { return createTemporalPartial( toValue(init.calendar), @@ -34,7 +33,7 @@ export function useTemporalStore(init: TemporalValueStoreInit) { return value; } - const temporalVal = shallowRef( + const temporalVal = shallowRef( normalizeNullish(fromDateToCalendarZonedDateTime(model.get(), toValue(init.calendar), toValue(init.timeZone))), ); @@ -62,7 +61,7 @@ export function useTemporalStore(init: TemporalValueStoreInit) { return zonedDateTime; } - return zonedDateTime.toDate(); + return fromZonedDateTimeToDate(zonedDateTime); } const temporalValue = computed({ @@ -78,25 +77,29 @@ export function useTemporalStore(init: TemporalValueStoreInit) { export function fromDateToCalendarZonedDateTime( date: Maybe, - calendar: Calendar, + calendar: string, timeZone: string, -): ZonedDateTime | null | undefined { +): Temporal.ZonedDateTime | null | undefined { const zonedDt = toZonedDateTime(date, timeZone); if (!zonedDt) { return zonedDt; } - return toCalendar(toTimeZone(zonedDt, timeZone), calendar); + return Temporal.ZonedDateTime.from(zonedDt).withCalendar(calendar); } -export function toZonedDateTime(value: Maybe, timeZone: string): Maybe { +export function toZonedDateTime(value: Maybe, timeZone: string): Maybe { if (isNullOrUndefined(value)) { return value; } if (value instanceof Date) { - value = fromDate(value, timeZone); + value = toTemporalInstant.call(value).toZonedDateTimeISO(timeZone); } return value; } + +export function fromZonedDateTimeToDate(value: Temporal.ZonedDateTime): Date { + return new Date(value.toInstant().epochMilliseconds); +} diff --git a/packages/core/src/useDateTime/useTimeField.ts b/packages/core/src/useDateTime/useTimeField.ts index 3ee23320..5d036c64 100644 --- a/packages/core/src/useDateTime/useTimeField.ts +++ b/packages/core/src/useDateTime/useTimeField.ts @@ -7,13 +7,13 @@ import { FieldTypePrefixes } from '../constants'; import { useDateFormatter, useLocale } from '../i18n'; import { useErrorMessage, useLabel } from '../a11y'; import { useTemporalStore } from './useTemporalStore'; -import { ZonedDateTime } from '@internationalized/date'; import { useInputValidity } from '../validation'; import { createDisabledContext } from '../helpers/createDisabledContext'; import { registerField } from '@formwerk/devtools'; import { useConstraintsValidator } from '../validation/useConstraintsValidator'; import { merge } from '../../../shared/src'; import { Simplify } from 'type-fest'; +import { Temporal } from 'temporal-polyfill'; export type TimeFormatOptions = Simplify< Pick @@ -140,7 +140,7 @@ export function useTimeField(_props: Reactivify) { }, }); - function onValueChange(value: ZonedDateTime) { + function onValueChange(value: Temporal.ZonedDateTime) { temporalValue.value = value; } diff --git a/packages/playground/package.json b/packages/playground/package.json index cdcb8493..8bbe35bc 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -9,7 +9,6 @@ }, "dependencies": { "@formwerk/core": "workspace:*", - "@internationalized/date": "^3.7.0", "@tailwindcss/postcss": "^4.0.13", "fuse.js": "^7.1.0", "tailwindcss": "^4.0.13", diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue index 324e93d0..72a45060 100644 --- a/packages/playground/src/App.vue +++ b/packages/playground/src/App.vue @@ -1,27 +1,9 @@ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4083a95c..ce408702 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,9 +137,6 @@ importers: '@formwerk/devtools': specifier: workspace:* version: link:../devtools - '@internationalized/date': - specifier: ^3.7.0 - version: 3.7.0 '@standard-schema/spec': specifier: 1.0.0 version: 1.0.0 @@ -149,6 +146,9 @@ importers: klona: specifier: ^2.0.6 version: 2.0.6 + temporal-polyfill: + specifier: ^0.3.0 + version: 0.3.0 type-fest: specifier: ^4.37.0 version: 4.37.0 @@ -191,9 +191,6 @@ importers: '@formwerk/core': specifier: workspace:* version: link:../core - '@internationalized/date': - specifier: ^3.7.0 - version: 3.7.0 '@tailwindcss/postcss': specifier: ^4.0.13 version: 4.0.13 @@ -839,9 +836,6 @@ packages: resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} - '@internationalized/date@3.7.0': - resolution: {integrity: sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==} - '@intlify/core-base@11.1.2': resolution: {integrity: sha512-nmG512G8QOABsserleechwHGZxzKSAlggGf9hQX0nltvSwyKNVuB/4o6iFeG2OnjXK253r8p8eSDOZf8PgFdWw==} engines: {node: '>= 16'} @@ -1148,9 +1142,6 @@ packages: '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@tailwindcss/node@4.0.13': resolution: {integrity: sha512-P9TmtE9Vew0vv5FwyD4bsg/dHHsIsAuUXkenuGUc5gm8fYgaxpdoxIKngCyEMEQxyCKR8PQY5V5VrrKNOx7exg==} @@ -3460,6 +3451,12 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + temporal-polyfill@0.3.0: + resolution: {integrity: sha512-qNsTkX9K8hi+FHDfHmf22e/OGuXmfBm9RqNismxBrnSmZVJKegQ+HYYXT+R7Ha8F/YSm2Y34vmzD4cxMu2u95g==} + + temporal-spec@0.3.0: + resolution: {integrity: sha512-n+noVpIqz4hYgFSMOSiINNOUOMFtV5cZQNCmmszA6GiVFVRt3G7AqVyhXjhCSmowvQn+NsGn+jMDMKJYHd3bSQ==} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -4472,10 +4469,6 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} - '@internationalized/date@3.7.0': - dependencies: - '@swc/helpers': 0.5.15 - '@intlify/core-base@11.1.2': dependencies: '@intlify/message-compiler': 11.1.2 @@ -4750,10 +4743,6 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@swc/helpers@0.5.15': - dependencies: - tslib: 2.8.1 - '@tailwindcss/node@4.0.13': dependencies: enhanced-resolve: 5.18.1 @@ -7279,6 +7268,12 @@ snapshots: tapable@2.2.1: {} + temporal-polyfill@0.3.0: + dependencies: + temporal-spec: 0.3.0 + + temporal-spec@0.3.0: {} + term-size@2.2.1: {} terser@5.39.0: diff --git a/scripts/config.ts b/scripts/config.ts index 82aa83ee..b89d2348 100644 --- a/scripts/config.ts +++ b/scripts/config.ts @@ -57,9 +57,9 @@ async function createConfig(pkg: keyof typeof pkgNameMap, format: ModuleFormat) pkg === 'core' ? '@formwerk/devtools' : undefined, pkg === 'core' ? '@standard-schema/utils' : undefined, pkg === 'core' ? '@standard-schema/spec' : undefined, - pkg === 'core' ? '@internationalized/date' : undefined, pkg === 'devtools' ? '@vue/devtools-api' : undefined, pkg === 'devtools' ? '@vue/devtools-kit' : undefined, + pkg === 'core' ? 'temporal-polyfill' : undefined, ].filter(Boolean) as string[], }, output: {