@wojtekmaj/react-timerange-picker
Version:
A time range picker for your React app.
188 lines (187 loc) • 9.03 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 Clock from 'react-clock';
import Fit from 'react-fit';
import TimeInput from 'react-time-picker/dist/TimeInput';
const baseClassName = 'react-timerange-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 ClockIcon = (_jsxs("svg", { ...iconProps, "aria-hidden": "true", className: `${baseClassName}__clock-button__icon ${baseClassName}__button__icon`, fill: "none", children: [_jsx("circle", { cx: "9.5", cy: "9.5", r: "7.5" }), _jsx("path", { d: "M9.5 4.5 v5 h4" })] }));
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 TimeRangePicker(props) {
const { amPmAriaLabel, autoFocus, className, clearAriaLabel, clearIcon = ClearIcon, clockAriaLabel, clockIcon = ClockIcon, closeClock: shouldCloseClockOnSelect = true, 'data-testid': dataTestid, disableClock, disabled, format, hourAriaLabel, hourPlaceholder, id, isOpen: isOpenProps = null, locale, maxDetail = 'minute', maxTime, minTime, minuteAriaLabel, minutePlaceholder, name = 'timerange', nativeInputAriaLabel, onChange: onChangeProps, onClockClose, onClockOpen, onFocus: onFocusProps, onInvalidChange, openClockOnFocus = true, rangeDivider = '–', required, secondAriaLabel, secondPlaceholder, shouldCloseClock, shouldOpenClock, value, ...otherProps } = props;
const [isOpen, setIsOpen] = useState(isOpenProps);
const wrapper = useRef(null);
const clockWrapper = useRef(null);
useEffect(() => {
setIsOpen(isOpenProps);
}, [isOpenProps]);
function openClock({ reason }) {
if (shouldOpenClock) {
if (!shouldOpenClock({ reason })) {
return;
}
}
setIsOpen(true);
if (onClockOpen) {
onClockOpen();
}
}
const closeClock = useCallback(({ reason }) => {
if (shouldCloseClock) {
if (!shouldCloseClock({ reason })) {
return;
}
}
setIsOpen(false);
if (onClockClose) {
onClockClose();
}
}, [onClockClose, shouldCloseClock]);
function toggleClock() {
if (isOpen) {
closeClock({ reason: 'buttonClick' });
}
else {
openClock({ reason: 'buttonClick' });
}
}
function onChange(value, shouldCloseClock = shouldCloseClockOnSelect) {
if (shouldCloseClock) {
closeClock({ reason: 'select' });
}
if (onChangeProps) {
onChangeProps(value);
}
}
function onChangeFrom(valueFrom, closeClock) {
const [, valueTo] = Array.isArray(value) ? value : [value];
onChange([valueFrom, valueTo || null], closeClock);
}
function onChangeTo(valueTo, closeClock) {
const [valueFrom] = Array.isArray(value) ? value : [value];
onChange([valueFrom || null, valueTo], closeClock);
}
function onFocus(event) {
if (onFocusProps) {
onFocusProps(event);
}
if (
// Internet Explorer still fires onFocus on disabled elements
disabled ||
isOpen ||
!openClockOnFocus ||
event.target.dataset.select === 'true') {
return;
}
openClock({ reason: 'focus' });
}
const onKeyDown = useCallback((event) => {
if (event.key === 'Escape') {
closeClock({ reason: 'escape' });
}
}, [closeClock]);
function clear() {
onChange(null);
}
function stopPropagation(event) {
event.stopPropagation();
}
const onOutsideAction = useCallback((event) => {
const { current: wrapperEl } = wrapper;
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) &&
(!clockWrapperEl || !clockWrapperEl.contains(target))) {
closeClock({ reason: 'outsideAction' });
}
}, [closeClock]);
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 = {
amPmAriaLabel,
hourAriaLabel,
minuteAriaLabel,
nativeInputAriaLabel,
secondAriaLabel,
};
const placeholderProps = {
hourPlaceholder,
minutePlaceholder,
secondPlaceholder,
};
const commonProps = {
...ariaLabelProps,
...placeholderProps,
className: `${baseClassName}__inputGroup`,
disabled,
format,
isClockOpen: isOpen,
locale,
maxDetail,
maxTime,
minTime,
onInvalidChange,
required,
};
return (_jsxs("div", { className: `${baseClassName}__wrapper`, children: [_jsx(TimeInput, { ...commonProps, autoFocus: autoFocus, name: `${name}_from`, onChange: onChangeFrom, value: valueFrom }), _jsx("span", { className: `${baseClassName}__range-divider`, children: rangeDivider }), _jsx(TimeInput, { ...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 })), clockIcon !== null && !disableClock && (_jsx("button", { "aria-expanded": isOpen || false, "aria-label": clockAriaLabel, className: `${baseClassName}__clock-button ${baseClassName}__button`, disabled: disabled, onClick: toggleClock, onFocus: stopPropagation, type: "button", children: typeof clockIcon === 'function' ? createElement(clockIcon) : clockIcon }))] }));
}
function renderClock() {
if (isOpen === null || disableClock) {
return null;
}
const { clockProps, portalContainer, value } = props;
const className = `${baseClassName}__clock`;
const classNames = clsx(className, `${className}--${isOpen ? 'open' : 'closed'}`);
const [valueFrom] = Array.isArray(value) ? value : [value];
const clock = _jsx(Clock, { locale: locale, value: valueFrom, ...clockProps });
return portalContainer ? (createPortal(_jsx("div", { ref: clockWrapper, className: classNames, children: clock }), portalContainer)) : (_jsx(Fit, { children: _jsx("div", { ref: (ref) => {
if (ref && !isOpen) {
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}--${isOpen ? 'open' : 'closed'}`, `${baseClassName}--${disabled ? 'disabled' : 'enabled'}`, className), "data-testid": dataTestid, id: id, ...eventProps, onFocus: onFocus, ref: wrapper, children: [renderInputs(), renderClock()] }));
}