Skip to content

Commit de835c6

Browse files
Copilotrenovate[bot]stipsan
authored
chore(deps): update date-fns to v4 and migrate to @date-fns/tz (#11295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: stipsan <[email protected]>
1 parent 9112203 commit de835c6

File tree

5 files changed

+78
-41
lines changed

5 files changed

+78
-41
lines changed

packages/sanity/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
"watch": "pkg-utils watch"
125125
},
126126
"dependencies": {
127+
"@date-fns/tz": "^1.4.1",
127128
"@dnd-kit/core": "^6.3.1",
128129
"@dnd-kit/modifiers": "^6.0.1",
129130
"@dnd-kit/sortable": "^7.0.2",
@@ -191,7 +192,7 @@
191192
"configstore": "^5.0.1",
192193
"console-table-printer": "^2.14.6",
193194
"dataloader": "^2.2.3",
194-
"date-fns": "^2.30.0",
195+
"date-fns": "^4.1.0",
195196
"debug": "^4.4.3",
196197
"esbuild": "catalog:",
197198
"esbuild-register": "catalog:",
@@ -309,7 +310,6 @@
309310
"@vitest/expect": "^3.2.4",
310311
"babel-plugin-react-compiler": "1.0.0",
311312
"blob-polyfill": "^9.0.20240710",
312-
"date-fns-tz": "2.0.1",
313313
"eslint": "catalog:",
314314
"eslint-plugin-boundaries": "^5.2.0",
315315
"eslint-plugin-testing-library": "^7.13.5",

packages/sanity/src/core/components/inputs/DateInputs/calendar/Calendar.tsx

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import {TZDate} from '@date-fns/tz'
12
import {ChevronLeftIcon, ChevronRightIcon, EarthGlobeIcon} from '@sanity/icons'
23
import {Box, Flex, Grid, Select, Text} from '@sanity/ui'
34
import {format} from '@sanity/util/legacyDateFormat'
45
import {addDays, addMonths, parse, setDate, setHours, setMinutes, setMonth, setYear} from 'date-fns'
5-
import {utcToZonedTime, zonedTimeToUtc} from 'date-fns-tz'
66
import {
77
type ComponentProps,
88
type FormEvent,
@@ -29,6 +29,23 @@ import {type CalendarLabels, type MonthNames} from './types'
2929
import {formatTime} from './utils'
3030
import {YearInput} from './YearInput'
3131

32+
/**
33+
* Helper function to create a TZDate from a Date's components in a specific timezone.
34+
* This is useful for interpreting local date/time values as being in a specific timezone.
35+
*/
36+
function createTZDateFromComponents(date: Date, timeZone: string): TZDate {
37+
return new TZDate(
38+
date.getFullYear(),
39+
date.getMonth(),
40+
date.getDate(),
41+
date.getHours(),
42+
date.getMinutes(),
43+
date.getSeconds(),
44+
date.getMilliseconds(),
45+
timeZone,
46+
)
47+
}
48+
3249
export const MONTH_PICKER_VARIANT = {
3350
select: 'select',
3451
carousel: 'carousel',
@@ -107,8 +124,20 @@ export const Calendar = forwardRef(function Calendar(
107124

108125
useEffect(() => {
109126
if (timeZone) {
110-
const utcDate = zonedTimeToUtc(selectedDate, timeZone.name)
111-
const zonedDate = utcToZonedTime(utcDate, timeZone.name)
127+
// Replicate the old date-fns-tz behavior:
128+
// fromZonedTime(selectedDate, tz) interprets selectedDate components as being in the timezone
129+
const utcDate = createTZDateFromComponents(selectedDate, timeZone.name)
130+
// toZonedTime(utcDate, tz) converts to timezone and returns a Date with local getters
131+
const tzDate = new TZDate(utcDate, timeZone.name)
132+
const zonedDate = new Date(
133+
tzDate.getFullYear(),
134+
tzDate.getMonth(),
135+
tzDate.getDate(),
136+
tzDate.getHours(),
137+
tzDate.getMinutes(),
138+
tzDate.getSeconds(),
139+
tzDate.getMilliseconds(),
140+
)
112141
setSavedSelectedDate(zonedDate)
113142
}
114143
}, [selectedDate, timeZone])
@@ -149,9 +178,11 @@ export const Calendar = forwardRef(function Calendar(
149178
return
150179
}
151180

152-
const utcDate = zonedTimeToUtc(newDate, timeZone.name)
181+
// Create a TZDate in the timezone with the new date values
182+
// This interprets the local date/time as being in the specified timezone
183+
const tzDate = createTZDateFromComponents(newDate, timeZone.name)
153184

154-
onSelect(utcDate)
185+
onSelect(tzDate)
155186
},
156187
[onSelect, savedSelectedDate, timeZone],
157188
)
@@ -162,10 +193,12 @@ export const Calendar = forwardRef(function Calendar(
162193
onSelect(setHours(setMinutes(savedSelectedDate, mins), hours))
163194
return
164195
}
165-
const zonedDate = utcToZonedTime(savedSelectedDate, timeZone.name)
196+
// Get the date in the timezone
197+
const zonedDate = new TZDate(savedSelectedDate, timeZone.name)
166198
const newZonedDate = setHours(setMinutes(zonedDate, mins), hours)
167-
const utcDate = zonedTimeToUtc(newZonedDate, timeZone.name)
168-
onSelect(utcDate)
199+
// Create a TZDate with the new time in the timezone
200+
const tzDate = createTZDateFromComponents(newZonedDate, timeZone.name)
201+
onSelect(tzDate)
169202
},
170203
[onSelect, savedSelectedDate, timeZone],
171204
)

packages/sanity/src/core/hooks/useTimeZone.tsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import {tz as tzHelper, TZDate} from '@date-fns/tz'
12
import {type ClientError} from '@sanity/client'
23
import {useToast} from '@sanity/ui'
34
import {sanitizeLocale} from '@sanity/util/legacyDateFormat'
4-
import {formatInTimeZone, utcToZonedTime, zonedTimeToUtc} from 'date-fns-tz'
5+
import {format as dateFnsFormat} from 'date-fns'
56
import {useCallback, useEffect, useMemo, useState} from 'react'
67
import {useObservable} from 'react-rx'
78
import {startWith} from 'rxjs/operators'
@@ -12,6 +13,23 @@ import ToastDescription from '../studio/timezones/toastDescription/ToastDescript
1213
import {type NormalizedTimeZone} from '../studio/timezones/types'
1314
import {debugWithName} from '../studio/timezones/utils/debug'
1415

16+
/**
17+
* Helper function to create a TZDate from a Date's components in a specific timezone.
18+
* This is useful for interpreting local date/time values as being in a specific timezone.
19+
*/
20+
function createTZDateFromComponents(date: Date, timeZone: string): TZDate {
21+
return new TZDate(
22+
date.getFullYear(),
23+
date.getMonth(),
24+
date.getDate(),
25+
date.getHours(),
26+
date.getMinutes(),
27+
date.getSeconds(),
28+
date.getMilliseconds(),
29+
timeZone,
30+
)
31+
}
32+
1533
const TimeZoneEvents = {
1634
update: 'timeZoneEventUpdate' as const,
1735
}
@@ -79,7 +97,7 @@ function getCachedTimeZoneInfo(
7997
const dateToUse = relativeDateForZones ?? new Date()
8098
const parts = formatter.formatToParts(dateToUse)
8199
const shortParts = shortFormatter.formatToParts(dateToUse)
82-
const rawOffset = formatInTimeZone(dateToUse, canonicalIdentifier, 'xxx')
100+
const rawOffset = dateFnsFormat(dateToUse, 'xxx', {in: tzHelper(canonicalIdentifier)})
83101
// If the offset is +02:00 then we can just show +2, if it has +13:45 then we should show +13:45, remove the leading +0 and just leave a + if a number under 10, remove the :00 at the end
84102
const offset = rawOffset
85103
.replace(/([+-])0(\d)/, '$1$2')
@@ -277,14 +295,16 @@ export const useTimeZone = (scope: TimeZoneScope) => {
277295
if (includeTimeZone) {
278296
dateFormat = `${format} (zzzz)`
279297
}
280-
return formatInTimeZone(date, timeZone?.name || getLocalTimeZone()?.name || 'UTC', dateFormat)
298+
return dateFnsFormat(date, dateFormat, {
299+
in: tzHelper(timeZone?.name || getLocalTimeZone()?.name || 'UTC'),
300+
})
281301
},
282302
[timeZone, getLocalTimeZone],
283303
)
284304

285305
const getCurrentZoneDate = useCallback(() => {
286306
if (!timeZone) return new Date()
287-
return utcToZonedTime(new Date(), timeZone.name)
307+
return new TZDate(new Date(), timeZone.name)
288308
}, [timeZone])
289309

290310
const getTimeZone = useCallback(
@@ -340,15 +360,16 @@ export const useTimeZone = (scope: TimeZoneScope) => {
340360
const utcToCurrentZoneDate = useCallback(
341361
(date: Date) => {
342362
if (!timeZone) return date
343-
return utcToZonedTime(date, timeZone.name)
363+
return new TZDate(date, timeZone.name)
344364
},
345365
[timeZone],
346366
)
347367

348368
const zoneDateToUtc = useCallback(
349369
(date: Date) => {
350370
if (!timeZone) return date
351-
return zonedTimeToUtc(date, timeZone.name)
371+
// Create a TZDate by interpreting the date components as being in the timezone
372+
return createTZDateFromComponents(date, timeZone.name)
352373
},
353374
[timeZone],
354375
)

packages/sanity/src/core/scheduled-publishing/components/dateInputs/DateTimeInput.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {getMinutes, isValid, parse, parseISO, setMinutes} from 'date-fns'
2-
import {formatInTimeZone} from 'date-fns-tz'
1+
import {tz as tzHelper} from '@date-fns/tz'
2+
import {format, getMinutes, isValid, parse, parseISO, setMinutes} from 'date-fns'
33
import {type ForwardedRef, forwardRef, useCallback} from 'react'
44

55
import {type TimeZoneScope, useTimeZone} from '../../../hooks/useTimeZone'
@@ -101,7 +101,7 @@ export const DateTimeInput = forwardRef(function DateTimeInput(
101101
)
102102

103103
const formatInputValue = useCallback(
104-
(date: Date) => formatInTimeZone(date, timeZone.name, `${inputDateTimeFormat}`),
104+
(date: Date) => format(date, `${inputDateTimeFormat}`, {in: tzHelper(timeZone.name)}),
105105
[inputDateTimeFormat, timeZone.name],
106106
)
107107

pnpm-lock.yaml

Lines changed: 5 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)