import React, { Component } from "react"; import { getHours, getMinutes, newDate, getStartOfDay, addMinutes, formatDate, isTimeInDisabledRange, isTimeDisabled, timesToInjectAfter, getHoursInDay, isSameMinute, getSeconds, safeToDate, type Locale, type TimeFilterOptions, KeyType, } from "./date_utils"; interface TimeProps extends Pick< TimeFilterOptions, "minTime" | "maxTime" | "excludeTimes" | "includeTimes" | "filterTime" > { format?: string; intervals?: number; selected?: Date | null; openToDate?: Date; onChange?: (time: Date) => void; timeClassName?: (time: Date) => string; todayButton?: React.ReactNode; monthRef?: HTMLDivElement; timeCaption?: string; injectTimes?: Date[]; handleOnKeyDown?: React.KeyboardEventHandler; locale?: Locale; showTimeSelectOnly?: boolean; showTimeCaption?: boolean; } interface TimeState { height: number | null; } export default class Time extends Component { static get defaultProps() { return { intervals: 30, todayButton: null, timeCaption: "Time", showTimeCaption: true, }; } static calcCenterPosition = ( listHeight: number, centerLiRef: HTMLLIElement, ): number => { return ( centerLiRef.offsetTop - (listHeight / 2 - centerLiRef.clientHeight / 2) ); }; private resizeObserver?: ResizeObserver; state: TimeState = { height: null, }; componentDidMount(): void { // code to ensure selected time will always be in focus within time window when it first appears this.scrollToTheSelectedTime(); this.observeDatePickerHeightChanges(); } componentWillUnmount(): void { this.resizeObserver?.disconnect(); } private header?: HTMLDivElement; private list?: HTMLUListElement; private centerLi?: HTMLLIElement; private observeDatePickerHeightChanges(): void { const { monthRef } = this.props; this.updateContainerHeight(); if (monthRef) { this.resizeObserver = new ResizeObserver(() => { this.updateContainerHeight(); }); this.resizeObserver.observe(monthRef); } } private updateContainerHeight(): void { if (this.props.monthRef && this.header) { const newHeight = this.props.monthRef.clientHeight - this.header.clientHeight; // Only update state if height actually changed to prevent infinite resize loops if (this.state.height !== newHeight) { this.setState({ height: newHeight, }); } } } scrollToTheSelectedTime = (): void => { requestAnimationFrame((): void => { if (!this.list) return; this.list.scrollTop = (this.centerLi && Time.calcCenterPosition( this.props.monthRef ? this.props.monthRef.clientHeight - (this.header?.clientHeight ?? 0) : this.list.clientHeight, this.centerLi, )) ?? 0; }); }; handleClick = (time: Date): void => { if ( ((this.props.minTime || this.props.maxTime) && isTimeInDisabledRange(time, this.props)) || ((this.props.excludeTimes || this.props.includeTimes || this.props.filterTime) && isTimeDisabled(time, this.props)) ) { return; } this.props.onChange?.(time); }; isSelectedTime = (time: Date) => { const selected = safeToDate(this.props.selected); return selected && isSameMinute(selected, time); }; isDisabledTime = (time: Date): boolean | undefined => ((this.props.minTime || this.props.maxTime) && isTimeInDisabledRange(time, this.props)) || ((this.props.excludeTimes || this.props.includeTimes || this.props.filterTime) && isTimeDisabled(time, this.props)); liClasses = (time: Date): string => { const classes = [ "react-datepicker__time-list-item", this.props.timeClassName ? this.props.timeClassName(time) : undefined, ]; if (this.isSelectedTime(time)) { classes.push("react-datepicker__time-list-item--selected"); } if (this.isDisabledTime(time)) { classes.push("react-datepicker__time-list-item--disabled"); } //convert this.props.intervals and the relevant time to seconds and check if it it's a clean multiple of the interval if ( this.props.injectTimes && (getHours(time) * 3600 + getMinutes(time) * 60 + getSeconds(time)) % ((this.props.intervals ?? Time.defaultProps.intervals) * 60) !== 0 ) { classes.push("react-datepicker__time-list-item--injected"); } return classes.join(" "); }; handleOnKeyDown = ( event: React.KeyboardEvent, time: Date, ): void => { if (event.key === KeyType.Space) { event.preventDefault(); event.key = KeyType.Enter; } if ( (event.key === KeyType.ArrowUp || event.key === KeyType.ArrowLeft) && event.target instanceof HTMLElement && event.target.previousSibling ) { event.preventDefault(); event.target.previousSibling instanceof HTMLElement && event.target.previousSibling.focus(); } if ( (event.key === KeyType.ArrowDown || event.key === KeyType.ArrowRight) && event.target instanceof HTMLElement && event.target.nextSibling ) { event.preventDefault(); event.target.nextSibling instanceof HTMLElement && event.target.nextSibling.focus(); } if (event.key === KeyType.Enter) { this.handleClick(time); } this.props.handleOnKeyDown?.(event); }; renderTimes = (): React.ReactElement[] => { let times: Date[] = []; const format = typeof this.props.format === "string" ? this.props.format : "p"; const intervals = this.props.intervals ?? Time.defaultProps.intervals; const activeDate = safeToDate(this.props.selected) || safeToDate(this.props.openToDate) || newDate(); const base = getStartOfDay(activeDate); const sortedInjectTimes = this.props.injectTimes && this.props.injectTimes.sort(function (a: Date, b: Date): number { return a.getTime() - b.getTime(); }); const minutesInDay = 60 * getHoursInDay(activeDate); const multiplier = minutesInDay / intervals; for (let i = 0; i < multiplier; i++) { const currentTime = addMinutes(base, i * intervals); times.push(currentTime); if (sortedInjectTimes) { const timesToInject = timesToInjectAfter( base, currentTime, i, intervals, sortedInjectTimes, ); times = times.concat(timesToInject); } } // Determine which time to focus and scroll into view when component mounts const timeToFocus = times.reduce((prev, time) => { if (time.getTime() <= activeDate.getTime()) { return time; } return prev; }, times[0]); return times.map((time): React.ReactElement => { return (
  • { if (time === timeToFocus) { this.centerLi = li; } }} onKeyDown={(event: React.KeyboardEvent): void => { this.handleOnKeyDown(event, time); }} tabIndex={time === timeToFocus ? 0 : -1} role="option" aria-selected={this.isSelectedTime(time) ? "true" : undefined} aria-disabled={this.isDisabledTime(time) ? "true" : undefined} > {formatDate(time, format, this.props.locale)}
  • ); }); }; renderTimeCaption = (): React.ReactElement => { if (this.props.showTimeCaption === false) { return <>; } return (
    { this.header = header; }} >
    {this.props.timeCaption}
    ); }; render() { const { height } = this.state; return (
    {this.renderTimeCaption()}
      { this.list = list; }} style={height ? { height } : {}} role="listbox" aria-label={this.props.timeCaption} > {this.renderTimes()}
    ); } }