Skip to content

Commit 6029219

Browse files
committed
[PF-2296] Upgrade date-fns to latest v4.x (#5076)
1 parent d568016 commit 6029219

13 files changed

Lines changed: 212 additions & 85 deletions

File tree

‎.changeset/date-fns-v4-upgrade.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@toptal/picasso-calendar': patch
3+
'@toptal/picasso-date-picker': patch
4+
'@toptal/picasso': patch
5+
---
6+
7+
### Calendar
8+
9+
- upgrade `date-fns` to `^4.1.0`. `@base-ui/react` — the primitive layer the kit is migrating to — peers `date-fns: ^4.0.0`, which the previous `^2.30.0` did not satisfy
10+
- import `isWeekend` and `isSameMonth` from the `date-fns` barrel instead of the `date-fns/isWeekend` and `date-fns/isSameMonth` submodules. date-fns v3 removed the default export from every submodule, so the old form resolves to `undefined`
11+
12+
### DatePicker
13+
14+
- upgrade `date-fns` to `^4.1.0` and `date-fns-tz` to `^3.2.0`. `date-fns-tz@3` is required because it is the first line whose peer range admits date-fns v3 and v4
15+
- rename `utcToZonedTime` to `toZonedTime`, which is what `date-fns-tz@3` calls it. This is internal to `timezoneConvert`; the `timezone` prop behaves exactly as before
16+
- import `parse`, `isValid`, `format`, `isWithinInterval`, `isEqual`, `isBefore` and `isAfter` from the `date-fns` barrel instead of per-function submodules, for the same reason as Calendar
17+
18+
No API or behavior change in any package. `react-day-picker@8.x` peers
19+
`date-fns: ^2.28.0 || ^3.0.0` and no `8.x` release admits v4, so that peer is now
20+
unsatisfied and recorded as an accepted exception in `pnpm-workspace.yaml`.
21+
It is verified safe: react-day-picker reaches date-fns only through named barrel
22+
imports, and all 31 functions it uses are unchanged in v4. The exception goes
23+
away with the react-day-picker v9+ migration, where date-fns stops being a peer
24+
of that package altogether.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@
218218
"cypress": "^13.6.0",
219219
"cypress-plugin-tab": "^1.0.5",
220220
"cypress-real-events": "^1.12.0",
221-
"date-fns": "^2.30.0",
221+
"date-fns": "^4.1.0",
222222
"debounce": "^1.2.1",
223223
"ejs": "^3.1.10",
224224
"escodegen": "^2.1.0",

‎packages/base/Calendar/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"@toptal/picasso-shared": "workspace:*",
2929
"@toptal/picasso-typography": "workspace:*",
3030
"@toptal/picasso-utils": "workspace:*",
31-
"date-fns": "^2.30.0",
31+
"date-fns": "^4.1.0",
3232
"react-day-picker": "^8.10.2"
3333
},
3434
"sideEffects": [

‎packages/base/Calendar/src/Calendar/Calendar.tsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ import type {
1212
DateRange,
1313
} from 'react-day-picker'
1414
import { DayPicker } from 'react-day-picker'
15-
import isWeekend from 'date-fns/isWeekend'
16-
import { format, isEqual } from 'date-fns'
15+
import { format, isEqual, isWeekend } from 'date-fns'
1716
import { useBreakpoint } from '@toptal/picasso-provider'
1817

1918
import type { RenderDay } from '../CalendarDay'

‎packages/base/Calendar/src/CalendarMonthHeader/CalendarMonthHeader.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ReactNode } from 'react'
22
import React from 'react'
33
import { useDayPicker, useNavigation } from 'react-day-picker'
4-
import isSameMonth from 'date-fns/isSameMonth'
4+
import { isSameMonth } from 'date-fns'
55
import { ButtonCircular } from '@toptal/picasso-button'
66
import { Typography } from '@toptal/picasso-typography'
77
import { ChevronMinor24, BackMinor24 } from '@toptal/picasso-icons'

