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
181 changes: 158 additions & 23 deletions packages/dataviews/src/dataform-controls/datetime.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
/**
* WordPress dependencies
*/
import { BaseControl, TimePicker, VisuallyHidden } from '@wordpress/components';
import { useCallback } from '@wordpress/element';
import {
BaseControl,
privateApis as componentsPrivateApis,
__experimentalInputControl as InputControl,
__experimentalVStack as VStack,
} from '@wordpress/components';
import { useCallback, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { getDate, getSettings } from '@wordpress/date';

/**
* External dependencies
*/
import { format, isValid } from 'date-fns';

/**
* Internal dependencies
Expand All @@ -12,6 +24,140 @@ import { OPERATOR_IN_THE_PAST, OPERATOR_OVER } from '../constants';
import RelativeDateControl, {
TIME_UNITS_OPTIONS,
} from './relative-date-control';
import { unlock } from '../lock-unlock';

const { DateCalendar } = unlock( componentsPrivateApis );

const parseDateTime = ( dateTimeString?: string ): Date | null => {
if ( ! dateTimeString ) {
return null;
}
const parsed = getDate( dateTimeString );
return parsed && isValid( parsed ) ? parsed : null;
};

const formatDateTime = ( date?: Date | string ): string => {
if ( ! date ) {
return '';
}
if ( typeof date === 'string' ) {
return date;
}
// Format as datetime-local input expects: YYYY-MM-DDTHH:mm
return format( date, "yyyy-MM-dd'T'HH:mm" );
};

function CalendarDateTimeControl( {
id,
value,
onChange,
label,
description,
hideLabelFromVision,
}: {
id: string;
value: string | undefined;
onChange: ( value: any ) => void;
label: string;
description?: string;
hideLabelFromVision?: boolean;
} ) {
const [ calendarMonth, setCalendarMonth ] = useState< Date >( () => {
const parsedDate = parseDateTime( value );
return parsedDate || new Date(); // Default to current month
} );

const onSelectDate = useCallback(
( newDate: Date | undefined | null ) => {
if ( newDate ) {
// Preserve time if it exists in current value, otherwise use current time
let finalDateTime = newDate;

if ( value ) {
const currentDateTime = parseDateTime( value );
if ( currentDateTime ) {
// Preserve the time part
finalDateTime = new Date( newDate );
finalDateTime.setHours( currentDateTime.getHours() );
finalDateTime.setMinutes(
currentDateTime.getMinutes()
);
}
}

const dateTimeValue = finalDateTime.toISOString();
onChange( { [ id ]: dateTimeValue } );
} else {
onChange( { [ id ]: undefined } );
}
},
[ id, onChange, value ]
);

const handleManualDateTimeChange = useCallback(
( newValue?: string ) => {
if ( newValue ) {
// Convert from datetime-local format to ISO string
const dateTime = new Date( newValue );
onChange( { [ id ]: dateTime.toISOString() } );

// Update calendar month to match
const parsedDate = parseDateTime( dateTime.toISOString() );
if ( parsedDate ) {
setCalendarMonth( parsedDate );
}
} else {
onChange( { [ id ]: undefined } );
}
},
[ id, onChange ]
);

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

return (
<BaseControl
__nextHasNoMarginBottom
id={ id }
label={ label }
help={ description }
hideLabelFromVision={ hideLabelFromVision }
>
<VStack spacing={ 4 }>
{ /* Calendar widget */ }
<DateCalendar
style={ { width: '100%' } }
selected={
value ? parseDateTime( value ) || undefined : undefined
}
onSelect={ onSelectDate }
month={ calendarMonth }
onMonthChange={ setCalendarMonth }
timeZone={ timezoneString || undefined }
weekStartsOn={ startOfWeek }
/>
{ /* Manual datetime input */ }
<InputControl
__next40pxDefaultSize
type="datetime-local"
label={ __( 'Date time' ) }
hideLabelFromVision
value={
value
? formatDateTime(
parseDateTime( value ) || undefined
)
: ''
}
onChange={ handleManualDateTimeChange }
/>
</VStack>
</BaseControl>
);
}

export default function DateTime< Item >( {
data,
Expand All @@ -20,17 +166,13 @@ export default function DateTime< Item >( {
hideLabelFromVision,
operator,
}: DataFormControlProps< Item > ) {
const { id, label } = field;
const { id, label, description } = field;
const value = field.getValue( { item: data } );

const onChangeControl = useCallback(
( newValue: string | null ) => onChange( { [ id ]: newValue } ),
[ id, onChange ]
);

if ( operator === OPERATOR_IN_THE_PAST || operator === OPERATOR_OVER ) {
return (
<RelativeDateControl
className="dataviews-controls__datetime"
id={ id }
value={ value && typeof value === 'object' ? value : {} }
onChange={ onChange }
Expand All @@ -42,20 +184,13 @@ export default function DateTime< Item >( {
}

return (
<fieldset className="dataviews-controls__datetime">
{ ! hideLabelFromVision && (
<BaseControl.VisualLabel as="legend">
{ label }
</BaseControl.VisualLabel>
) }
{ hideLabelFromVision && (
<VisuallyHidden as="legend">{ label }</VisuallyHidden>
) }
<TimePicker
currentTime={ typeof value === 'string' ? value : undefined }
onChange={ onChangeControl }
hideLabelFromVision
/>
</fieldset>
<CalendarDateTimeControl
id={ id }
value={ typeof value === 'string' ? value : undefined }
onChange={ onChange }
label={ label }
description={ description }
hideLabelFromVision={ hideLabelFromVision }
/>
);
}
26 changes: 19 additions & 7 deletions test/e2e/specs/site-editor/page-list.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,26 @@ test.describe( 'Page List', () => {
performEdit: async ( page ) => {
const dateEl = page.getByLabel( 'Edit Date' );
await dateEl.click();
const date = new Date();
const yy = Number( date.getFullYear() );
const yyEl = page.locator(
`input[type="number"][value="${ yy }"]`
);

await yyEl.focus();
await page.keyboard.press( 'ArrowUp' );
// Wait for the datetime control to appear
const datetimeInput = page.locator(
'input[type="datetime-local"]'
);
await datetimeInput.waitFor( { state: 'visible' } );

// Get current datetime value and increment year
const currentValue = await datetimeInput.inputValue();
if ( currentValue ) {
const currentDate = new Date( currentValue );
const newDate = new Date( currentDate );
newDate.setFullYear( currentDate.getFullYear() + 1 );

// Format for datetime-local input (YYYY-MM-DDTHH:MM)
const formattedDate = newDate
.toISOString()
.slice( 0, 16 );
await datetimeInput.fill( formattedDate );
}
},
assertEditedState: async ( page ) => {
const date = new Date();
Expand Down
Loading