Skip to content
Merged
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
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- `ExternalLink`: Fix arrow direction for RTL languages. The external link arrow now correctly points to the top-left (↖) instead of top-right (↗) in RTL layouts. ([#73400](https://github.com/WordPress/gutenberg/pull/73400))
- Fixed an issue where the `Guide` component’s close button became invisible on hover when used on light backgrounds. The component's close button now relies on the default button hover effect, and the custom hover color is applied only within `welcome-guide` implementations to maintain consistency. ([#73220](https://github.com/WordPress/gutenberg/pull/73220))
- `DateTimePicker`: Fixed timezone handling when selecting specific dates around changes in daylight savings time when browser and server timezone settings are not in sync, which would cause an incorrect date to be selected. ([#73444](https://github.com/WordPress/gutenberg/pull/73444))

## 30.8.0 (2025-11-12)

Expand Down
4 changes: 4 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
],
"dependencies": {
"@ariakit/react": "^0.4.15",
"@date-fns/utc": "^2.1.1",
"@emotion/cache": "^11.7.1",
"@emotion/css": "^11.7.1",
"@emotion/react": "^11.7.1",
Expand Down Expand Up @@ -90,6 +91,9 @@
"remove-accents": "^0.5.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"timezone-mock": "^1.3.6"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
Expand Down
206 changes: 206 additions & 0 deletions packages/components/src/date-time/date-time/test/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import timezoneMock from 'timezone-mock';

/**
* WordPress dependencies
*/
import { getSettings, setSettings, type DateSettings } from '@wordpress/date';

/**
* Internal dependencies
*/
import DateTimePicker from '..';

describe( 'DateTimePicker', () => {
let originalSettings: DateSettings;
beforeAll( () => {
originalSettings = getSettings();
setSettings( {
...originalSettings,
timezone: {
offset: -5,
offsetFormatted: '-5',
string: 'America/New_York',
abbr: 'EST',
},
} );
} );

afterEach( () => {
jest.restoreAllMocks();
timezoneMock.unregister();
} );

afterAll( () => {
setSettings( originalSettings );
} );

it( 'should display and select dates correctly when timezones match', async () => {
const user = userEvent.setup();
const onChange = jest.fn();

timezoneMock.register( 'US/Eastern' );

const { rerender } = render(
<DateTimePicker
currentDate="2025-11-15T00:00:00"
onChange={ onChange }
/>
);

expect(
screen.getByRole( 'button', {
name: 'November 15, 2025. Selected',
} )
).toBeVisible();

onChange.mockImplementation( ( newDate ) => {
rerender(
<DateTimePicker currentDate={ newDate } onChange={ onChange } />
);
} );

await user.click(
screen.getByRole( 'button', { name: 'November 20, 2025' } )
);

expect( onChange ).toHaveBeenCalledWith( '2025-11-20T00:00:00' );
expect(
screen.getByRole( 'button', {
name: 'November 20, 2025. Selected',
} )
).toBeVisible();
} );

describe( 'timezone differences between browser and site', () => {
describe.each( [
{
direction: 'browser behind site',
timezone: 'US/Pacific' as const,
time: '21:00:00', // Evening: shifts to next day UTC
},
{
// Test a scenario where local time is UTC time, to verify that
// using gmdateI18n (UTC) for formatting works correctly when
// the browser's timezone already has no offset from UTC.
direction: 'browser matches UTC (zero offset)',
timezone: 'UTC' as const,
time: '00:00:00',
},
{
direction: 'browser ahead of site',
timezone: 'Australia/Adelaide' as const,
time: '00:00:00', // Midnight: shifts to previous day UTC
},
] )( '$direction', ( { timezone, time } ) => {
describe.each( [
{
period: 'DST start',
initialDate: `2025-03-10T${ time }`,
initialButton: 'March 10, 2025. Selected',
clickButton: 'March 11, 2025',
expectedDay: 11,
expectedDate: `2025-03-11T${ time }`,
selectedButton: 'March 11, 2025. Selected',
wrongMonthButton: 'February 28, 2025',
},
{
period: 'DST end',
initialDate: `2025-11-01T${ time }`,
initialButton: 'November 1, 2025. Selected',
clickButton: 'November 2, 2025',
expectedDay: 2,
expectedDate: `2025-11-02T${ time }`,
selectedButton: 'November 2, 2025. Selected',
wrongMonthButton: 'October 31, 2025',
},
] )(
'$period',
( {
initialDate,
initialButton,
clickButton,
expectedDay,
expectedDate,
selectedButton,
wrongMonthButton,
} ) => {
it( 'should display and select dates correctly', async () => {
const user = userEvent.setup();
const onChange = jest.fn();

timezoneMock.register( timezone );

const { rerender } = render(
<DateTimePicker
currentDate={ initialDate }
onChange={ onChange }
/>
);

// Calendar should not show dates from wrong month
expect(
screen.queryByRole( 'button', {
name: wrongMonthButton,
} )
).not.toBeInTheDocument();

// Should show correct initial date as selected
expect(
screen.getByRole( 'button', {
name: initialButton,
} )
).toBeVisible();

onChange.mockImplementation( ( newDate ) => {
rerender(
<DateTimePicker
currentDate={ newDate }
onChange={ onChange }
/>
);
} );

await user.click(
screen.getByRole( 'button', { name: clickButton } )
);

expect( screen.getByLabelText( 'Day' ) ).toHaveValue(
expectedDay
);
expect( onChange ).toHaveBeenCalledWith( expectedDate );
expect(
screen.getByRole( 'button', {
name: selectedButton,
} )
).toBeVisible();
} );
}
);
} );
} );

it( 'should preserve time when changing date', async () => {
const user = userEvent.setup();
const onChange = jest.fn();

timezoneMock.register( 'UTC' );

render(
<DateTimePicker
currentDate="2025-11-15T14:30:00"
onChange={ onChange }
/>
);

await user.click(
screen.getByRole( 'button', { name: 'November 20, 2025' } )
);

expect( onChange ).toHaveBeenCalledWith( '2025-11-20T14:30:00' );
} );
} );
22 changes: 6 additions & 16 deletions packages/components/src/date-time/date/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type { KeyboardEventHandler } from 'react';
*/
import { __, _n, sprintf, isRTL } from '@wordpress/i18n';
import { arrowLeft, arrowRight } from '@wordpress/icons';
import { dateI18n, getSettings } from '@wordpress/date';
import { getSettings, gmdateI18n } from '@wordpress/date';
import { useState, useRef, useEffect } from '@wordpress/element';

/**
Expand Down Expand Up @@ -129,14 +129,8 @@ export function DatePicker( {
size="compact"
/>
<NavigatorHeading level={ 3 }>
<strong>
{ dateI18n(
'F',
viewing,
-viewing.getTimezoneOffset()
) }
</strong>{ ' ' }
{ dateI18n( 'Y', viewing, -viewing.getTimezoneOffset() ) }
<strong>{ gmdateI18n( 'F', viewing ) }</strong>{ ' ' }
{ gmdateI18n( 'Y', viewing ) }
</NavigatorHeading>
<ViewNextMonthButton
icon={ isRTL() ? arrowLeft : arrowRight }
Expand All @@ -161,7 +155,7 @@ export function DatePicker( {
>
{ calendar[ 0 ][ 0 ].map( ( day ) => (
<DayOfWeek key={ day.toString() }>
{ dateI18n( 'D', day, -day.getTimezoneOffset() ) }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also be achieved by passing true as the third argument of dateI18n, but it's described as being "effectively deprecated". It's unclear but I don't get the impression that "effectively deprecated" extends to gmtdateI18n itself.

* @param timezone Timezone to output result in or a
* UTC offset. Defaults to timezone from
* site. Notice: `boolean` is effectively
* deprecated, but still supported for
* backward compatibility reasons.

{ gmdateI18n( 'D', day ) }
</DayOfWeek>
) ) }
{ calendar[ 0 ].map( ( week ) =>
Expand Down Expand Up @@ -320,18 +314,14 @@ function Day( {
onClick={ onClick }
onKeyDown={ onKeyDown }
>
{ dateI18n( 'j', day, -day.getTimezoneOffset() ) }
{ gmdateI18n( 'j', day ) }
</DayButton>
);
}

function getDayLabel( date: Date, isSelected: boolean, numEvents: number ) {
const { formats } = getSettings();
const localizedDate = dateI18n(
formats.date,
date,
-date.getTimezoneOffset()
);
const localizedDate = gmdateI18n( formats.date, date );
if ( isSelected && numEvents > 0 ) {
return sprintf(
// translators: 1: The calendar date. 2: Number of events on the calendar date.
Expand Down
Loading
Loading