From 1b0592997d649313d1212ba8d24fa913b5e299a2 Mon Sep 17 00:00:00 2001 From: Matthew Haines-Young Date: Tue, 19 Nov 2024 14:50:31 +0000 Subject: [PATCH] Add new DateTimeControl component --- README.md | 1 + src/components/DateTimeControl/README.md | 27 ++++++ src/components/DateTimeControl/index.js | 101 +++++++++++++++++++++ src/components/DateTimeControl/timezone.js | 53 +++++++++++ src/index.js | 1 + 5 files changed, 183 insertions(+) create mode 100644 src/components/DateTimeControl/README.md create mode 100644 src/components/DateTimeControl/index.js create mode 100644 src/components/DateTimeControl/timezone.js diff --git a/README.md b/README.md index 9263433..0cc9676 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ One way to ensure all dependencies are loaded is to use the [`@wordpress/depende ## Components - [`ConditionalComponent`](src/components/ConditionalComponent) +- [`DateTimeControl`](src/components/DateTimeControl) - [`FetchAllTermSelectControl`](src/components/FetchAllTermSelectControl) - [`FileControls`](src/components/FileControls) - [`ImageControl`](src/components/ImageControl) diff --git a/src/components/DateTimeControl/README.md b/src/components/DateTimeControl/README.md new file mode 100644 index 0000000..c56ebd4 --- /dev/null +++ b/src/components/DateTimeControl/README.md @@ -0,0 +1,27 @@ +# DateTimeControl + +The `DateTimeControl` component provides a date and time picker with timezone information. + +## Props + +- `label` (string): The label for the date/time control. +- `id` (string): The ID for the base control. +- `onChange` (Function): Callback function to handle date/time change. +- `value` (string): The current date/time value in UTC format. + +## Usage + +```js +import DateTimeControl from './components/datetime-control'; + +... + + + setAttributes( { eventStart: newValue } ) + } +/> +``` diff --git a/src/components/DateTimeControl/index.js b/src/components/DateTimeControl/index.js new file mode 100644 index 0000000..c2ecad2 --- /dev/null +++ b/src/components/DateTimeControl/index.js @@ -0,0 +1,101 @@ +import { + DateTimePicker, + BaseControl, + Popover, + Button, +} from '@wordpress/components'; +import { gmdate, date, getSettings as getDateSettings } from '@wordpress/date'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +import TimeZone from './timezone'; + +/** + * DateTimeControl component. + * + * @param {object} props - Component properties. + * @param {string} props.label - The label for the date/time control. + * @param {string} props.id - The ID for the base control. + * @param {Function} props.onChange - Callback function to handle date/time change. + * @param {string} props.value - The current date/time value in UTC format. + * + * @returns {ReactNode|null} The DateTimeControl component. + */ +function DateTimeControl( { label, id, onChange, value } ) { + const [ isDatePickerVisible, setIsDatePickerVisible ] = useState( false ); + const dateSettings = getDateSettings(); + + /** + * Convert a date string in the current site local time into a UTC formatted as a MySQL date. + * + * @param {string} localDateString Local date. + * @returns {string} UTC formatted as a MySQL date. + */ + const convertToUTC = ( localDateString ) => { + const localDate = wp.date.getDate( localDateString ); + return gmdate( 'Y-m-d H:i:s', localDate ); + }; + + /** + * Convert a UTC date in MySQL format into a date string localised to the current site timezone. + * + * @param {string} utcDateString UTC date string. + * @param {string} format Format of returned date. + * @returns {string|null} Localised date string. + */ + const convertFromUTC = ( utcDateString, format = 'Y-m-d H:i:s' ) => { + const utcDate = new Date( utcDateString + ' +00:00' ); + + if ( utcDate instanceof Date && ! isNaN( utcDate ) ) { + return date( format, utcDate ); + } + + return null; + }; + + return ( + + { value && ( +

+ { convertFromUTC( value, dateSettings.formats.datetime ) } + +

+ ) } + + + + { isDatePickerVisible && ( + + setIsDatePickerVisible( ! isDatePickerVisible ) } + > +
+ + onChange( convertToUTC( newValue ) ) } + /> + +
+
+ ) } +
+ ); +} + +export default DateTimeControl; diff --git a/src/components/DateTimeControl/timezone.js b/src/components/DateTimeControl/timezone.js new file mode 100644 index 0000000..60fdeed --- /dev/null +++ b/src/components/DateTimeControl/timezone.js @@ -0,0 +1,53 @@ +import { Tooltip } from '@wordpress/components'; +import { getSettings as getDateSettings } from '@wordpress/date'; +import { __ } from '@wordpress/i18n'; + +/** + * TimeZone component. + * + * This component determines the user's timezone offset and compares it with the system timezone offset. + * If they match, it returns null. Otherwise, it displays the timezone abbreviation and details in a tooltip. + * + * @returns {ReactNode|null} The timezone abbreviation and details in a tooltip, or null if the user's timezone matches the system timezone. + */ +const TimeZone = () => { + const { timezone } = getDateSettings(); + + // Convert timezone offset to hours. + const userTimezoneOffset = -1 * ( new Date().getTimezoneOffset() / 60 ); + + // System timezone and user timezone match, nothing needed. + // Compare as numbers because it comes over as string. + if ( Number( timezone.offset ) === userTimezoneOffset ) { + return null; + } + + const offsetSymbol = Number( timezone.offset ) >= 0 ? '+' : ''; + const zoneAbbr = + timezone.abbr !== '' && isNaN( Number( timezone.abbr ) ) + ? timezone.abbr + : `UTC${ offsetSymbol }${ timezone.offsetFormatted }`; + + // Replace underscore with space in strings like `America/Costa_Rica`. + const prettyTimezoneString = timezone.string.replace( '_', ' ' ); + + const timezoneDetail = + timezone.string === 'UTC' + ? __( 'Coordinated Universal Time' ) + : `(${ zoneAbbr }) ${ prettyTimezoneString }`; + + // When the prettyTimezoneString is empty, there is no additional timezone + // detail information to show in a Tooltip. + const hasNoAdditionalTimezoneDetail = + prettyTimezoneString.trim().length === 0; + + return hasNoAdditionalTimezoneDetail ? ( + <>{ zoneAbbr } + ) : ( + + { zoneAbbr } + + ); +}; + +export default TimeZone; diff --git a/src/index.js b/src/index.js index 0759158..05a7ba4 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ export { default as ConditionalComponent } from './components/ConditionalCompone export { default as FetchAllTermSelectControl } from './components/FetchAllTermSelectControl'; export { default as FileControls } from './components/FileControls'; export { default as GenericServerSideEdit } from './components/GenericServerSideEdit'; +export { default as DateTimeControl } from './components/DateTimeControl'; export { default as ImageControl } from './components/ImageControl'; export { default as InnerBlockSlider } from './components/InnerBlockSlider'; export { default as InnerBlocksDisplaySingle } from './components/InnerBlockSlider/inner-block-single-display';