diff --git a/lib/components/form/call-taker/date-time-picker.tsx b/lib/components/form/call-taker/date-time-picker.tsx index 96292df86..47ae53ba7 100644 --- a/lib/components/form/call-taker/date-time-picker.tsx +++ b/lib/components/form/call-taker/date-time-picker.tsx @@ -5,7 +5,13 @@ import { IntlShape, useIntl } from 'react-intl' import { isMatch, parse } from 'date-fns' import { OverlayTrigger, Tooltip } from 'react-bootstrap' import coreUtils from '@opentripplanner/core-utils' -import React, { useEffect, useRef, useState } from 'react' +import React, { + ChangeEvent, + useCallback, + useEffect, + useRef, + useState +} from 'react' import { AppReduxState, FilterType, SortType } from '../../../util/state-types' import { DepartArriveTypeMap, DepartArriveValue } from '../date-time-modal' @@ -69,6 +75,30 @@ const safeFormat = (date: Date | '', time: string, options?: OptionsWithTZ) => { } return '' } +/** + * Parse a time input expressed in the agency time zone. + * @returns A date if the parsing succeeded, or null. + */ +const parseInputAsTime = ( + homeTimezone: string, + timeInput: string = getCurrentTime(homeTimezone), + date: string = getCurrentDate(homeTimezone) +) => { + if (!timeInput) timeInput = getCurrentTime(homeTimezone) + + // Match one of the supported time formats + const matchedTimeFormat = SUPPORTED_TIME_FORMATS.find((timeFormat) => + isMatch(timeInput, timeFormat) + ) + if (matchedTimeFormat) { + const resolvedDateTime = format( + parse(timeInput, matchedTimeFormat, new Date()), + 'HH:mm:ss' + ) + return toDate(`${date}T${resolvedDateTime}`) + } + return '' +} type Props = { date?: string @@ -121,69 +151,36 @@ const DateTimeOptions = ({ ) const [date, setDate] = useState(initialDate) const [time, setTime] = useState(initialTime) - const [typedTime, setTypedTime] = useState(initialTime) + const [typedTime, setTypedTime] = useState( + safeFormat(parseInputAsTime(homeTimezone, time, date), timeFormat, { + timeZone: homeTimezone + }) + ) const timeRef = useRef(null) const intl = useIntl() - /** - * Parse a time input expressed in the agency time zone. - * @returns A date if the parsing succeeded, or null. - */ - const parseInputAsTime = ( - timeInput: string = getCurrentTime(homeTimezone), - date: string = getCurrentDate(homeTimezone) - ) => { - if (!timeInput) timeInput = getCurrentTime(homeTimezone) - - // Match one of the supported time formats - const matchedTimeFormat = SUPPORTED_TIME_FORMATS.find((timeFormat) => - isMatch(timeInput, timeFormat) - ) - if (matchedTimeFormat) { - const resolvedDateTime = format( - parse(timeInput, matchedTimeFormat, new Date()), - 'HH:mm:ss' - ) - return toDate(`${date}T${resolvedDateTime}`) - } - return '' - } - - const dateTime = parseInputAsTime(time, date) + const dateTime = parseInputAsTime(homeTimezone, time, date) // Update state when external state is updated useEffect(() => { if (initialDate !== date) setDate(initialDate) if (initialTime !== time) { - setTime(initialTime) + handleTimeChange(initialTime || '') } + // This effect is design to flow from state to component only + // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialTime, initialDate]) - useEffect(() => { - // Don't update if still typing - if (timeRef.current !== document.activeElement) { - setTypedTime( - safeFormat(dateTime, timeFormat, { - timeZone: homeTimezone - }) || - // TODO: there doesn't seem to be an intl object present? - 'Invalid Time' - ) - } - }, [time]) - useEffect(() => { if (initialDepartArrive && departArrive !== initialDepartArrive) { setDepartArrive(initialDepartArrive) } + // This effect is design to flow from state to component only + // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialDepartArrive]) - useEffect(() => { - if (departArrive === 'NOW') setTypedTime('') - }, [departArrive]) - // Handler for setting the query parameters useEffect(() => { if (safeFormat(dateTime, OTP_API_DATE_FORMAT, {}) !== '' && setQueryParam) { @@ -197,42 +194,64 @@ const DateTimeOptions = ({ }) }) } - - if ( - syncSortWithDepartArrive && - DepartArriveTypeMap[departArrive] !== sort.type - ) { - importedUpdateItineraryFilter({ - sort: { - ...sort, - type: DepartArriveTypeMap[departArrive] - } - }) - } }, [dateTime, departArrive, homeTimezone, setQueryParam]) - // Handler for updating the time and date fields when NOW is selected - useEffect(() => { - if (departArrive === 'NOW') { - setTime(getCurrentTime(homeTimezone)) - setDate(getCurrentDate(homeTimezone)) - setTypedTime( - safeFormat(dateTime, timeFormat, { - timeZone: homeTimezone + const handleDepartArriveChange = useCallback( + (e: ChangeEvent) => { + const newValue = e.target.value as DepartArriveValue + setDepartArrive(newValue) + + // Handler for updating the time and date fields when NOW is selected + if (newValue === 'NOW') { + handleTimeChange(getCurrentTime(homeTimezone)) + setDate(getCurrentDate(homeTimezone)) + setTypedTime( + safeFormat(dateTime, timeFormat, { + timeZone: homeTimezone + }) + ) + } + + // Update sort type if needed + if ( + syncSortWithDepartArrive && + DepartArriveTypeMap[newValue] !== sort.type + ) { + importedUpdateItineraryFilter({ + sort: { + ...sort, + type: DepartArriveTypeMap[newValue] + } }) - ) - } - }, [departArrive, setTime, setDate, homeTimezone]) + } + }, + [syncSortWithDepartArrive, sort, importedUpdateItineraryFilter] + ) - const unsetNow = () => { + const unsetNow = useCallback(() => { if (departArrive === 'NOW') setDepartArrive('DEPART') - } + }, [departArrive]) + + const handleTimeChange = useCallback( + (newTime: string) => { + setTime(newTime) + // Only update typed time if not actively typing + if (timeRef.current !== document.activeElement) { + setTypedTime( + safeFormat(dateTime, timeFormat, { + timeZone: homeTimezone + }) || 'Invalid Time' + ) + } + }, + [dateTime, timeFormat, homeTimezone] + ) return ( <> { - setTime(e.target.value) - setTypedTime(e.target.value) - unsetNow() - }} + onChange={useCallback( + (e) => { + handleTimeChange(e.target.value) + setTypedTime(e.target.value) + unsetNow() + }, + [handleTimeChange, setTypedTime, unsetNow] + )} onFocus={(e) => e.target.select()} onKeyDown={onKeyDown} ref={timeRef} @@ -278,15 +300,18 @@ const DateTimeOptions = ({ { - if (!e.target.value) { - e.preventDefault() - // TODO: prevent selection from advancing to next field - return - } - setDate(e.target.value) - unsetNow() - }} + onChange={useCallback( + (e) => { + if (!e.target.value) { + e.preventDefault() + // TODO: prevent selection from advancing to next field + return + } + setDate(e.target.value) + unsetNow() + }, + [unsetNow, setDate] + )} onKeyDown={onKeyDown} style={{ fontSize: '14px',