import { Children, ReactElement, useEffect, useId, useState } from 'react'; import classnames from 'classnames'; import { Locale } from 'date-fns/locale'; import { Box, Button, ButtonGroup, Flex, TapArea, Text, useDefaultLabel, useDeviceType, useGlobalEventsHandler, } from 'morphe'; import InternalDateField from './DateField/InternalDateField'; import borderStyles from './DateRange.css'; import InternalDatePicker from './DateRange/InternalDatePicker'; const MOBILE_DATEFIELD_WIDTH = 171; const DATEFIELD_WIDTH = 280; type Props = { /** * DateRange accepts imported locales from the open source date utility library date-fns. See the [locales variant](https://gestalt.pinterest.systems/web/datefield#localeData) to learn more. */ localeData?: Locale; /** * Maximal selectable date. Disables any date values after the provided date. See the [disable future and past variant](https://gestalt.pinterest.systems/web/datefield#Disable-past-and-future-dates) to learn more. */ maxDate?: Date; /** * Minimal selectable date. Disables any date values before the provided date. See the [disable future and past variant](https://gestalt.pinterest.systems/web/datefield#Disable-past-and-future-dates) to learn more. */ minDate?: Date; /** * Callback triggered when the user clicks the Cancel button to not persist the selected dates. It should be used to close DateRange. See the [controlled component variant](https://gestalt.pinterest.systems/web/daterange#Controlled-component) to learn more. */ onCancel?: () => void; /** * Callback triggered when the start date or end date input loses focus. See the [error messaging variant](https://gestalt.pinterest.systems/web/daterange#Error-messaging) to learn more. */ onDateBlur?: { startDate: (args: { event: React.FocusEvent; value: string }) => void; endDate: (args: { event: React.FocusEvent; value: string }) => void; }; /** * Callback triggered when the start date or end date secondary input loses focus. See the [error messaging variant](https://gestalt.pinterest.systems/web/daterange#Error-messaging) to learn more. */ onSecondaryDateBlur?: { startDate: (args: { event: React.FocusEvent; value: string }) => void; endDate: (args: { event: React.FocusEvent; value: string }) => void; }; /** * DateField is a controlled component. `onDateChange` is the callback triggered when the start date value changes. Should be used to modify the controlled value. See the [controlled component variant](https://gestalt.pinterest.systems/web/daterange#Controlled-component) to learn more. */ onDateChange: (startDate: { value: Date | null }, endDate: { value: Date | null }) => void; /** * DateField is a controlled component. `onSecondaryDateChange` is the callback triggered when the start date value changes. Should be used to modify the controlled value. See the [controlled component variant](https://gestalt.pinterest.systems/web/daterange#Controlled-component) to learn more. */ onSecondaryDateChange?: ( startDate: { value: Date | null }, endDate: { value: Date | null }, ) => void; /** * Callback triggered when the start date or end date values entered are invalid. See the [error messaging variant](https://gestalt.pinterest.systems/web/daterange#Error-messaging) to learn more. */ onDateError?: { startDate: (args: { errorMessage: string; value: Date | null }) => void; endDate: (args: { errorMessage: string; value: Date | null }) => void; }; /** * Callback triggered when the secondary start date or secondary end date values entered are invalid. See the [error messaging variant](https://gestalt.pinterest.systems/web/daterange#Error-messaging) to learn more. */ onSecondaryDateError?: { startDate: (args: { errorMessage: string; value: Date | null }) => void; endDate: (args: { errorMessage: string; value: Date | null }) => void; }; /** * Callback triggered when the user focus on the start date or end date input DateFields. See the [error messaging variant](https://gestalt.pinterest.systems/web/daterange#Error-messaging) to learn more. */ onDateFocus?: { startDate: (args: { event: React.FocusEvent; value: string }) => void; endDate: (args: { event: React.FocusEvent; value: string }) => void; }; /** * Callback triggered when the user focus on the start date or end date secondary input DateFields. See the [error messaging variant](https://gestalt.pinterest.systems/web/daterange#Error-messaging) to learn more. */ onSecondaryDateFocus?: { startDate: (args: { event: React.FocusEvent; value: string }) => void; endDate: (args: { event: React.FocusEvent; value: string }) => void; }; /** * Callback triggered when the user clicks the Apply button to persist the selected dates. It should be used to persist the dates selected and close the DateRange. See the [controlled component variant](https://gestalt.pinterest.systems/web/daterange#Controlled-component) to learn more. */ onSubmit?: () => void; /** * An optional RadioGroup to provide preestablished date range options. See the [with RadioGroup variant](https://gestalt.pinterest.systems/web/daterange#With-RadioGroup) to learn more. */ radioGroup?: ReactElement; /** * DateRange is a controlled component. `dateValue` sets the value of the start date and end date. See the [controlled component variant](https://gestalt.pinterest.systems/web/daterange#Controlled-component) to learn more. */ dateValue: { startDate: Date | null; endDate: Date | null }; /** * DateRange is a controlled component. `secondaryDateValue` sets the value of the start date and end date. See the [secondary date range variant](https://gestalt.pinterest.systems/web/daterange#Secondary-date-range) to learn more. */ secondaryDateValue?: { startDate: Date | null; endDate: Date | null }; /** * Customize your error message for the cases the user enters invalid dates. See the [error messaging variant](https://gestalt.pinterest.systems/web/daterange#Error-messaging) to learn more. */ dateErrorMessage?: { startDate: string | null; endDate: string | null }; /** * Customize your error message for the cases the user enters invalid dates. See the [error messaging variant](https://gestalt.pinterest.systems/web/daterange#Error-messaging) to learn more. */ secondaryDateErrorMessage?: { startDate: string | null; endDate: string | null }; /** * Prevents the user from changing the date values from the date fields (not from interacting with the fields). */ readOnly?: boolean; }; enum DateRangeType { Primary, Secondary, } type DateInputProps = { key: DateRangeType; dateValues: Props['dateValue']; onDateChangeEvent: Props['onDateChange']; errorMessages?: Props['dateErrorMessage']; onDateBlurEvent?: Props['onDateBlur']; onDateErrorEvent?: Props['onDateError']; onDateFocusEvent?: Props['onDateFocus']; }; function getDateInputProps(props: Props): Array { const dateInputs: Array = []; const primaryRange = { key: DateRangeType.Primary, dateValues: props.dateValue, onDateChangeEvent: props.onDateChange, errorMessages: props.dateErrorMessage, onDateBlurEvent: props.onDateBlur, onDateErrorEvent: props.onDateError, onDateFocusEvent: props.onDateFocus, }; dateInputs.push(primaryRange); if (props.secondaryDateValue && props.onSecondaryDateChange) { const secondaryRange = { key: DateRangeType.Secondary, dateValues: props.secondaryDateValue, onDateChangeEvent: props.onSecondaryDateChange, errorMessages: props.secondaryDateErrorMessage, onDateBlurEvent: props.onSecondaryDateBlur, onDateErrorEvent: props.onSecondaryDateError, onDateFocusEvent: props.onSecondaryDateFocus, }; dateInputs.push(secondaryRange); } return dateInputs; } /** * [DateRange](https://gestalt.pinterest.systems/web/daterange) enables users to preview and select a range of days by picking dates from a calendar or adding a text input. * DateRange is distributed in its own package and must be installed separately. * * ![DateRange light mode](https://raw.githubusercontent.com/pinterest/gestalt/master/playwright/visual-test/DateRange.spec.ts-snapshots/DateRange-chromium-darwin.png) * ![DateRange dark mode](https://raw.githubusercontent.com/pinterest/gestalt/master/playwright/visual-test/DateRange-dark.spec.ts-snapshots/DateRange-dark-chromium-darwin.png) */ function DateRange({ localeData, maxDate, minDate, onCancel, onDateChange, onDateError, onDateFocus, onDateBlur, onSubmit, radioGroup, dateValue, dateErrorMessage, secondaryDateValue, secondaryDateErrorMessage, onSecondaryDateBlur, onSecondaryDateChange, onSecondaryDateError, onSecondaryDateFocus, readOnly, }: Props) { const componentId = useId(); const deviceType = useDeviceType(); const isMobile = deviceType === 'mobile'; const [selectedRange, setSelectedRange] = useState(DateRangeType.Primary); // Consume GlobalEventsHandlerProvider const { dateRangeHandlers } = useGlobalEventsHandler() ?? { dateRangeHandlers: undefined, }; useEffect(() => { if (dateRangeHandlers?.onRender) dateRangeHandlers?.onRender(); }, [dateRangeHandlers]); useEffect(() => { if (secondaryDateValue === undefined) { setSelectedRange(DateRangeType.Primary); } }, [secondaryDateValue]); if (!dateValue.startDate && dateValue.endDate) { onDateChange({ value: null }, { value: null }); } if ( secondaryDateValue && onSecondaryDateChange && !secondaryDateValue.startDate && secondaryDateValue.endDate ) { onSecondaryDateChange({ value: null }, { value: null }); } const { applyText, cancelText } = useDefaultLabel('DateRange'); const dateInputs = getDateInputProps({ onCancel, onDateChange, onDateError, onDateFocus, onDateBlur, onSubmit, dateValue, dateErrorMessage, secondaryDateValue, secondaryDateErrorMessage, onSecondaryDateBlur, onSecondaryDateChange, onSecondaryDateError, onSecondaryDateFocus, }); return ( {radioGroup && // @ts-expect-error - TS2339 Children.only(radioGroup).type.displayName === 'RadioGroup' && !isMobile ? (
{radioGroup}
) : null} {dateInputs.map( ({ key, dateValues, errorMessages, onDateChangeEvent, onDateBlurEvent, onDateErrorEvent, onDateFocusEvent, }) => { const { startDate, endDate } = dateValues; const isInputSelected = selectedRange === key; const shouldHighlight = secondaryDateValue && isInputSelected; const multipleRanges = dateInputs.length > 1; return (
setSelectedRange(key)}> { if (isInputSelected && (value?.getTime() || value === null)) { onDateChangeEvent({ value }, { value: endDate }); } }} onError={onDateErrorEvent?.startDate} onFocus={onDateFocusEvent?.endDate} readOnly={readOnly} value={startDate} /> {/* We are not using Flex here because the error message prevents keeping the dash aligned to the form field */} { if (isInputSelected && (value?.getTime() || value === null)) { onDateChangeEvent({ value: startDate }, { value }); } }} onError={onDateErrorEvent?.endDate} onFocus={onDateFocusEvent?.endDate} readOnly={readOnly} value={endDate} />
); }, )} ; value: Date | null; } | { startDate: Date; endDate: Date; }'. | TS2339 - Property 'endDate' does not exist on type '{ event: ChangeEvent; value: Date | null; } | { startDate: Date; endDate: Date; }'. onChange={({ startDate, endDate }) => { if (selectedRange === DateRangeType.Primary) { onDateChange({ value: startDate }, { value: endDate }); } if (selectedRange === DateRangeType.Secondary && onSecondaryDateChange) { onSecondaryDateChange({ value: startDate }, { value: endDate }); } }} rangeEndDate={dateValue.endDate} rangeStartDate={dateValue.startDate} secondaryRangeEndDate={secondaryDateValue?.endDate} secondaryRangeStartDate={secondaryDateValue?.startDate} selectedRange={selectedRange} /> {onSubmit && onCancel ? (