import React, { ReactElement, useState, useRef, useCallback, useEffect, useMemo, FocusEvent, } from 'react'; import formatDate from 'date-fns/fp/format'; import parse from 'date-fns/fp/parse'; import isAfter from 'date-fns/fp/isAfter'; import isBefore from 'date-fns/fp/isBefore'; import css from '../../utils/css'; import Input from '../Input'; import Dropdown from '../Dropdown'; import Calendar from './Calendar'; import Button from '../Button'; import { IconName } from '../Icon'; import { DatePickerContainer, DateRangeInputWrapper, DateSeparator, FocusBar, } from './StyledDatePicker'; import { focusInput, generateFocusBarStyle } from './utils'; import { fromUndefinedable, map, getOrElse } from '../../fp/Option'; import { pipe } from '../../fp/function'; import { useHover } from '../../utils/hooks'; import { CommonProps } from '../common'; export interface DateRangePickerProps extends Omit { /** * Specify the [automated assistance](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) in filling out form field values by the browser. */ autoComplete?: string; /** * Allow to clear value after selected dates. */ clearable?: boolean; /** * Whether the picker is disabled. */ disabled?: boolean; /** * Date format. Following date-fns's format (https://date-fns.org/v2.16.1/docs/format). */ format?: string; /** * Ids of element. */ id?: { endDate?: string; startDate?: string }; /** * Whether the input is invalid. */ invalid?: boolean; /** * The latest date user can select. */ maxDate?: Date; /** * The earliest date user can select. */ minDate?: Date; /** * Names of element, is used to refer to the form data for submission. */ name?: { endDate?: string; startDate?: string }; /** * onBlur event handler. */ onBlur?: (e: FocusEvent) => void; /** * onChange event handler. */ onChange?: ({ startDate, endDate, }: { endDate?: string; startDate?: string; }) => void; /** * Placeholder text in the absence of any value. */ placeholder?: { endDate: string; startDate: string }; /** * Name of Icon or an Icon element to render on the left side of the input, before the user's cursor. */ prefix?: IconName | ReactElement; /** * The size of the input box. */ size?: 'small' | 'medium' | 'large'; /** * Current selected date which must be in correct format. If value is invalid, it will be skipped. */ value?: { endDate?: string; startDate?: string }; } const RemoveBtnIcon = ({ onChange, }: { onChange?: DateRangePickerProps['onChange']; }) => ( { onChange?.({ startDate: undefined, endDate: undefined }); }} style={{ fontSize: 'inherit' }} /> ); const DateRangePicker = ({ autoComplete, value, onBlur, onChange, minDate, maxDate, size = 'medium', invalid = false, placeholder, prefix, disabled = false, clearable = true, format = 'dd/MM/yyyy', name, id, className, style, sx = {}, 'data-test-id': dataTestId, }: DateRangePickerProps): ReactElement => { const [focusingField, setFocusingField] = useState< 'startDate' | 'endDate' | 'none' >('none'); const [dateClickCount, setDateClickCount] = useState(0); const startDate = value !== undefined && value.startDate !== undefined ? parse(new Date(), format, value.startDate) : undefined; const endDate = value !== undefined && value.endDate !== undefined ? parse(new Date(), format, value.endDate) : undefined; const startDateInputRef = useRef(null); const endDateInputRef = useRef(null); const wrapperRef = useRef(null); const isHoveringWrapper = useHover(wrapperRef); const removeShown = clearable === true && isHoveringWrapper && (value?.startDate !== undefined || value?.endDate !== undefined); const closeCalendar = useCallback(() => { setFocusingField('none'); }, [setFocusingField]); const onFocusStartDateInput = useCallback(() => { setFocusingField('startDate'); }, [setFocusingField]); const onFocusEndDateInput = useCallback(() => { setFocusingField('endDate'); }, [setFocusingField]); const resetDateClickCount = useCallback(() => { setDateClickCount(0); }, [setDateClickCount]); const onSelectDate = useCallback( (date: Date): void => { if (onChange === undefined) return; switch (focusingField) { case 'startDate': if (endDate === undefined || isAfter(endDate, date)) { onChange({ startDate: formatDate(format, date), endDate: undefined, }); focusInput(endDateInputRef.current); } else { onChange({ startDate: formatDate(format, date), endDate: formatDate(format, endDate), }); if (dateClickCount + 1 < 2) focusInput(endDateInputRef.current); } break; case 'endDate': if (startDate === undefined || isBefore(startDate, date)) { onChange({ startDate: undefined, endDate: formatDate(format, date), }); focusInput(startDateInputRef.current); } else { onChange({ startDate: formatDate(format, startDate), endDate: formatDate(format, date), }); if (dateClickCount + 1 < 2) focusInput(startDateInputRef.current); } break; } setDateClickCount(dateClickCount + 1); }, [ focusingField, startDate, endDate, dateClickCount, setDateClickCount, format, onChange, ] ); useEffect(() => { if (dateClickCount === 2) { closeCalendar(); } }, [dateClickCount, closeCalendar]); const maybeId = fromUndefinedable(id); const maybeName = fromUndefinedable(name); const maybeStartDate = fromUndefinedable(startDate); const maybeEndDate = fromUndefinedable(endDate); const maybePlaceholder = fromUndefinedable(placeholder); const focusBarStyle = useMemo(() => { switch (focusingField) { case 'none': return {}; case 'startDate': return generateFocusBarStyle(startDateInputRef.current); case 'endDate': return generateFocusBarStyle(endDateInputRef.current); } }, [focusingField]); const isCalendarOpening = useMemo(() => { switch (focusingField) { case 'none': return false; case 'startDate': return true; case 'endDate': return true; } }, [focusingField]); const dateInputs = ( -} onFocus={onFocusStartDateInput} onBlur={onBlur} onClick={resetDateClickCount} size={size} invalid={invalid} disabled={disabled} ref={startDateInputRef} readonly placeholder={pipe( maybePlaceholder, map(obj => obj.startDate), getOrElse(() => format) )} value={pipe( maybeStartDate, map(formatDate(format)), getOrElse(() => '') )} id={pipe( maybeId, map(obj => obj.startDate), getOrElse(() => undefined) )} name={pipe( maybeName, map(obj => obj.startDate), getOrElse(() => undefined) )} autoComplete={autoComplete} /> : 'calendar' } onFocus={onFocusEndDateInput} onBlur={onBlur} onClick={resetDateClickCount} size={size} invalid={invalid} disabled={disabled} ref={endDateInputRef} readonly placeholder={pipe( maybePlaceholder, map(obj => obj.endDate), getOrElse(() => format) )} value={pipe( maybeEndDate, map(formatDate(format)), getOrElse(() => '') )} id={pipe( maybeId, map(obj => obj.endDate), getOrElse(() => undefined) )} name={pipe( maybeName, map(obj => obj.endDate), getOrElse(() => undefined) )} autoComplete={autoComplete} /> ); const calendar = ( ); return ( ); }; export default DateRangePicker;