import _ from 'lodash' import React, { useCallback, useEffect, useRef, useState } from 'react' import { format } from 'date-fns' /* Import components here */ import { Calendar } from '../calendar' import { InputWrapper } from '../inputs' import { Label } from '../typography/label' import { TextInput } from '../inputs/text-input' import { StyledDatePicker, StyledDialogBox, StyledSelect, TimeWrapper } from './date-picker.styles' /* Import interfaces here */ import { DatePickerProps } from './date-picker.interfaces' import { datePickerTheme } from './date-picker.theme' import { Writeable, isRefObject } from '../utils' const MINUTES = ['00', '15', '30', '45'] const fireEvent = (eventName: string, element: HTMLInputElement) => { setTimeout(() => { const event = new Event(eventName, { bubbles: true, cancelable: true }) element.dispatchEvent(event) }, 0) } export const DatePicker = ({ className, disabled = false, openInDialog = false, label, initialValue, inputRef, status = 'basic', minTime = '00:00', maxTime = '24:00', name, onChange, }: DatePickerProps): JSX.Element => { const hours = _.range(parseInt(minTime.split(':')[0]), parseInt(maxTime.split(':')[0]) + 1) .map(String) .map(h => h.padStart(2, '0')) const [dateValue, setDateValue] = useState('') const [hourValue, setHourValue] = useState(hours[0]) const [minuteValue, setMinuteValue] = useState(MINUTES[0]) const defaultDate = initialValue || new Date() const [month, setMonth] = useState(defaultDate) const [calendarIsOpen, setCalendarIsOpen] = useState(false) const hiddenInputRef = useRef() const onChangeRef = useRef(onChange) // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop const refCallback = (ref: HTMLInputElement): void => { hiddenInputRef.current = ref if (!inputRef) return // TODO: Fix correct types and remove disabled rules // eslint-disable-next-line // @ts-ignore if (isRefObject(inputRef)) { // eslint-disable-next-line // @ts-ignore // eslint-disable-next-line no-extra-semi ;(inputRef as Writeable).current = ref } else { // eslint-disable-next-line // @ts-ignore inputRef(ref) } } const updateHiddenValue = (date: string, hour: string, minute: string): void => { if (hiddenInputRef.current === undefined) return const value = `${date}T${hour}:${minute}` hiddenInputRef.current.value = value } const onChangeDate = useCallback( (e: React.ChangeEvent) => { updateHiddenValue(e.target.value, hourValue, minuteValue) hiddenInputRef.current && fireEvent('input', hiddenInputRef.current) }, [hourValue, minuteValue], ) const onSelectDate = (date: Date) => { updateHiddenValue(format(date, 'yyyy-MM-dd'), hourValue, minuteValue) hiddenInputRef.current && fireEvent('input', hiddenInputRef.current) hiddenInputRef.current && fireEvent('blur', hiddenInputRef.current) setCalendarIsOpen(false) } // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop const handleChangeHour = (e: React.ChangeEvent) => { setHourValue(e.target.value) updateHiddenValue(dateValue, e.target.value, minuteValue) hiddenInputRef.current && fireEvent('input', hiddenInputRef.current) } // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop const handleChangeMinute = (e: React.ChangeEvent) => { setMinuteValue(e.target.value) updateHiddenValue(dateValue, hourValue, e.target.value) hiddenInputRef.current && fireEvent('input', hiddenInputRef.current) } // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop const handleBlurTime = () => { hiddenInputRef.current && fireEvent('blur', hiddenInputRef.current) } const handleBlur = useCallback(() => { hiddenInputRef.current && fireEvent('blur', hiddenInputRef.current) }, []) // Store onChange in a ref, so it can be used without triggering other effects // if it's updated useEffect(() => { onChangeRef.current = onChange }, [onChange]) useEffect(() => { if (hiddenInputRef.current === undefined) return // Redefined the value property on the hidden input to catch changes from // react-hook-forms const originalValuePropertyDescriptor = Object.getOwnPropertyDescriptor(hiddenInputRef.current, 'value') if (originalValuePropertyDescriptor === undefined) throw new Error('Unable to get descriptor') Object.defineProperty(hiddenInputRef.current, '_value', originalValuePropertyDescriptor) Object.defineProperty(hiddenInputRef.current, 'value', { get() { return this._value }, set(value: string) { const [date, time] = value.split('T') const [h, m] = time.split(':') setDateValue(date) setHourValue(h) setMinuteValue(m) if (value !== this._value) { this._value = value onChangeRef.current(value) } }, }) // Set initial values const { value } = hiddenInputRef.current const [date, time] = value.split('T') const [h, m] = time.split(':') setDateValue(date) setHourValue(h) setMinuteValue(m) }, []) // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop const openCalendar = () => setCalendarIsOpen(true) // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop const closeCalendar = () => setCalendarIsOpen(false) /** * Defined the calendar component to render. This way we don't have to describe the component twice since it is rendered conditionally. */ const Component = () => ( ) return ( {hours.map(h => ( ))} : {MINUTES.map(m => ( ))} {openInDialog ? ( ) : ( calendarIsOpen && )} ) } DatePicker.defaultTheme = datePickerTheme