From 95f34cbc52b524c18d85ac0ef4513fea75efe5a3 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 14 Aug 2026 15:46:48 +0100 Subject: [PATCH 1/2] DataViews: Store the clicked calendar day in the datetime control. --- .../components/dataform-controls/datetime.tsx | 15 ++-- .../dataform-controls/test/datetime.tsx | 77 +++++++++++++++++++ 2 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 packages/dataviews/src/components/dataform-controls/test/datetime.tsx diff --git a/packages/dataviews/src/components/dataform-controls/datetime.tsx b/packages/dataviews/src/components/dataform-controls/datetime.tsx index 45b41780a99dfc..3a2e1725ca399b 100644 --- a/packages/dataviews/src/components/dataform-controls/datetime.tsx +++ b/packages/dataviews/src/components/dataform-controls/datetime.tsx @@ -1,3 +1,4 @@ +import { format } from 'date-fns'; import { BaseControl, privateApis as componentsPrivateApis, @@ -66,15 +67,19 @@ function CalendarDateTimeControl< Item >( { const onSelectDate = useCallback( ( newDate: Date | null ) => { if ( newDate ) { - // Extract the date part in WP timezone from the calendar selection - const wpDate = dateI18n( 'Y-m-d', newDate ); - - // Preserve time if it exists in current value, otherwise use current time + // Read the selected day through the date's own fields, like the + // calendar renders it. Converting the instant with `dateI18n` + // instead would shift the day across midnight whenever the site + // timezone is behind the calendar's. + const wpDate = format( newDate, 'yyyy-MM-dd' ); + + // Preserve time if it exists in current value, otherwise use + // the start of the selected day. let wpTime: string; if ( value ) { wpTime = dateI18n( 'H:i', getDate( value ) ); } else { - wpTime = dateI18n( 'H:i', newDate ); + wpTime = format( newDate, 'HH:mm' ); } // Combine date and time in WP timezone and convert to ISO diff --git a/packages/dataviews/src/components/dataform-controls/test/datetime.tsx b/packages/dataviews/src/components/dataform-controls/test/datetime.tsx new file mode 100644 index 00000000000000..89647f03d6fdf2 --- /dev/null +++ b/packages/dataviews/src/components/dataform-controls/test/datetime.tsx @@ -0,0 +1,77 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { dateI18n, getDate, getSettings, setSettings } from '@wordpress/date'; +import normalizeFields from '../../../field-types'; +import DateTime from '../datetime'; + +jest.mock( '@wordpress/a11y', () => ( { speak: jest.fn() } ) ); + +type TestItem = { + published?: string; +}; + +const field = normalizeFields< TestItem >( [ + { + id: 'published', + label: 'Published', + type: 'datetime', + }, +] )[ 0 ]; + +const fullDateFormatter = new Intl.DateTimeFormat( 'en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', +} ); + +const getDayButton = ( date: Date ) => + screen.getByRole( 'button', { + name: new RegExp( fullDateFormatter.format( date ) ), + } ); + +describe( 'DateTime control', () => { + describe( 'with a site timezone different from the local timezone', () => { + const originalSettings = getSettings(); + + beforeAll( () => { + // An offset this far behind guarantees the site's calendar day + // differs from the local one at local midnight, wherever the + // test runs. + setSettings( { + ...originalSettings, + timezone: { + ...originalSettings.timezone, + offset: -12, + string: '', + }, + } ); + } ); + + afterAll( () => { + setSettings( originalSettings ); + } ); + + it( 'should store the day that was clicked in the calendar', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render( + + ); + + await user.click( getDayButton( new Date( 2024, 2, 20 ) ) ); + + expect( onChange ).toHaveBeenCalledTimes( 1 ); + const edits = onChange.mock.calls[ 0 ][ 0 ]; + // The stored value keeps the clicked calendar day and the time of + // the previous value, both in the site timezone. + expect( dateI18n( 'Y-m-d H:i', getDate( edits.published ) ) ).toBe( + '2024-03-20 22:30' + ); + } ); + } ); +} ); From 6cee1c298eede9cb6b9977fee02ac607be2b6030 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 14 Aug 2026 15:47:56 +0100 Subject: [PATCH 2/2] Add changelog entry. --- packages/dataviews/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index fa8d06c61d3b2a..8689ec5a8cef36 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -9,6 +9,7 @@ ### Bug Fix +- DataForm: Store the day that was clicked in the `datetime` control's calendar instead of the previous day when the site timezone is behind the browser's. [#81637](https://github.com/WordPress/gutenberg/pull/81637) - Validated form controls: Align invalid focus styling for InputBase-based controls with the design system ([#80417](https://github.com/WordPress/gutenberg/pull/80417)). - DataViews: Prevent the filter operator select focus ring from being clipped. [#80417](https://github.com/WordPress/gutenberg/pull/80417) - DataForm: Send a single update per calendar interaction in the `datetime` control instead of two identical ones. Selecting or clearing a date now reveals the validation message by firing a synthetic `invalid` event on the input, rather than briefly moving focus into it and re-sending the value, and announces it to screen readers since focus stays on the calendar. [#81440](https://github.com/WordPress/gutenberg/pull/81440)