import React, { useCallback, useId, useRef } from 'react'; import type { DatePickerProps as RACDatePickerProps } from 'react-aria-components'; import { DatePicker as RACDatePicker, Group } from 'react-aria-components'; import { type CalendarDate, parseDate } from '@internationalized/date'; import classNames from 'classnames'; import { useAriaNotify, useLocale, useTranslations } from '@shoptet/ui-core-web'; import { useControlledState } from '@shoptet/utils'; import type { SafeExtract, Size } from '../../../../types'; import type { PlaceholderProps } from '../../types'; import { dictionary } from '../dictionary'; import { useDatePickerClipboard } from './useDatePickerClipboard'; const formatIsoDateForLocale = (locale: string, isoDate: string): string => { if (isoDate === '') return ''; const [year, month, day] = isoDate.split('-').map(Number); if (!year || !month || !day) return isoDate; return new Intl.DateTimeFormat(locale, { dateStyle: 'long' }).format(new Date(year, month - 1, day)); }; const toRACDateValue = (date: string | null | undefined): CalendarDate | null | undefined => { if (date === null) { return null; } if (date === undefined) { return undefined; } try { return parseDate(date); } catch { return null; } }; type RACProps = Pick< RACDatePickerProps, | 'shouldForceLeadingZeros' | 'autoFocus' | 'onFocus' | 'onBlur' | 'onKeyDown' | 'onKeyUp' | 'onOpenChange' | 'isOpen' | 'onOpenChange' | 'defaultOpen' | 'shouldCloseOnSelect' | 'isDateUnavailable' | 'name' | 'aria-label' | 'aria-labelledby' | 'aria-describedby' | 'aria-details' >; export interface DateInputContainerProps extends React.PropsWithChildren, RACProps, PlaceholderProps { /** * The id of the input. If not provided, a unique id will be generated. */ id?: string; /** * The value of the input in ISO 8601 format (f.e. '2024-12-31'). */ value?: string | null; /** * The default value of the input in ISO 8601 format (f.e. '2024-12-31') (uncontrolled). */ defaultValue?: string | null; /** * Callback fired when the value changes. * @param value The new value in ISO 8601 format (f.e. '2024-12-31'). If the value is invalid, it will be `null`. */ onChange?: (value: string | null) => void; /** * Whether the input is disabled. */ disabled?: boolean; /** * Whether the input is required. */ required?: boolean; /** * Whether the input is read-only. */ readOnly?: boolean; /** * Whether the date should force leading zeros in the month and day segments. */ shouldForceLeadingZeros?: RACProps['shouldForceLeadingZeros']; /** * Whether the input should be focused when the page loads. */ autoFocus?: RACProps['autoFocus']; /** * Callback fired when the input is focused. */ onFocus?: RACProps['onFocus']; /** * Callback fired when the input is blurred. */ onBlur?: RACProps['onBlur']; /** * Callback fired when a key is pressed. */ onKeyDown?: RACProps['onKeyDown']; /** * Callback fired when a key is released. */ onKeyUp?: RACProps['onKeyUp']; /** * Callback fired when the calendar is opened or closed. */ onOpenChange?: RACProps['onOpenChange']; /** * Whether the calendar is open. */ isOpen?: RACProps['isOpen']; /** * Whether the calendar should be open by default. */ defaultOpen?: RACProps['defaultOpen']; /** * Whether the calendar should close when a date is selected. */ shouldCloseOnSelect?: RACProps['shouldCloseOnSelect']; /** * The minimum allowed date in ISO 8601 format. */ min?: string | number | undefined; /** * The maximum allowed date in ISO 8601 format. */ max?: string | number | undefined; /** * A unique name for the input. */ name?: RACProps['name']; /** * Width of the input. */ width?: SafeExtract; } export const DateInputContainer: React.FC = ({ children, id, value, defaultValue, required, readOnly, disabled, placeholder, onChange, width, min, max, ...inputProps }) => { const generatedId = useId(); id = id ?? generatedId; const [internalValue, setInternalValue] = useControlledState(value ?? defaultValue ?? null, value); const commit = useCallback( (next: string | null) => { setInternalValue(next); onChange?.(next); }, [onChange, setInternalValue] ); const handleChange = useCallback( (next: CalendarDate | null) => { commit(next ? next.toString() : null); }, [commit] ); const normalizedMin = typeof min === 'string' ? min : undefined; const normalizedMax = typeof max === 'string' ? max : undefined; const groupRef = useRef(null); const locale = useLocale(); const translations = useTranslations(dictionary); const notify = useAriaNotify(groupRef); const announceSelectAll = useCallback( (selected: string | null) => { const formatted = selected ? formatIsoDateForLocale(locale, selected) : ''; notify(translations.selectedAnnouncement(formatted)); }, [locale, notify, translations] ); const [isAllSelected, clipboardHandlers] = useDatePickerClipboard({ value: internalValue, commit, onSelectAll: announceSelectAll, readOnly, disabled, }); return ( {children} ); };