UNPKG

@wojtekmaj/react-timerange-picker

Version:

A time range picker for your React app.

188 lines (187 loc) 9.03 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 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()] })); }