‎packages/base/DatePicker/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
"@toptal/picasso-outlined-input": "workspace:*",
3131
"@toptal/picasso-popper": "workspace:*",
3232
"@toptal/picasso-utils": "workspace:*",
33-
"date-fns": "^2.30.0",
34-
"date-fns-tz": "^2.0.0"
33+
"date-fns": "^4.1.0",
34+
"date-fns-tz": "^3.2.0"
3535
},
3636
"sideEffects": [
3737
"**/styles.ts",

‎packages/base/DatePicker/src/DatePicker/DatePicker.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable max-lines-per-function, max-lines */
22
/* eslint-disable complexity, max-statements */ // Squiggly lines makes code difficult to work with
33
import type { BaseProps } from '@toptal/picasso-shared'
4-
import formatDate from 'date-fns/format'
4+
import { format as formatDate } from 'date-fns'
55
import type { PopperHandle } from '@toptal/picasso-popper'
66
import type { KeyboardEvent, ReactNode } from 'react'
77
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'

‎packages/base/DatePicker/src/DatePicker/test.tsx‎

Lines changed: 119 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,105 @@ const testIds = {
1414

1515
const FAR_EAST_TIMEZONE = 'Asia/Tokyo'
1616
const NEW_YORK_TIMEZONE = 'America/New_York'
17+
const SHANGHAI_TIMEZONE = 'Asia/Shanghai'
18+
const LOS_ANGELES_TIMEZONE = 'America/Los_Angeles'
19+
20+
/**
21+
* Both sides of the midnight boundary for the offsets the component realistically
22+
* meets, including both DST states of the two zones that observe it. Tests run
23+
* under `TZ=UTC`, so every case is one where the UTC date and the target zone's
24+
* date disagree — the situation that makes a date land on the wrong day.
25+
*
26+
* Each literal carries the offset the zone genuinely has at that instant. Getting
27+
* that wrong does not fail the test, it silently moves the case off the boundary
28+
* it is meant to probe, so keep literal and zone in step when editing.
29+
*/
30+
const MIDNIGHT_BOUNDARY_CASES = [
31+
{
32+
label: 'Tokyo +09:00, just after midnight',
33+
date: '2020-06-25T00:00:00+09:00',
34+
timezone: FAR_EAST_TIMEZONE,
35+
expectedSelectedDate: '25',
36+
expectedInputValue: 'Jun 25, 2020',
37+
},
38+
{
39+
label: 'Tokyo +09:00, one second before midnight',
40+
date: '2020-06-24T23:59:59+09:00',
41+
timezone: FAR_EAST_TIMEZONE,
42+
expectedSelectedDate: '24',
43+
expectedInputValue: 'Jun 24, 2020',
44+
},
45+
{
46+
label: 'Shanghai +08:00, just after midnight',
47+
date: '2020-06-25T00:00:00+08:00',
48+
timezone: SHANGHAI_TIMEZONE,
49+
expectedSelectedDate: '25',
50+
expectedInputValue: 'Jun 25, 2020',
51+
},
52+
{
53+
label: 'Shanghai +08:00, one second before midnight',
54+
date: '2020-06-24T23:59:59+08:00',
55+
timezone: SHANGHAI_TIMEZONE,
56+
expectedSelectedDate: '24',
57+
expectedInputValue: 'Jun 24, 2020',
58+
},
59+
{
60+
label: 'Los Angeles -08:00 (PST), just after midnight',
61+
date: '2020-01-25T00:00:00-08:00',
62+
timezone: LOS_ANGELES_TIMEZONE,
63+
expectedSelectedDate: '25',
64+
expectedInputValue: 'Jan 25, 2020',
65+
},
66+
{
67+
label: 'Los Angeles -08:00 (PST), one second before midnight',
68+
date: '2020-01-24T23:59:59-08:00',
69+
timezone: LOS_ANGELES_TIMEZONE,
70+
expectedSelectedDate: '24',
71+
expectedInputValue: 'Jan 24, 2020',
72+
},
73+
{
74+
label: 'Los Angeles -07:00 (PDT), just after midnight',
75+
date: '2020-06-25T00:00:00-07:00',
76+
timezone: LOS_ANGELES_TIMEZONE,
77+
expectedSelectedDate: '25',
78+
expectedInputValue: 'Jun 25, 2020',
79+
},
80+
{
81+
label: 'Los Angeles -07:00 (PDT), one second before midnight',
82+
date: '2020-06-24T23:59:59-07:00',
83+
timezone: LOS_ANGELES_TIMEZONE,
84+
expectedSelectedDate: '24',
85+
expectedInputValue: 'Jun 24, 2020',
86+
},
87+
{
88+
label: 'New York -04:00 (EDT), just after midnight',
89+
date: '2020-06-25T00:00:00-04:00',
90+
timezone: NEW_YORK_TIMEZONE,
91+
expectedSelectedDate: '25',
92+
expectedInputValue: 'Jun 25, 2020',
93+
},
94+
{
95+
label: 'New York -04:00 (EDT), one second before midnight',
96+
date: '2020-06-24T23:59:59-04:00',
97+
timezone: NEW_YORK_TIMEZONE,
98+
expectedSelectedDate: '24',
99+
expectedInputValue: 'Jun 24, 2020',
100+
},
101+
{
102+
label: 'New York -05:00 (EST), just after midnight',
103+
date: '2020-01-25T00:00:00-05:00',
104+
timezone: NEW_YORK_TIMEZONE,
105+
expectedSelectedDate: '25',
106+
expectedInputValue: 'Jan 25, 2020',
107+
},
108+
{
109+
label: 'New York -05:00 (EST), one second before midnight',
110+
date: '2020-01-24T23:59:59-05:00',
111+
timezone: NEW_YORK_TIMEZONE,
112+
expectedSelectedDate: '24',
113+
expectedInputValue: 'Jan 24, 2020',
114+
},
115+
]
17116

18117
// eslint-disable-next-line max-lines-per-function
19118
describe('DatePicker', () => {
@@ -173,6 +272,22 @@ describe('DatePicker', () => {
173272
)
174273
})
175274

275+
it.each(MIDNIGHT_BOUNDARY_CASES)(
276+
'should display date in given timezone across a midnight boundary ($label)',
277+
({ date, timezone, expectedInputValue }) => {
278+
const { getByPlaceholderText } = renderDatePicker({
279+
...defaultProps,
280+
timezone,
281+
value: new Date(date),
282+
})
283+
284+
expect(getByPlaceholderText(defaultProps.placeholder)).toHaveAttribute(
285+
'value',
286+
expectedInputValue
287+
)
288+
}
289+
)
290+
176291
it('should work within interval', () => {
177292
const MIN_DATE = new Date(2020, 6, 10)
178293
const MAX_DATE = new Date(2020, 6, 25)
@@ -434,24 +549,8 @@ describe('DatePicker', () => {
434549
})
435550

436551
describe('Calendar', () => {
437-
it.each([
438-
{
439-
date: '2020-06-25T00:00:00+09:00',
440-
timezone: FAR_EAST_TIMEZONE,
441-
expectedSelectedDate: '25',
442-
},
443-
{
444-
date: '2020-06-24T23:59:59+09:00',
445-
timezone: FAR_EAST_TIMEZONE,
446-
expectedSelectedDate: '24',
447-
},
448-
{
449-
date: '2020-06-25T00:00:00-05:00',
450-
timezone: NEW_YORK_TIMEZONE,
451-
expectedSelectedDate: '25',
452-
},
453-
])(
454-
'should display date in given timezone',
552+
it.each(MIDNIGHT_BOUNDARY_CASES)(
553+
'should display date in given timezone ($label)',
455554
({ date, timezone, expectedSelectedDate }) => {
456555
const { getByPlaceholderText, getByTestId } = renderDatePicker({
457556
...defaultProps,
@@ -467,21 +566,8 @@ describe('DatePicker', () => {
467566
}
468567
)
469568

470-
it.each([
471-
{
472-
date: '2020-06-25T00:00:00+09:00',
473-
timezone: FAR_EAST_TIMEZONE,
474-
},
475-
{
476-
date: '2020-06-24T23:59:59+09:00',
477-
timezone: FAR_EAST_TIMEZONE,
478-
},
479-
{
480-
date: '2020-06-25T00:00:00-05:00',
481-
timezone: NEW_YORK_TIMEZONE,
482-
},
483-
])(
484-
'should display date in given timezone after day click',
569+
it.each(MIDNIGHT_BOUNDARY_CASES)(
570+
'should display date in given timezone after day click ($label)',
485571
async ({ date, timezone }) => {
486572
const { getByPlaceholderText, getByTestId, getByText } =
487573
renderDatePicker({

‎packages/base/DatePicker/src/DatePicker/utils.ts‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import parse from 'date-fns/parse'
2-
import isValid from 'date-fns/isValid'
3-
import formatDate from 'date-fns/format'
4-
import isWithinInterval from 'date-fns/isWithinInterval'
5-
import isEqual from 'date-fns/isEqual'
6-
import isBefore from 'date-fns/isBefore'
7-
import isAfter from 'date-fns/isAfter'
8-
import { utcToZonedTime, format as tzFormat, toDate } from 'date-fns-tz'
1+
import {
2+
parse,
3+
isValid,
4+
format as formatDate,
5+
isWithinInterval,
6+
isEqual,
7+
isBefore,
8+
isAfter,
9+
} from 'date-fns'
10+
import { toZonedTime, format as tzFormat, toDate } from 'date-fns-tz'
911
import type {
1012
DateOrDateRangeType,
1113
DateRangeType,
@@ -25,7 +27,7 @@ export const timezoneConvert = (
2527
* when someone is editing properties in a Storybook examples.
2628
*/
2729
try {
28-
return utcToZonedTime(dateToConvert, timeZone)
30+
return toZonedTime(dateToConvert, timeZone)
2931
} catch {
3032
return dateToConvert
3133
}

‎packages/picasso/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@
112112
"d3": "^7.8.2",
113113
"d3-hierarchy": "^3.1.2",
114114
"d3-zoom": "^3",
115-
"date-fns": "^2.30.0",
116-
"date-fns-tz": "^2.0.0",
115+
"date-fns": "^4.1.0",
116+
"date-fns-tz": "^3.2.0",
117117
"debounce": "^1.2.1",
118118
"detect-browser": "^5.3.0",
119119
"glider-js": "^1.7.8",

0 commit comments

Comments
 (0)