UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

170 lines (168 loc) 7.15 kB
/* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import React, { useEffect, useMemo, useRef, useState } from 'react'; import moment from 'moment-timezone'; import PublicRoundedIcon from '@mui/icons-material/PublicRounded'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import TextField from '@mui/material/TextField'; import Autocomplete from '@mui/material/Autocomplete'; import FormControl from '@mui/material/FormControl'; import useLocale from '../../hooks/useLocale'; import useUpdateRefs from '../../hooks/useUpdateRefs'; import { createAtLeastHalfHourInFutureDate, createTransposedToTimezoneDate, getZDateOffset } from '../../utils/datetime'; export function DateTimeTimezonePicker(props) { const locale = useLocale(); const resolvedLocaleData = useMemo(() => Intl.DateTimeFormat().resolvedOptions(), []); const { id, value: dateProp, disabled = false, disablePast = false, localeCode = locale.localeCode || 'en-US', dateTimeFormatOptions = locale.dateTimeFormatOptions ?? resolvedLocaleData, onChange, onError } = props; const hour12 = dateTimeFormatOptions?.hour12; const timeZones = useMemo(() => moment.tz.names(), []); const [selectedDate, setSelectedDate] = useState(null); const [selectedTimezone, setSelectedTimezone] = useState(resolvedLocaleData.timeZone ?? null); // The control timezone lags behind selectedTimezone. It is only updated when there's a different // selectedTimezone to the navigator's locale, and the value (date) prop changes. const [controlTimezone, setControlTimezone] = useState(resolvedLocaleData.timeZone ?? null); const mountedRef = useRef(false); const effectRefs = useUpdateRefs({ dateProp, selectedDate, selectedTimezone, controlTimezone, disablePast, onChange }); const handleChange = (newValue) => { setSelectedDate(newValue); if (newValue.toISOString() !== moment(effectRefs.current.dateProp).toISOString()) { effectRefs.current.onChange?.(createTransposedToTimezoneDate(newValue, selectedTimezone)); } }; const handleTimezoneChange = (event, value) => { event.preventDefault(); event.stopPropagation(); setSelectedTimezone(value); }; useEffect(() => { if (!dateProp) { mountedRef.current = true; // Date is nullish; clear the field. setSelectedDate(null); } else if (disablePast && dateProp <= new Date()) { mountedRef.current = true; const future = createAtLeastHalfHourInFutureDate(); setSelectedDate(moment(future)); effectRefs.current.onChange?.(future); } else if (mountedRef.current) { const { selectedTimezone, controlTimezone, selectedDate } = effectRefs.current; if (moment(dateProp).toISOString() === selectedDate.toISOString()) { // Skip if dateProp is the same as the selected date. return; } // Not the first render; dateProp changed, update the date. let nextDate; if (selectedTimezone !== controlTimezone) { // The date prop converted to a date is always in the user's browser locale. If the selected timezone (offset) is different from the // user's timezone, we need to convert the date to the selected timezone to keep the internal and external date in sync. const dateMoment = moment(dateProp).tz(selectedTimezone); nextDate = dateMoment; setControlTimezone(selectedTimezone); } else { nextDate = dateProp ? moment(dateProp) : null; } setSelectedDate(nextDate); } else { // First render; set the initial date. mountedRef.current = true; setSelectedDate(dateProp ? moment(dateProp) : null); } }, [disablePast, dateProp, effectRefs]); useEffect(() => { const { selectedDate } = effectRefs.current; if (mountedRef.current && selectedDate) { const date = createTransposedToTimezoneDate(selectedDate, selectedTimezone); if (date.toISOString() !== moment(effectRefs.current.dateProp).toISOString()) { effectRefs.current.onChange?.(date); } } }, [effectRefs, selectedTimezone]); return React.createElement( FormControl, { id: id, fullWidth: true }, React.createElement( LocalizationProvider, { dateAdapter: AdapterMoment, adapterLocale: localeCode }, React.createElement(DateTimePicker, { sx: { mt: 1 }, ampm: hour12, value: selectedDate, onChange: handleChange, disablePast: disablePast, disabled: disabled, onError: onError, slotProps: { textField: { size: 'small' } }, // Not using the timezone prop since it would cause the date to get adjusted to that timezone. // The idea of this control is to keep the date/time value stable as you pick timezones and only // reflect the actual value change externally; but for the user, the date doesn't move around if // he/she did not manually change it. // timezone={selectedTimezone} timezone: controlTimezone }), React.createElement(Autocomplete, { options: timeZones, disabled: disabled, getOptionLabel: (timezone) => timezone + (selectedDate ? ` (GMT${getZDateOffset(selectedDate, timezone)})` : ''), value: selectedTimezone, onChange: handleTimezoneChange, popupIcon: React.createElement(PublicRoundedIcon, null), disableClearable: true, renderInput: (params) => React.createElement(TextField, { ...params, size: 'small', variant: 'outlined', fullWidth: true }), sx: { my: 1 } }) ) ); } export default DateTimeTimezonePicker;