UNPKG

react-datetime-picker

Version:

A date range picker for your React app.

257 lines (256 loc) 12.8 kB
'use client'; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { createElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import clsx from 'clsx'; import makeEventProps from 'make-event-props'; import Calendar from 'react-calendar'; import Clock from 'react-clock'; import Fit from 'react-fit'; import DateTimeInput from './DateTimeInput.js'; const baseClassName = 'react-datetime-picker'; const outsideActionEvents = ['mousedown', 'focusin', 'touchstart']; const allViews = ['hour', 'minute', 'second']; const iconProps = { xmlns: 'http://www.w3.org/2000/svg', width: 19, height: 19, viewBox: '0 0 19 19', stroke: 'black', strokeWidth: 2, }; const CalendarIcon = (_jsxs("svg", { ...iconProps, "aria-hidden": "true", className: `${baseClassName}__calendar-button__icon ${baseClassName}__button__icon`, children: [_jsx("rect", { fill: "none", height: "15", width: "15", x: "2", y: "2" }), _jsx("line", { x1: "6", x2: "6", y1: "0", y2: "4" }), _jsx("line", { x1: "13", x2: "13", y1: "0", y2: "4" })] })); const ClearIcon = (_jsxs("svg", { ...iconProps, "aria-hidden": "true", className: `${baseClassName}__clear-button__icon ${baseClassName}__button__icon`, children: [_jsx("line", { x1: "4", x2: "15", y1: "4", y2: "15" }), _jsx("line", { x1: "15", x2: "4", y1: "4", y2: "15" })] })); export default function DateTimePicker(props) { const { amPmAriaLabel, autoFocus, calendarAriaLabel, calendarIcon = CalendarIcon, className, clearAriaLabel, clearIcon = ClearIcon, closeWidgets: shouldCloseWidgetsOnSelect = true, 'data-testid': dataTestid, dayAriaLabel, dayPlaceholder, disableCalendar, disableClock, disabled, format, hourAriaLabel, hourPlaceholder, id, isCalendarOpen: isCalendarOpenProps = null, isClockOpen: isClockOpenProps = null, locale, maxDate, maxDetail = 'minute', minDate, minuteAriaLabel, minutePlaceholder, monthAriaLabel, monthPlaceholder, name = 'datetime', nativeInputAriaLabel, onCalendarClose, onCalendarOpen, onChange: onChangeProps, onClockClose, onClockOpen, onFocus: onFocusProps, onInvalidChange, openWidgetsOnFocus = true, required, secondAriaLabel, secondPlaceholder, shouldCloseWidgets, shouldOpenWidgets, showLeadingZeros, value, yearAriaLabel, yearPlaceholder, ...otherProps } = props; const [isCalendarOpen, setIsCalendarOpen] = useState(isCalendarOpenProps); const [isClockOpen, setIsClockOpen] = useState(isClockOpenProps); const wrapper = useRef(null); const calendarWrapper = useRef(null); const clockWrapper = useRef(null); useEffect(() => { setIsCalendarOpen(isCalendarOpenProps); }, [isCalendarOpenProps]); useEffect(() => { setIsClockOpen(isClockOpenProps); }, [isClockOpenProps]); function openCalendar({ reason }) { if (shouldOpenWidgets) { if (!shouldOpenWidgets({ reason, widget: 'calendar' })) { return; } } setIsClockOpen(isClockOpen ? false : isClockOpen); setIsCalendarOpen(true); if (onCalendarOpen) { onCalendarOpen(); } } const closeCalendar = useCallback(({ reason }) => { if (shouldCloseWidgets) { if (!shouldCloseWidgets({ reason, widget: 'calendar' })) { return; } } setIsCalendarOpen(false); if (onCalendarClose) { onCalendarClose(); } }, [onCalendarClose, shouldCloseWidgets]); function toggleCalendar() { if (isCalendarOpen) { closeCalendar({ reason: 'buttonClick' }); } else { openCalendar({ reason: 'buttonClick' }); } } function openClock({ reason }) { if (shouldOpenWidgets) { if (!shouldOpenWidgets({ reason, widget: 'clock' })) { return; } } setIsCalendarOpen(isCalendarOpen ? false : isCalendarOpen); setIsClockOpen(true); if (onClockOpen) { onClockOpen(); } } const closeClock = useCallback(({ reason }) => { if (shouldCloseWidgets) { if (!shouldCloseWidgets({ reason, widget: 'clock' })) { return; } } setIsClockOpen(false); if (onClockClose) { onClockClose(); } }, [onClockClose, shouldCloseWidgets]); const closeWidgets = useCallback(({ reason }) => { closeCalendar({ reason }); closeClock({ reason }); }, [closeCalendar, closeClock]); function onChange(value, shouldCloseWidgets = shouldCloseWidgetsOnSelect) { if (shouldCloseWidgets) { closeWidgets({ reason: 'select' }); } if (onChangeProps) { onChangeProps(value); } } function onDateChange(nextValue, shouldCloseWidgets) { // React-Calendar passes an array of values when selectRange is enabled const [nextValueFrom] = Array.isArray(nextValue) ? nextValue : [nextValue]; const [valueFrom] = Array.isArray(value) ? value : [value]; if (valueFrom && nextValueFrom) { const valueFromDate = new Date(valueFrom); const nextValueFromWithHour = new Date(nextValueFrom); nextValueFromWithHour.setHours(valueFromDate.getHours(), valueFromDate.getMinutes(), valueFromDate.getSeconds(), valueFromDate.getMilliseconds()); onChange(nextValueFromWithHour, shouldCloseWidgets); } else { onChange(nextValueFrom, shouldCloseWidgets); } } function onFocus(event) { if (onFocusProps) { onFocusProps(event); } if ( // Internet Explorer still fires onFocus on disabled elements disabled || !openWidgetsOnFocus || event.target.dataset.select === 'true') { return; } switch (event.target.name) { case 'day': case 'month': case 'year': { if (isCalendarOpen) { return; } openCalendar({ reason: 'focus' }); break; } case 'hour12': case 'hour24': case 'minute': case 'second': { if (isClockOpen) { return; } openClock({ reason: 'focus' }); break; } default: } } const onKeyDown = useCallback((event) => { if (event.key === 'Escape') { closeWidgets({ reason: 'escape' }); } }, [closeWidgets]); function clear() { onChange(null); } function stopPropagation(event) { event.stopPropagation(); } const onOutsideAction = useCallback((event) => { const { current: wrapperEl } = wrapper; const { current: calendarWrapperEl } = calendarWrapper; const { current: clockWrapperEl } = clockWrapper; // Try event.composedPath first to handle clicks inside a Shadow DOM. const target = ('composedPath' in event ? event.composedPath()[0] : event.target); if (target && wrapperEl && !wrapperEl.contains(target) && (!calendarWrapperEl || !calendarWrapperEl.contains(target)) && (!clockWrapperEl || !clockWrapperEl.contains(target))) { closeWidgets({ reason: 'outsideAction' }); } }, [closeWidgets]); const handleOutsideActionListeners = useCallback((shouldListen = isCalendarOpen || isClockOpen) => { for (const event of outsideActionEvents) { if (shouldListen) { document.addEventListener(event, onOutsideAction); } else { document.removeEventListener(event, onOutsideAction); } } if (shouldListen) { document.addEventListener('keydown', onKeyDown); } else { document.removeEventListener('keydown', onKeyDown); } }, [isCalendarOpen, isClockOpen, onOutsideAction, onKeyDown]); useEffect(() => { handleOutsideActionListeners(); return () => { handleOutsideActionListeners(false); }; }, [handleOutsideActionListeners]); function renderInputs() { const [valueFrom] = Array.isArray(value) ? value : [value]; const ariaLabelProps = { amPmAriaLabel, dayAriaLabel, hourAriaLabel, minuteAriaLabel, monthAriaLabel, nativeInputAriaLabel, secondAriaLabel, yearAriaLabel, }; const placeholderProps = { dayPlaceholder, hourPlaceholder, minutePlaceholder, monthPlaceholder, secondPlaceholder, yearPlaceholder, }; return (_jsxs("div", { className: `${baseClassName}__wrapper`, children: [_jsx(DateTimeInput, { ...ariaLabelProps, ...placeholderProps, autoFocus: autoFocus, className: `${baseClassName}__inputGroup`, disabled: disabled, format: format, isWidgetOpen: isCalendarOpen || isClockOpen, locale: locale, maxDate: maxDate, maxDetail: maxDetail, minDate: minDate, name: name, onChange: onChange, onInvalidChange: onInvalidChange, required: required, showLeadingZeros: showLeadingZeros, value: valueFrom }), clearIcon !== null && (_jsx("button", { "aria-label": clearAriaLabel, className: `${baseClassName}__clear-button ${baseClassName}__button`, disabled: disabled, onClick: clear, onFocus: stopPropagation, type: "button", children: typeof clearIcon === 'function' ? createElement(clearIcon) : clearIcon })), calendarIcon !== null && !disableCalendar && (_jsx("button", { "aria-expanded": isCalendarOpen || false, "aria-label": calendarAriaLabel, className: `${baseClassName}__calendar-button ${baseClassName}__button`, disabled: disabled, onClick: toggleCalendar, onFocus: stopPropagation, type: "button", children: typeof calendarIcon === 'function' ? createElement(calendarIcon) : calendarIcon }))] })); } function renderCalendar() { if (isCalendarOpen === null || disableCalendar) { return null; } const { calendarProps, portalContainer, value } = props; const className = `${baseClassName}__calendar`; const classNames = clsx(className, `${className}--${isCalendarOpen ? 'open' : 'closed'}`); const calendar = (_jsx(Calendar, { locale: locale, maxDate: maxDate, minDate: minDate, onChange: (value) => onDateChange(value), value: value, ...calendarProps })); return portalContainer ? (createPortal(_jsx("div", { ref: calendarWrapper, className: classNames, children: calendar }), portalContainer)) : (_jsx(Fit, { children: _jsx("div", { ref: (ref) => { if (ref && !isCalendarOpen) { ref.removeAttribute('style'); } }, className: classNames, children: calendar }) })); } function renderClock() { if (isClockOpen === null || disableClock) { return null; } const { clockProps, maxDetail = 'minute', portalContainer, value } = props; const className = `${baseClassName}__clock`; const classNames = clsx(className, `${className}--${isClockOpen ? 'open' : 'closed'}`); const [valueFrom] = Array.isArray(value) ? value : [value]; const maxDetailIndex = allViews.indexOf(maxDetail); const clock = (_jsx(Clock, { locale: locale, renderMinuteHand: maxDetailIndex > 0, renderSecondHand: maxDetailIndex > 1, value: valueFrom, ...clockProps })); return portalContainer ? (createPortal(_jsx("div", { ref: clockWrapper, className: classNames, children: clock }), portalContainer)) : (_jsx(Fit, { children: _jsx("div", { ref: (ref) => { if (ref && !isClockOpen) { ref.removeAttribute('style'); } }, className: classNames, children: clock }) })); } const eventProps = useMemo(() => makeEventProps(otherProps), // biome-ignore lint/correctness/useExhaustiveDependencies: FIXME [otherProps]); return ( // biome-ignore lint/a11y/noStaticElementInteractions: False positive caused by non interactive wrapper listening for bubbling events _jsxs("div", { className: clsx(baseClassName, `${baseClassName}--${isCalendarOpen || isClockOpen ? 'open' : 'closed'}`, `${baseClassName}--${disabled ? 'disabled' : 'enabled'}`, className), "data-testid": dataTestid, id: id, ...eventProps, onFocus: onFocus, ref: wrapper, children: [renderInputs(), renderCalendar(), renderClock()] })); }