@wojtekmaj/react-daterange-picker
Version:
A date range picker for your React app.
191 lines (190 loc) • 9.7 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 DateInput from 'react-date-picker/dist/DateInput';
import Fit from 'react-fit';
const baseClassName = 'react-daterange-picker';
const outsideActionEvents = ['mousedown', 'focusin', 'touchstart'];
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 DateRangePicker(props) {
const { autoFocus, calendarAriaLabel, calendarIcon = CalendarIcon, className, clearAriaLabel, clearIcon = ClearIcon, closeCalendar: shouldCloseCalendarOnSelect = true, 'data-testid': dataTestid, dayAriaLabel, dayPlaceholder, disableCalendar, disabled, format, id, isOpen: isOpenProps = null, locale, maxDate, maxDetail = 'month', minDate, monthAriaLabel, monthPlaceholder, name = 'daterange', nativeInputAriaLabel, onCalendarClose, onCalendarOpen, onChange: onChangeProps, onFocus: onFocusProps, onInvalidChange, openCalendarOnFocus = true, rangeDivider = '–', required, shouldCloseCalendar, shouldOpenCalendar, showLeadingZeros, value, yearAriaLabel, yearPlaceholder, ...otherProps } = props;
const [isOpen, setIsOpen] = useState(isOpenProps);
const wrapper = useRef(null);
const calendarWrapper = useRef(null);
useEffect(() => {
setIsOpen(isOpenProps);
}, [isOpenProps]);
function openCalendar({ reason }) {
if (shouldOpenCalendar) {
if (!shouldOpenCalendar({ reason })) {
return;
}
}
setIsOpen(true);
if (onCalendarOpen) {
onCalendarOpen();
}
}
const closeCalendar = useCallback(({ reason }) => {
if (shouldCloseCalendar) {
if (!shouldCloseCalendar({ reason })) {
return;
}
}
setIsOpen(false);
if (onCalendarClose) {
onCalendarClose();
}
}, [onCalendarClose, shouldCloseCalendar]);
function toggleCalendar() {
if (isOpen) {
closeCalendar({ reason: 'buttonClick' });
}
else {
openCalendar({ reason: 'buttonClick' });
}
}
function onChange(value, shouldCloseCalendar = shouldCloseCalendarOnSelect) {
if (shouldCloseCalendar) {
closeCalendar({ reason: 'select' });
}
if (onChangeProps) {
onChangeProps(value);
}
}
function onChangeFrom(nextValue, closeCalendar) {
const [nextValueFrom] = Array.isArray(nextValue) ? nextValue : [nextValue];
const [, valueTo] = Array.isArray(value) ? value : [value];
const valueToDate = valueTo ? new Date(valueTo) : null;
onChange([nextValueFrom, valueToDate], closeCalendar);
}
function onChangeTo(nextValue, closeCalendar) {
const [, nextValueTo] = Array.isArray(nextValue) ? nextValue : [null, nextValue];
const [valueFrom] = Array.isArray(value) ? value : [value];
const valueFromDate = valueFrom ? new Date(valueFrom) : null;
onChange([valueFromDate, nextValueTo], closeCalendar);
}
function onFocus(event) {
if (onFocusProps) {
onFocusProps(event);
}
if (
// Internet Explorer still fires onFocus on disabled elements
disabled ||
isOpen ||
!openCalendarOnFocus ||
event.target.dataset.select === 'true') {
return;
}
openCalendar({ reason: 'focus' });
}
const onKeyDown = useCallback((event) => {
if (event.key === 'Escape') {
closeCalendar({ reason: 'escape' });
}
}, [closeCalendar]);
function clear() {
onChange(null);
}
function stopPropagation(event) {
event.stopPropagation();
}
const onOutsideAction = useCallback((event) => {
const { current: wrapperEl } = wrapper;
const { current: calendarWrapperEl } = calendarWrapper;
// 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))) {
closeCalendar({ reason: 'outsideAction' });
}
}, [closeCalendar]);
const handleOutsideActionListeners = useCallback((shouldListen = isOpen) => {
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);
}
}, [isOpen, onOutsideAction, onKeyDown]);
// biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on isOpen change
useEffect(() => {
handleOutsideActionListeners();
return () => {
handleOutsideActionListeners(false);
};
}, [handleOutsideActionListeners, isOpen]);
function renderInputs() {
const [valueFrom, valueTo] = Array.isArray(value) ? value : [value];
const ariaLabelProps = {
dayAriaLabel,
monthAriaLabel,
nativeInputAriaLabel,
yearAriaLabel,
};
const placeholderProps = {
dayPlaceholder,
monthPlaceholder,
yearPlaceholder,
};
const commonProps = {
...ariaLabelProps,
...placeholderProps,
className: `${baseClassName}__inputGroup`,
disabled,
format,
isCalendarOpen: isOpen,
locale,
maxDate,
maxDetail,
minDate,
onInvalidChange,
required,
showLeadingZeros,
};
return (_jsxs("div", { className: `${baseClassName}__wrapper`, children: [_jsx(DateInput, { ...commonProps, autoFocus: autoFocus, name: `${name}_from`, onChange: onChangeFrom, returnValue: "start", value: valueFrom }), _jsx("span", { className: `${baseClassName}__range-divider`, children: rangeDivider }), _jsx(DateInput, { ...commonProps, name: `${name}_to`, onChange: onChangeTo, returnValue: "end", 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": isOpen || 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 (isOpen === null || disableCalendar) {
return null;
}
const { calendarProps, portalContainer, value } = props;
const className = `${baseClassName}__calendar`;
const classNames = clsx(className, `${className}--${isOpen ? 'open' : 'closed'}`);
const calendar = (_jsx(Calendar, { locale: locale, maxDate: maxDate, maxDetail: maxDetail, minDate: minDate, onChange: (value) => onChange(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 && !isOpen) {
ref.removeAttribute('style');
}
}, className: classNames, children: calendar }) }));
}
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}--${isOpen ? 'open' : 'closed'}`, `${baseClassName}--${disabled ? 'disabled' : 'enabled'}`, className), "data-testid": dataTestid, id: id, ...eventProps, onFocus: onFocus, ref: wrapper, children: [renderInputs(), renderCalendar()] }));
}