@wojtekmaj/react-datetimerange-picker
Version:
A datetime range picker for your React app.
292 lines (291 loc) • 14.3 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 DateTimeInput from 'react-datetime-picker/dist/DateTimeInput';
import Fit from 'react-fit';
const baseClassName = 'react-datetimerange-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 DateTimeRangePicker(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 = 'datetimerange', nativeInputAriaLabel, onCalendarClose, onCalendarOpen, onChange: onChangeProps, onClockClose, onClockOpen, onFocus: onFocusProps, onInvalidChange, openWidgetsOnFocus = true, rangeDivider = '–', 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 : isCalendarOpen);
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 onChangeFrom(valueFrom, closeCalendar) {
const [, valueTo] = Array.isArray(value) ? value : [value];
const valueToDate = valueTo ? new Date(valueTo) : null;
onChange([valueFrom, valueToDate], closeCalendar);
}
function onChangeTo(valueTo, closeCalendar) {
const [valueFrom] = Array.isArray(value) ? value : [value];
const valueFromDate = valueFrom ? new Date(valueFrom) : null;
onChange([valueFromDate, valueTo], closeCalendar);
}
function onDateChange(nextValue, shouldCloseWidgets) {
// React-Calendar passes an array of values when selectRange is enabled
const [rawNextValueFrom, rawNextValueTo] = Array.isArray(nextValue) ? nextValue : [nextValue];
const [valueFrom, valueTo] = Array.isArray(value) ? value : [value];
const nextValueFrom = (() => {
if (!valueFrom || !rawNextValueFrom) {
return rawNextValueFrom;
}
const valueFromDate = new Date(valueFrom);
const nextValueFromWithHour = new Date(rawNextValueFrom);
nextValueFromWithHour.setHours(valueFromDate.getHours(), valueFromDate.getMinutes(), valueFromDate.getSeconds(), valueFromDate.getMilliseconds());
return nextValueFromWithHour;
})();
const nextValueTo = (() => {
if (!valueTo || !rawNextValueTo) {
return rawNextValueTo;
}
const valueToDate = new Date(valueTo);
const nextValueToWithHour = new Date(rawNextValueTo);
nextValueToWithHour.setHours(valueToDate.getHours(), valueToDate.getMinutes(), valueToDate.getSeconds(), valueToDate.getMilliseconds());
return nextValueToWithHour;
})();
onChange([nextValueFrom || null, nextValueTo || null], 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, valueTo] = Array.isArray(value) ? value : [value];
const ariaLabelProps = {
amPmAriaLabel,
dayAriaLabel,
hourAriaLabel,
minuteAriaLabel,
monthAriaLabel,
nativeInputAriaLabel,
secondAriaLabel,
yearAriaLabel,
};
const placeholderProps = {
dayPlaceholder,
hourPlaceholder,
minutePlaceholder,
monthPlaceholder,
secondPlaceholder,
yearPlaceholder,
};
const commonProps = {
...ariaLabelProps,
...placeholderProps,
className: `${baseClassName}__inputGroup`,
disabled,
format,
isWidgetOpen: isCalendarOpen || isClockOpen,
locale,
maxDate,
maxDetail,
minDate,
onInvalidChange,
required,
showLeadingZeros,
};
return (_jsxs("div", { className: `${baseClassName}__wrapper`, children: [_jsx(DateTimeInput, { ...commonProps, autoFocus: autoFocus, name: `${name}_from`, onChange: onChangeFrom, value: valueFrom }), _jsx("span", { className: `${baseClassName}__range-divider`, children: rangeDivider }), _jsx(DateTimeInput, { ...commonProps, name: `${name}_to`, onChange: onChangeTo, value: valueTo }), 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), selectRange: true, 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()] }));
}