Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 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/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

### Bug Fix

- DataForm: Keep the displayed calendar month of the `date` and `datetime` controls in sync when the value changes from outside the control, e.g. after an undo, a reset, or switching the edited item. [#81635](https://github.com/WordPress/gutenberg/pull/81635)
- 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)
Expand Down
1 change: 1 addition & 0 deletions packages/dataviews/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"sideEffects": false,
"dependencies": {
"@ariakit/react": "^0.4.37",
"@date-fns/tz": "^1.5.0",
"@wordpress/a11y": "file:../a11y",
"@wordpress/base-styles": "file:../base-styles",
"@wordpress/components": "file:../components",
Expand Down
103 changes: 86 additions & 17 deletions packages/dataviews/src/components/dataform-controls/date.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {
areIntervalsOverlapping,
endOfMonth,
format,
isSameMonth,
isValid as isValidDate,
subMonths,
subDays,
Expand Down Expand Up @@ -29,6 +32,7 @@ import {
ValidityIndicator,
} from '@wordpress/ui';
import RelativeDateControl from './utils/relative-date-control';
import toCalendarDate from './utils/to-calendar-date';
import useDisabledDateMatchers from './utils/use-disabled-date-matchers';
import {
OPERATOR_IN_THE_PAST,
Expand Down Expand Up @@ -289,13 +293,36 @@ function CalendarDateControl< Item >( {
const weekStartsOn =
( fieldFormat as FormatDate ).weekStartsOn ??
getSettings().l10n.startOfWeek;
const {
timezone: { string: timezoneString },
} = getSettings();

const fieldValue = getValue( { item: data } );
const value = typeof fieldValue === 'string' ? fieldValue : undefined;
const [ calendarMonth, setCalendarMonth ] = useState< Date >( () => {
const parsedDate = parseDate( value );
return parsedDate || new Date(); // Default to current month
// Default to current month
return toCalendarDate( parsedDate || new Date(), timezoneString );
} );
// Follow the value when it changes, so that a change from outside the
// control, e.g. an undo, a reset, or switching the edited item, brings
// the selection into view. Months are compared in the calendar's time
// zone: near a month boundary, a date can belong to a different month
// there than in the browser's time zone.
useEffect( () => {
const parsedDate = parseDate( value );
if ( parsedDate ) {
const targetMonth = toCalendarDate( parsedDate, timezoneString );
setCalendarMonth( ( currentMonth ) =>
isSameMonth(
targetMonth,
toCalendarDate( currentMonth, timezoneString )
)
? currentMonth
: targetMonth
);
}
}, [ value, timezoneString ] );

const [ isTouched, setIsTouched ] = useState( false );
const validityTargetRef = useRef< HTMLInputElement >( null );
Expand Down Expand Up @@ -326,12 +353,12 @@ function CalendarDateControl< Item >( {
const presetDate = preset.getValue();
const dateValue = formatDate( presetDate );

setCalendarMonth( presetDate );
setCalendarMonth( toCalendarDate( presetDate, timezoneString ) );
onChangeCallback( dateValue );
setSelectedPresetId( preset.id );
setIsTouched( true );
},
[ onChangeCallback ]
[ onChangeCallback, timezoneString ]
);

const handleManualDateChange = useCallback(
Expand All @@ -340,19 +367,17 @@ function CalendarDateControl< Item >( {
if ( newValue ) {
const parsedDate = parseDate( newValue );
if ( parsedDate ) {
setCalendarMonth( parsedDate );
setCalendarMonth(
toCalendarDate( parsedDate, timezoneString )
);
}
}
setSelectedPresetId( null );
setIsTouched( true );
},
[ onChangeCallback ]
[ onChangeCallback, timezoneString ]
);

const {
timezone: { string: timezoneString },
} = getSettings();

let displayLabel = label;
if ( isValid?.required && ! markWhenOptional ) {
displayLabel = `${ label } (${ __( 'Required' ) })`;
Expand Down Expand Up @@ -477,6 +502,9 @@ function CalendarDateRangeControl< Item >( {
const weekStartsOn =
( fieldFormat as FormatDate ).weekStartsOn ??
getSettings().l10n.startOfWeek;
const {
timezone: { string: timezoneString },
} = getSettings();

const { minConstraint, maxConstraint, disabledMatchers } =
useDisabledDateMatchers( isValid, parseDate );
Expand Down Expand Up @@ -510,8 +538,49 @@ function CalendarDateRangeControl< Item >( {
}, [ value ] );

const [ calendarMonth, setCalendarMonth ] = useState< Date >( () => {
return selectedRange?.from || new Date();
return toCalendarDate(
selectedRange?.from || new Date(),
timezoneString
);
} );
// Follow the value when it changes, so that a change from outside the
// control, e.g. an undo, a reset, or switching the edited item, brings
// the range into view. Keep the current view when part of the new range
// is already visible, so that selecting the end of a cross-month range
// doesn't move the view away. Months are compared in the calendar's
// time zone: near a month boundary, a date can belong to a different
// month there than in the browser's time zone.
const [ fromValue, toValue ] = value ?? [];
useEffect( () => {
setCalendarMonth( ( currentMonth ) => {
const from = parseDate( fromValue );
const to = parseDate( toValue );
const targetMonth = from ?? to;
const currentCalendarMonth = toCalendarDate(
currentMonth,
timezoneString
);
const calendarFrom = from && toCalendarDate( from, timezoneString );
const calendarTo = to && toCalendarDate( to, timezoneString );
const isRangeVisible =
calendarFrom && calendarTo
? areIntervalsOverlapping(
{ start: calendarFrom, end: calendarTo },
{
start: startOfMonth( currentCalendarMonth ),
end: endOfMonth( currentCalendarMonth ),
}
)
: [ calendarFrom, calendarTo ].some(
( date ) =>
date &&
isSameMonth( date, currentCalendarMonth )
);
return targetMonth && ! isRangeVisible
? toCalendarDate( targetMonth, timezoneString )
: currentMonth;
} );
}, [ fromValue, toValue, timezoneString ] );

const [ isTouched, setIsTouched ] = useState( false );
const fromInputRef = useRef< HTMLInputElement >( null );
Expand Down Expand Up @@ -549,12 +618,12 @@ function CalendarDateRangeControl< Item >( {
const handlePresetClick = useCallback(
( preset: ( typeof DATE_RANGE_PRESETS )[ 0 ] ) => {
const [ startDate, endDate ] = preset.getValue();
setCalendarMonth( startDate );
setCalendarMonth( toCalendarDate( startDate, timezoneString ) );
updateDateRange( startDate, endDate );
setSelectedPresetId( preset.id );
setIsTouched( true );
},
[ updateDateRange ]
[ updateDateRange, timezoneString ]
);

const handleManualDateChange = useCallback(
Expand All @@ -571,18 +640,18 @@ function CalendarDateRangeControl< Item >( {
if ( newValue ) {
const parsedDate = parseDate( newValue );
if ( parsedDate ) {
setCalendarMonth( parsedDate );
setCalendarMonth(
toCalendarDate( parsedDate, timezoneString )
);
}
}

setSelectedPresetId( null );
setIsTouched( true );
},
[ value, updateDateRange ]
[ value, updateDateRange, timezoneString ]
);

const { timezone } = getSettings();

let displayLabel = label;
if ( field.isValid?.required && ! markWhenOptional ) {
displayLabel = `${ label } (${ __( 'Required' ) })`;
Expand Down Expand Up @@ -687,7 +756,7 @@ function CalendarDateRangeControl< Item >( {
onValueChange={ onSelectCalendarRange }
month={ calendarMonth }
onMonthChange={ setCalendarMonth }
timeZone={ timezone.string || undefined }
timeZone={ timezoneString || undefined }
weekStartsOn={ weekStartsOn }
disabled={ disabled || disabledMatchers }
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isSameMonth } from 'date-fns';
import {
BaseControl,
privateApis as componentsPrivateApis,
Expand All @@ -10,6 +11,7 @@ import { Calendar, Stack } from '@wordpress/ui';
import type { DataFormControlProps, FormatDatetime } from '../../types';
import { OPERATOR_IN_THE_PAST, OPERATOR_OVER } from '../../constants';
import RelativeDateControl from './utils/relative-date-control';
import toCalendarDate from './utils/to-calendar-date';
import useDisabledDateMatchers from './utils/use-disabled-date-matchers';
import getCustomValidity from './utils/get-custom-validity';
import parseDateTime from '../../field-types/utils/parse-date-time';
Expand Down Expand Up @@ -39,11 +41,34 @@ function CalendarDateTimeControl< Item >( {
const disabled = field.isDisabled( { item: data, field } );
const fieldValue = getValue( { item: data } );
const value = typeof fieldValue === 'string' ? fieldValue : undefined;
const {
timezone: { string: timezoneString },
} = getSettings();

const [ calendarMonth, setCalendarMonth ] = useState< Date >( () => {
const parsedDate = parseDateTime( value );
return parsedDate || new Date(); // Default to current month
// Default to current month
return toCalendarDate( parsedDate || new Date(), timezoneString );
} );
// Follow the value when it changes, so that a change from outside the
// control, e.g. an undo, a reset, or switching the edited item, brings
// the selection into view. Months are compared in the calendar's time
// zone: near a month boundary, a date can belong to a different month
// there than in the browser's time zone.
useEffect( () => {
const parsedDate = parseDateTime( value );
if ( parsedDate ) {
const targetMonth = toCalendarDate( parsedDate, timezoneString );
setCalendarMonth( ( currentMonth ) =>
isSameMonth(
targetMonth,
toCalendarDate( currentMonth, timezoneString )
)
? currentMonth
: targetMonth
);
}
}, [ value, timezoneString ] );

const inputControlRef = useRef< HTMLInputElement >( null );
const validationTimeoutRef =
Expand Down Expand Up @@ -122,22 +147,21 @@ function CalendarDateTimeControl< Item >( {
// Update calendar month to match
const parsedDate = parseDateTime( dateTime.toISOString() );
if ( parsedDate ) {
setCalendarMonth( parsedDate );
setCalendarMonth(
toCalendarDate( parsedDate, timezoneString )
);
}
} else {
onChangeCallback( undefined );
}
},
[ onChangeCallback ]
[ onChangeCallback, timezoneString ]
);

const { format: fieldFormat } = field;
const weekStartsOn =
( fieldFormat as FormatDatetime ).weekStartsOn ??
getSettings().l10n.startOfWeek;
const {
timezone: { string: timezoneString },
} = getSettings();

let displayLabel = label;
if ( isValid?.required && ! markWhenOptional && ! hideLabelFromVision ) {
Expand Down
Loading
Loading