react-datetime-picker
Version:
A date range picker for your React app.
257 lines (256 loc) • 12.8 kB
JavaScript
'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()] }));
}