Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/date-fns-v4-upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@toptal/picasso-calendar': patch
'@toptal/picasso-date-picker': patch
'@toptal/picasso': patch
---

### Calendar

- 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
- 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`

### DatePicker

- 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
- 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
- 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

No API or behavior change in any package. `react-day-picker@8.x` peers
`date-fns: ^2.28.0 || ^3.0.0` and no `8.x` release admits v4, so that peer is now
unsatisfied and recorded as an accepted exception in `pnpm-workspace.yaml`.
It is verified safe: react-day-picker reaches date-fns only through named barrel
imports, and all 31 functions it uses are unchanged in v4. The exception goes
away with the react-day-picker v9+ migration, where date-fns stops being a peer
of that package altogether.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@
"cypress": "^13.6.0",
"cypress-plugin-tab": "^1.0.5",
"cypress-real-events": "^1.12.0",
"date-fns": "^2.30.0",
"date-fns": "^4.1.0",
"debounce": "^1.2.1",
"ejs": "^3.1.10",
"escodegen": "^2.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/base/Calendar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@toptal/picasso-shared": "workspace:*",
"@toptal/picasso-typography": "workspace:*",
"@toptal/picasso-utils": "workspace:*",
"date-fns": "^2.30.0",
"date-fns": "^4.1.0",
"react-day-picker": "^8.10.0"
},
"sideEffects": [
Expand Down
3 changes: 1 addition & 2 deletions packages/base/Calendar/src/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import type {
DateRange,
} from 'react-day-picker'
import { DayPicker } from 'react-day-picker'
import isWeekend from 'date-fns/isWeekend'
import { format, isEqual } from 'date-fns'
import { format, isEqual, isWeekend } from 'date-fns'
import { useBreakpoint } from '@toptal/picasso-provider'

import type { RenderDay } from '../CalendarDay'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react'
import React from 'react'
import { useDayPicker, useNavigation } from 'react-day-picker'
import isSameMonth from 'date-fns/isSameMonth'
import { isSameMonth } from 'date-fns'
import { ButtonCircular } from '@toptal/picasso-button'
import { Typography } from '@toptal/picasso-typography'
import { ChevronMinor24, BackMinor24 } from '@toptal/picasso-icons'
Expand Down
4 changes: 2 additions & 2 deletions packages/base/DatePicker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
"@toptal/picasso-outlined-input": "workspace:*",
"@toptal/picasso-popper": "workspace:*",
"@toptal/picasso-utils": "workspace:*",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0"
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0"
},
"sideEffects": [
"**/styles.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/base/DatePicker/src/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable max-lines-per-function, max-lines */
/* eslint-disable complexity, max-statements */ // Squiggly lines makes code difficult to work with
import type { BaseProps } from '@toptal/picasso-shared'
import formatDate from 'date-fns/format'
import { format as formatDate } from 'date-fns'
import type { PopperHandle } from '@toptal/picasso-popper'
import type { KeyboardEvent, ReactNode } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
Expand Down
152 changes: 119 additions & 33 deletions packages/base/DatePicker/src/DatePicker/test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,105 @@ const testIds = {

const FAR_EAST_TIMEZONE = 'Asia/Tokyo'
const NEW_YORK_TIMEZONE = 'America/New_York'
const SHANGHAI_TIMEZONE = 'Asia/Shanghai'
const LOS_ANGELES_TIMEZONE = 'America/Los_Angeles'

/**
* Both sides of the midnight boundary for the offsets the component realistically
* meets, including both DST states of the two zones that observe it. Tests run
* under `TZ=UTC`, so every case is one where the UTC date and the target zone's
* date disagree — the situation that makes a date land on the wrong day.
*
* Each literal carries the offset the zone genuinely has at that instant. Getting
* that wrong does not fail the test, it silently moves the case off the boundary
* it is meant to probe, so keep literal and zone in step when editing.
*/
const MIDNIGHT_BOUNDARY_CASES = [
{
label: 'Tokyo +09:00, just after midnight',
date: '2020-06-25T00:00:00+09:00',
timezone: FAR_EAST_TIMEZONE,
expectedSelectedDate: '25',
expectedInputValue: 'Jun 25, 2020',
},
{
label: 'Tokyo +09:00, one second before midnight',
date: '2020-06-24T23:59:59+09:00',
timezone: FAR_EAST_TIMEZONE,
expectedSelectedDate: '24',
expectedInputValue: 'Jun 24, 2020',
},
{
label: 'Shanghai +08:00, just after midnight',
date: '2020-06-25T00:00:00+08:00',
timezone: SHANGHAI_TIMEZONE,
expectedSelectedDate: '25',
expectedInputValue: 'Jun 25, 2020',
},
{
label: 'Shanghai +08:00, one second before midnight',
date: '2020-06-24T23:59:59+08:00',
timezone: SHANGHAI_TIMEZONE,
expectedSelectedDate: '24',
expectedInputValue: 'Jun 24, 2020',
},
{
label: 'Los Angeles -08:00 (PST), just after midnight',
date: '2020-01-25T00:00:00-08:00',
timezone: LOS_ANGELES_TIMEZONE,
expectedSelectedDate: '25',
expectedInputValue: 'Jan 25, 2020',
},
{
label: 'Los Angeles -08:00 (PST), one second before midnight',
date: '2020-01-24T23:59:59-08:00',
timezone: LOS_ANGELES_TIMEZONE,
expectedSelectedDate: '24',
expectedInputValue: 'Jan 24, 2020',
},
{
label: 'Los Angeles -07:00 (PDT), just after midnight',
date: '2020-06-25T00:00:00-07:00',
timezone: LOS_ANGELES_TIMEZONE,
expectedSelectedDate: '25',
expectedInputValue: 'Jun 25, 2020',
},
{
label: 'Los Angeles -07:00 (PDT), one second before midnight',
date: '2020-06-24T23:59:59-07:00',
timezone: LOS_ANGELES_TIMEZONE,
expectedSelectedDate: '24',
expectedInputValue: 'Jun 24, 2020',
},
{
label: 'New York -04:00 (EDT), just after midnight',
date: '2020-06-25T00:00:00-04:00',
timezone: NEW_YORK_TIMEZONE,
expectedSelectedDate: '25',
expectedInputValue: 'Jun 25, 2020',
},
{
label: 'New York -04:00 (EDT), one second before midnight',
date: '2020-06-24T23:59:59-04:00',
timezone: NEW_YORK_TIMEZONE,
expectedSelectedDate: '24',
expectedInputValue: 'Jun 24, 2020',
},
{
label: 'New York -05:00 (EST), just after midnight',
date: '2020-01-25T00:00:00-05:00',
timezone: NEW_YORK_TIMEZONE,
expectedSelectedDate: '25',
expectedInputValue: 'Jan 25, 2020',
},
{
label: 'New York -05:00 (EST), one second before midnight',
date: '2020-01-24T23:59:59-05:00',
timezone: NEW_YORK_TIMEZONE,
expectedSelectedDate: '24',
expectedInputValue: 'Jan 24, 2020',
},
]

// eslint-disable-next-line max-lines-per-function
describe('DatePicker', () => {
Expand Down Expand Up @@ -173,6 +272,22 @@ describe('DatePicker', () => {
)
})

it.each(MIDNIGHT_BOUNDARY_CASES)(
'should display date in given timezone across a midnight boundary ($label)',
({ date, timezone, expectedInputValue }) => {
const { getByPlaceholderText } = renderDatePicker({
...defaultProps,
timezone,
value: new Date(date),
})

expect(getByPlaceholderText(defaultProps.placeholder)).toHaveAttribute(
'value',
expectedInputValue
)
}
)

it('should work within interval', () => {
const MIN_DATE = new Date(2020, 6, 10)
const MAX_DATE = new Date(2020, 6, 25)
Expand Down Expand Up @@ -434,24 +549,8 @@ describe('DatePicker', () => {
})

describe('Calendar', () => {
it.each([
{
date: '2020-06-25T00:00:00+09:00',
timezone: FAR_EAST_TIMEZONE,
expectedSelectedDate: '25',
},
{
date: '2020-06-24T23:59:59+09:00',
timezone: FAR_EAST_TIMEZONE,
expectedSelectedDate: '24',
},
{
date: '2020-06-25T00:00:00-05:00',
timezone: NEW_YORK_TIMEZONE,
expectedSelectedDate: '25',
},
])(
'should display date in given timezone',
it.each(MIDNIGHT_BOUNDARY_CASES)(
'should display date in given timezone ($label)',
({ date, timezone, expectedSelectedDate }) => {
const { getByPlaceholderText, getByTestId } = renderDatePicker({
...defaultProps,
Expand All @@ -467,21 +566,8 @@ describe('DatePicker', () => {
}
)

it.each([
{
date: '2020-06-25T00:00:00+09:00',
timezone: FAR_EAST_TIMEZONE,
},
{
date: '2020-06-24T23:59:59+09:00',
timezone: FAR_EAST_TIMEZONE,
},
{
date: '2020-06-25T00:00:00-05:00',
timezone: NEW_YORK_TIMEZONE,
},
])(
'should display date in given timezone after day click',
it.each(MIDNIGHT_BOUNDARY_CASES)(
'should display date in given timezone after day click ($label)',
async ({ date, timezone }) => {
const { getByPlaceholderText, getByTestId, getByText } =
renderDatePicker({
Expand Down
20 changes: 11 additions & 9 deletions packages/base/DatePicker/src/DatePicker/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import parse from 'date-fns/parse'
import isValid from 'date-fns/isValid'
import formatDate from 'date-fns/format'
import isWithinInterval from 'date-fns/isWithinInterval'
import isEqual from 'date-fns/isEqual'
import isBefore from 'date-fns/isBefore'
import isAfter from 'date-fns/isAfter'
import { utcToZonedTime, format as tzFormat, toDate } from 'date-fns-tz'
import {
parse,
isValid,
format as formatDate,
isWithinInterval,
isEqual,
isBefore,
isAfter,
} from 'date-fns'
import { toZonedTime, format as tzFormat, toDate } from 'date-fns-tz'
import type {
DateOrDateRangeType,
DateRangeType,
Expand All @@ -25,7 +27,7 @@ export const timezoneConvert = (
* when someone is editing properties in a Storybook examples.
*/
try {
return utcToZonedTime(dateToConvert, timeZone)
return toZonedTime(dateToConvert, timeZone)
} catch {
return dateToConvert
}
Expand Down
4 changes: 2 additions & 2 deletions packages/picasso/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@
"d3": "^7.8.2",
"d3-hierarchy": "^3.1.2",
"d3-zoom": "^3",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"debounce": "^1.2.1",
"detect-browser": "^5.3.0",
"glider-js": "^1.7.8",
Expand Down
2 changes: 1 addition & 1 deletion packages/topkit-analytics-charts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@toptal/picasso-test-utils": "workspace:*",
"@types/d3": "^7.4.0",
"@types/d3-array": "3.0.4",
"date-fns": "^2.30.0"
"date-fns": "^4.1.0"
},
"dependencies": {
"@toptal/picasso-utils": "workspace:*",
Expand Down
Loading
Loading