import * as React from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/DatePicker/date-picker'; import buttonStyles from '@patternfly/react-styles/css/components/Button/button'; import { TextInput, TextInputProps } from '../TextInput/TextInput'; import { Popover, PopoverProps } from '../Popover/Popover'; import { InputGroup, InputGroupItem } from '../InputGroup'; import OutlinedCalendarAltIcon from '@patternfly/react-icons/dist/esm/icons/outlined-calendar-alt-icon'; import { CalendarMonth, CalendarFormat } from '../CalendarMonth'; import { useImperativeHandle } from 'react'; import { KeyTypes } from '../../helpers'; import { isValidDate } from '../../helpers/datetimeUtils'; import { HelperText, HelperTextItem } from '../HelperText'; /** Props that customize the requirement of a date */ export interface DatePickerRequiredObject { /** Flag indicating the date is required. */ isRequired?: boolean; /** Error message to display when the text input is empty and the isRequired prop is also passed in. */ emptyDateText?: string; } /** The main date picker component. */ export interface DatePickerProps extends CalendarFormat, Omit, 'onChange' | 'onFocus' | 'onBlur' | 'disabled' | 'ref'> { /** The container to append the menu to. Defaults to 'inline'. * If your menu is being cut off you can append it to an element higher up the DOM tree. * Some examples: * menuAppendTo={() => document.body}; * menuAppendTo={document.getElementById('target')} */ appendTo?: HTMLElement | ((ref?: HTMLElement) => HTMLElement) | 'inline'; /** Accessible label for the date picker. */ 'aria-label'?: string; /** Accessible label for the button to open the date picker. */ buttonAriaLabel?: string; /** Additional classes added to the date picker. */ className?: string; /** How to format the date in the text input. */ dateFormat?: (date: Date) => string; /** How to parse the date in the text input. */ dateParse?: (value: string) => Date; /** Helper text to display alongside the date picker. Expects a HelperText component. */ helperText?: React.ReactNode; /** Additional props for the text input. */ inputProps?: TextInputProps; /** Flag indicating the date picker is disabled. */ isDisabled?: boolean; /** Error message to display when the text input contains a non-empty value in an invalid format. */ invalidFormatText?: string; /** Callback called every time the text input loses focus. */ onBlur?: (event: any, value: string, date?: Date) => void; /** Callback called every time the text input value changes. */ onChange?: (event: React.FormEvent, value: string, date?: Date) => void; /** String to display in the empty text input as a hint for the expected date format. */ placeholder?: string; /** Props to pass to the popover that contains the calendar month component. */ popoverProps?: Partial>; /** Options to customize the requirement of a date */ requiredDateOptions?: DatePickerRequiredObject; /** Functions that returns an error message if a date is invalid. */ validators?: ((date: Date) => string)[]; /** Value of the text input. */ value?: string; } /** Allows finer control over the calendar's open state when a React ref is passed into the * date picker component. Accessed via ref.current[property], e.g. ref.current.toggleCalendar(). */ export interface DatePickerRef { /** Current calendar open status. */ isCalendarOpen: boolean; /** Sets the calendar open status. */ setCalendarOpen: (isOpen: boolean) => void; /** Toggles the calendar open status. If no parameters are passed, the calendar will simply * toggle its open status. * If the isOpen parameter is passed, that will set the calendar open status to the value * of the isOpen parameter. * If the eventKey parameter is set to 'Escape', that will invoke the date pickers * onEscapePress event to toggle the correct control appropriately. */ toggleCalendar: (isOpen?: boolean) => void; } export const yyyyMMddFormat = (date: Date) => `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date .getDate() .toString() .padStart(2, '0')}`; const DatePickerBase = ( { className, locale = undefined, dateFormat = yyyyMMddFormat, dateParse = (val: string) => val.split('-').length === 3 && new Date(`${val}T00:00:00`), isDisabled = false, placeholder = 'YYYY-MM-DD', value: valueProp = '', 'aria-label': ariaLabel = 'Date picker', buttonAriaLabel = 'Toggle date picker', onChange = (): any => undefined, onBlur = (): any => undefined, invalidFormatText = 'Invalid date', requiredDateOptions, helperText, appendTo = 'inline', popoverProps, monthFormat, weekdayFormat, longWeekdayFormat, dayFormat, weekStart, validators = [], rangeStart, style: styleProps = {}, inputProps = {}, ...props }: DatePickerProps, ref: React.ForwardedRef ) => { const [value, setValue] = React.useState(valueProp); const [valueDate, setValueDate] = React.useState(dateParse(value)); const [errorText, setErrorText] = React.useState(''); const [popoverOpen, setPopoverOpen] = React.useState(false); const [selectOpen, setSelectOpen] = React.useState(false); const [pristine, setPristine] = React.useState(true); const widthChars = React.useMemo(() => Math.max(dateFormat(new Date()).length, placeholder.length), [dateFormat]); const style = { '--pf-v5-c-date-picker__input--c-form-control--width-chars': widthChars, ...(styleProps as any) }; const buttonRef = React.useRef(); const datePickerWrapperRef = React.useRef(); const triggerRef = React.useRef(); const emptyDateText = requiredDateOptions?.emptyDateText || 'Date cannot be blank'; React.useEffect(() => { setValue(valueProp); setValueDate(dateParse(valueProp)); }, [valueProp]); React.useEffect(() => { setPristine(!value); const newValueDate = dateParse(value); if (errorText && isValidDate(newValueDate)) { setError(newValueDate); } }, [value]); const setError = (date: Date) => { setErrorText(validators.map((validator) => validator(date)).join('\n') || ''); }; const onTextInput = (event: React.FormEvent, value: string) => { setValue(value); setErrorText(''); const newValueDate = dateParse(value); setValueDate(newValueDate); if (isValidDate(newValueDate)) { onChange(event, value, new Date(newValueDate)); } else { onChange(event, value); } }; const onInputBlur = (event: any) => { const newValueDate = dateParse(value); const dateIsValid = isValidDate(newValueDate); const onBlurDateArg = dateIsValid ? new Date(newValueDate) : undefined; onBlur(event, value, onBlurDateArg); if (dateIsValid) { setError(newValueDate); } if (!dateIsValid && !pristine) { setErrorText(invalidFormatText); } if (!dateIsValid && pristine && requiredDateOptions?.isRequired) { setErrorText(emptyDateText); } }; const onDateClick = (_event: React.MouseEvent, newValueDate: Date) => { const newValue = dateFormat(newValueDate); setValue(newValue); setValueDate(newValueDate); setError(newValueDate); setPopoverOpen(false); onChange(null, newValue, new Date(newValueDate)); }; const onKeyPress = (ev: React.KeyboardEvent) => { if (ev.key === 'Enter' && value) { if (isValidDate(valueDate)) { setError(valueDate); } else { setErrorText(invalidFormatText); } } }; useImperativeHandle( ref, () => ({ setCalendarOpen: (isOpen: boolean) => setPopoverOpen(isOpen), toggleCalendar: (setOpen?: boolean) => { setPopoverOpen((prev) => (setOpen !== undefined ? setOpen : !prev)); }, isCalendarOpen: popoverOpen }), [setPopoverOpen, popoverOpen, selectOpen] ); return (
(date: Date) => !validator(date))} onSelectToggle={(open) => setSelectOpen(open)} monthFormat={monthFormat} weekdayFormat={weekdayFormat} longWeekdayFormat={longWeekdayFormat} dayFormat={dayFormat} weekStart={weekStart} rangeStart={rangeStart} isDateFocused /> } showClose={false} isVisible={popoverOpen} shouldClose={(event, hideFunction) => { event = event as KeyboardEvent; if (event.key === KeyTypes.Escape && selectOpen) { event.stopPropagation(); setSelectOpen(false); return false; } // Let our button handle toggling if (buttonRef.current && buttonRef.current.contains(event.target as Node)) { return false; } if (popoverOpen) { event.stopPropagation(); setPopoverOpen(false); hideFunction(); // If datepicker is required and the popover is opened without the text input // first receiving focus, we want to validate that the text input is not blank upon // closing the popover requiredDateOptions?.isRequired && !value && setErrorText(emptyDateText); } if (event.key === KeyTypes.Escape && popoverOpen) { event.stopPropagation(); } return true; }} withFocusTrap hasNoPadding hasAutoWidth appendTo={appendTo} triggerRef={triggerRef} {...popoverProps} >
{(errorText || helperText) && (
{errorText ? ( {errorText} ) : ( helperText )}
)}
); }; export const DatePicker = React.forwardRef(DatePickerBase); DatePicker.displayName = 'DatePicker';