/** * WordPress dependencies */ import { BaseControl, privateApis as componentsPrivateApis, } from '@wordpress/components'; import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { dateI18n, getDate, getSettings } from '@wordpress/date'; import { Stack } from '@wordpress/ui'; /** * Internal dependencies */ import type { DataFormControlProps, FormatDatetime } from '../../types'; import { OPERATOR_IN_THE_PAST, OPERATOR_OVER } from '../../constants'; import RelativeDateControl from './utils/relative-date-control'; import getCustomValidity from './utils/get-custom-validity'; import parseDateTime from '../../field-types/utils/parse-date-time'; import { unlock } from '../../lock-unlock'; const { DateCalendar, ValidatedInputControl } = unlock( componentsPrivateApis ); const formatDateTime = ( value?: string ): string => { if ( ! value ) { return ''; } // Format in WordPress timezone for datetime-local input: YYYY-MM-DDTHH:mm return dateI18n( 'Y-m-d\\TH:i', getDate( value ) ); }; function CalendarDateTimeControl< Item >( { data, field, onChange, hideLabelFromVision, markWhenOptional, validity, config, }: DataFormControlProps< Item > ) { const { compact } = config || {}; const { id, label, description, setValue, getValue, isValid } = field; const fieldValue = getValue( { item: data } ); const value = typeof fieldValue === 'string' ? fieldValue : undefined; const [ calendarMonth, setCalendarMonth ] = useState< Date >( () => { const parsedDate = parseDateTime( value ); return parsedDate || new Date(); // Default to current month } ); const inputControlRef = useRef< HTMLInputElement >( null ); const validationTimeoutRef = useRef< ReturnType< typeof setTimeout > >( undefined ); const previousFocusRef = useRef< Element | null >( null ); const onChangeCallback = useCallback( ( newValue: string | undefined ) => onChange( setValue( { item: data, value: newValue } ) ), [ data, onChange, setValue ] ); // Cleanup timeout on unmount useEffect( () => { return () => { if ( validationTimeoutRef.current ) { clearTimeout( validationTimeoutRef.current ); } }; }, [] ); const onSelectDate = useCallback( ( newDate: Date | undefined | null ) => { let dateTimeValue: string | undefined; 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 let wpTime: string; if ( value ) { wpTime = dateI18n( 'H:i', getDate( value ) ); } else { wpTime = dateI18n( 'H:i', newDate ); } // Combine date and time in WP timezone and convert to ISO const finalDateTime = getDate( `${ wpDate }T${ wpTime }` ); dateTimeValue = finalDateTime.toISOString(); onChangeCallback( dateTimeValue ); // Clear any existing timeout if ( validationTimeoutRef.current ) { clearTimeout( validationTimeoutRef.current ); } } else { onChangeCallback( undefined ); } // Save the currently focused element previousFocusRef.current = inputControlRef.current && inputControlRef.current.ownerDocument.activeElement; // Trigger validation display by simulating focus, blur, and changes. // Use a timeout to ensure it runs after the value update. validationTimeoutRef.current = setTimeout( () => { if ( inputControlRef.current ) { inputControlRef.current.focus(); inputControlRef.current.blur(); onChangeCallback( dateTimeValue ); // Restore focus to the previously focused element if ( previousFocusRef.current && previousFocusRef.current instanceof HTMLElement ) { previousFocusRef.current.focus(); } } }, 0 ); }, [ onChangeCallback, value ] ); const handleManualDateTimeChange = useCallback( ( newValue?: string ) => { if ( newValue ) { // Interpret the datetime-local value in WordPress timezone const dateTime = getDate( newValue ); onChangeCallback( dateTime.toISOString() ); // Update calendar month to match const parsedDate = parseDateTime( dateTime.toISOString() ); if ( parsedDate ) { setCalendarMonth( parsedDate ); } } else { onChangeCallback( undefined ); } }, [ onChangeCallback ] ); 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 ) { displayLabel = `${ label } (${ __( 'Required' ) })`; } else if ( ! isValid?.required && markWhenOptional && ! hideLabelFromVision ) { displayLabel = `${ label } (${ __( 'Optional' ) })`; } return ( { /* Manual datetime input */ } { /* Calendar widget */ } { ! compact && ( ) } ); } export default function DateTime< Item >( { data, field, onChange, hideLabelFromVision, markWhenOptional, operator, validity, config, }: DataFormControlProps< Item > ) { if ( operator === OPERATOR_IN_THE_PAST || operator === OPERATOR_OVER ) { return ( ); } return ( ); }