UNPKG

rsuite

Version:

A suite of react components

812 lines (774 loc) 27.5 kB
'use client'; import _extends from "@babel/runtime/helpers/esm/extends"; import React, { useEffect, useRef, useState, useMemo } from 'react'; import isNil from 'lodash/isNil'; import omit from 'lodash/omit'; import pick from 'lodash/pick'; import CalendarIcon from '@rsuite/icons/Calendar'; import TimeIcon from '@rsuite/icons/Time'; import Toolbar from "../DatePicker/Toolbar.js"; import PredefinedRanges from "../DatePicker/PredefinedRanges.js"; import Stack from "../Stack/index.js"; import DateRangeInput from "../DateRangeInput/index.js"; import InputGroup from "../InputGroup/index.js"; import Header from "./Header.js"; import useDateDisabled from "./hooks/useDateDisabled.js"; import useCustomizedInput from "../DatePicker/hooks/useCustomizedInput.js"; import Box from "../internals/Box/index.js"; import Calendar from "./Calendar.js"; import * as StaticMethods from "./disabledDateUtils.js"; import { DateRangePickerProvider } from "./DateRangePickerProvider.js"; import { getSafeCalendarDate, getMonthHoverRange, getWeekHoverRange, isSameRange } from "./utils.js"; import { DATERANGE_DISABLED_TARGET as TARGET } from "../internals/constants/index.js"; import { useStyles, useCustom, useControlled, useUniqueId, useEventCallback } from "../internals/hooks/index.js"; import { splitRanges } from "../DatePicker/utils.js"; import { PickerPopup, PickerToggleTrigger, PickerIndicator, PickerLabel, triggerPropKeys, usePickerRef, onMenuKeyDown } from "../internals/Picker/index.js"; import { forwardRef, createChainedFunction, mergeRefs, partitionHTMLProps, getStringLength } from "../internals/utils/index.js"; import { addMonths, addDays, copyTime, calendarOnlyProps, endOfDay, isValid, isBefore, isSameDay, isAfter, isSameMonth, reverseDateRangeOmitTime, startOfDay, DateMode, useDateMode } from "../internals/utils/date/index.js"; /** * A date range picker allows you to select a date range from a calendar. * * @see https://rsuitejs.com/components/date-range-picker */ const DateRangePicker = forwardRef((props, ref) => { var _ref, _ref2; const { formatDate, propsWithDefaults } = useCustom('DateRangePicker', props); const { as, block, classPrefix = 'picker', className, appearance = 'default', editable = true, cleanable = true, character = ' ~ ', calendarSnapping, defaultCalendarValue, defaultValue, plaintext, disabled, shouldDisableDate, shouldDisableHour, shouldDisableMinute, shouldDisableSecond, format, hoverRange, id: idProp, isoWeek = false, weekStart, limitEndYear = 1000, limitStartYear, locale, loading, label, popupClassName, popupStyle, oneTap, placeholder = '', placement = 'bottomStart', ranges, readOnly, showOneCalendar = false, showWeekNumbers, showMeridiem, showHeader = true, style, size, caretAs: caretAsProp, value: valueProp, monthDropdownProps, hideHours, hideMinutes, hideSeconds, onChange, onClean, onEnter, onExit, onOk, onSelect, onShortcutClick, renderTitle, renderValue, renderCell, ...restProps } = propsWithDefaults; const id = useUniqueId('rs-', idProp); const { trigger, root, target, overlay } = usePickerRef(ref); const { merge, prefix } = useStyles(classPrefix); const formatStr = format || (locale === null || locale === void 0 ? void 0 : locale.shortDateFormat) || 'yyyy-MM-dd'; const rangeFormatStr = `${formatStr}${character}${formatStr}`; const [value, setValue] = useControlled(valueProp, defaultValue !== null && defaultValue !== void 0 ? defaultValue : null); const { mode, has } = useDateMode(formatStr); // Show only the calendar month panel. formatStr = 'yyyy-MM' const onlyShowMonth = mode === DateMode.Month; // Only show the time panel. formatStr = 'HH:mm:ss' const onlyShowTime = mode === DateMode.Time; // Allows two calendar panels to display the same month. const allowSameMonth = onlyShowMonth || showOneCalendar || onlyShowTime; // Default gap between two calendars, if `showOneCalendar` is set, the gap is 0 const calendarGap = allowSameMonth ? 0 : 1; /** * Whether to complete the selection. * Everytime selection will change this value. If the value is false, it means that the selection has not been completed. * * In `oneTap` mode, select action will not change this value, its value should be true always. */ const [isSelectedIdle, setSelectedIdle] = useState(true); /** * The currently selected date range. * * The time range is selected by two clicks. After the first click, * the cursor will store a temporary event date in the process until * the second click to determine the end date of the date range. * */ const [selectedDates, setSelectedDates] = useState((_ref = valueProp !== null && valueProp !== void 0 ? valueProp : defaultValue) !== null && _ref !== void 0 ? _ref : []); // The date of the current hover, used to reduce the calculation of `handleMouseMove` const [hoverDateRange, setHoverDateRange] = useState(value); // The displayed calendar panel is rendered based on this value. const [calendarDateRange, setCalendarDateRangeValue] = useState(getSafeCalendarDate({ value: (_ref2 = value !== null && value !== void 0 ? value : defaultCalendarValue) !== null && _ref2 !== void 0 ? _ref2 : null, allowSameMonth })); /** * When hoverRange is set, `selectValue` will be updated during the hover process, * which will cause the `selectValue` to be updated after the first click, * so declare a Ref to temporarily store the `selectValue` of the first click. */ const selectRangeValueRef = useRef(null); /** * * The key of the currently active calendar panel. * Used to switch when only one calendar panel is displayed. */ const [activeCalendarKey, setActiveCalendarKey] = useState(); /** * Get the time on the calendar. */ const getCalendarDatetime = calendarKey => { const index = calendarKey === 'start' ? 0 : 1; return (calendarDateRange === null || calendarDateRange === void 0 ? void 0 : calendarDateRange[index]) || (defaultCalendarValue === null || defaultCalendarValue === void 0 ? void 0 : defaultCalendarValue[index]); }; /** * Call this function to update the calendar panel rendering benchmark value. * If params `value` is not passed, it defaults to [new Date(), addMonth(new Date(), 1)]. */ const setCalendarDateRange = ({ dateRange, calendarKey, eventName }) => { let nextValue = dateRange; // The time should remain the same when the dates in the date range are changed. if (has('time') && dateRange !== null && dateRange !== void 0 && dateRange.length && (eventName === 'changeDate' || eventName === 'changeMonth')) { const startDate = copyTime({ from: getCalendarDatetime('start'), to: dateRange[0] }); const endDate = copyTime({ from: getCalendarDatetime('end'), to: dateRange.length === 1 ? addMonths(startDate, calendarGap) : dateRange[1] }); nextValue = [startDate, endDate]; } else if (dateRange === null && typeof defaultCalendarValue !== 'undefined') { // Make the calendar render the value of defaultCalendarValue after clearing the value. nextValue = defaultCalendarValue; } const nextCalendarDate = getSafeCalendarDate({ value: nextValue, calendarKey, allowSameMonth }); setCalendarDateRangeValue(nextCalendarDate); if (onlyShowMonth && eventName === 'changeMonth') { setSelectedDates(nextCalendarDate); } }; useEffect(() => { // If value changes, update the selected and hover date values on the calendar panel. setSelectedDates(valueProp !== null && valueProp !== void 0 ? valueProp : []); setHoverDateRange(valueProp !== null && valueProp !== void 0 ? valueProp : null); }, [valueProp]); const getInputHtmlSize = () => { const padding = 4; let strings = rangeFormatStr; if (value) { const [startDate, endDate] = value; strings = `${formatDate(startDate, formatStr)}${character}${formatDate(endDate, formatStr)}`; } return getStringLength(strings) + padding; }; /** * preset hover range */ const getHoverRangeValue = date => { function getHoverRangeFunc() { if (hoverRange === 'week') { return date => getWeekHoverRange(date, { isoWeek, weekStart, locale: locale === null || locale === void 0 ? void 0 : locale.dateLocale }); } else if (hoverRange === 'month') { return getMonthHoverRange; } return hoverRange; } const hoverRangeFunc = getHoverRangeFunc(); if (isNil(hoverRangeFunc)) { return null; } let hoverValues = hoverRangeFunc(date); const isHoverRangeValid = hoverValues instanceof Array && hoverValues.length === 2; if (!isHoverRangeValid) { return null; } if (isAfter(hoverValues[0], hoverValues[1])) { hoverValues = reverseDateRangeOmitTime(hoverValues); } return hoverValues; }; const setDateRange = (event, nextValue, closeOverlay = true) => { // If nextValue is null, it means that the user is erasing the selected dates. setSelectedDates(nextValue !== null && nextValue !== void 0 ? nextValue : []); setValue(nextValue); if (!isSameRange(nextValue, value, formatStr)) { onChange === null || onChange === void 0 || onChange(nextValue, event); } // `closeOverlay` default value is `true` if (closeOverlay !== false) { handleClose(); } }; /** * Select the date range. If oneTap is not set, you need to click twice to select the start time and end time. * The MouseMove event is called between the first click and the second click to update the selection state. */ const onMouseMove = useEventCallback(date => { const nextHoverDateRange = getHoverRangeValue(date); // If hasDoneSelect is false, // it means there's already one selected date // and waiting for user to select the second date to complete the selection. if (!isSelectedIdle) { // If `hoverRange` is set, you need to change the value of hoverDateRange according to the rules if (!isNil(nextHoverDateRange) && !isNil(selectRangeValueRef.current)) { let nextSelectedDates = [selectRangeValueRef.current[0], nextHoverDateRange[1]]; if (isBefore(nextHoverDateRange[0], selectRangeValueRef.current[0])) { nextSelectedDates = [nextHoverDateRange[0], selectRangeValueRef.current[1]]; } setSelectedDates(nextSelectedDates); } else { setHoverDateRange(prevHoverValue => isNil(prevHoverValue) ? null : [prevHoverValue[0], date]); } // Before the first click, if nextHoverDateRange has a value, hoverDateRange needs to be updated } else if (!isNil(nextHoverDateRange)) { setHoverDateRange(nextHoverDateRange); } }); /** * Callback for selecting a date cell in the calendar grid */ const handleSelectDate = useEventCallback((index, date, event) => { const calendarKey = index === 0 ? 'start' : 'end'; let nextSelectDates = hoverDateRange !== null && hoverDateRange !== void 0 ? hoverDateRange : []; const hoverRangeValue = getHoverRangeValue(date); const noHoverRangeValid = isNil(hoverRangeValue); // in `oneTap` mode if (oneTap) { setDateRange(event, noHoverRangeValid ? [startOfDay(date), endOfDay(date)] : hoverRangeValue); onSelect === null || onSelect === void 0 || onSelect(date, event); return; } // no preset hover range can use if (noHoverRangeValid) { // start select if (isSelectedIdle) { nextSelectDates = [date]; } else { // finish select nextSelectDates[1] = date; } } else { if (!isSelectedIdle) { nextSelectDates = selectedDates; selectRangeValueRef.current = null; } else { nextSelectDates = hoverRangeValue; selectRangeValueRef.current = hoverRangeValue; } } if (nextSelectDates.length === 2) { // If user have completed the selection, then sort the selected dates. if (isAfter(nextSelectDates[0], nextSelectDates[1])) { nextSelectDates = reverseDateRangeOmitTime(nextSelectDates); } if (has('time')) { nextSelectDates = [copyTime({ from: getCalendarDatetime('start'), to: nextSelectDates[0] }), copyTime({ from: getCalendarDatetime('end'), to: nextSelectDates[1] })]; } setHoverDateRange(nextSelectDates); } else { setHoverDateRange([nextSelectDates[0], nextSelectDates[0]]); } if (isSelectedIdle) { setActiveCalendarKey('end'); } else { setActiveCalendarKey('start'); } setSelectedDates(nextSelectDates); if (!isSameMonth(calendarDateRange[index], date) || calendarSnapping) { setCalendarDateRange({ dateRange: nextSelectDates, calendarKey, eventName: 'changeDate' }); } onSelect === null || onSelect === void 0 || onSelect(date, event); setSelectedIdle(!isSelectedIdle); }); /** * If `selectValue` changed, there will be the following effects. * 1. Check if the selection is completed. * 2. if the selection is completed, set the temporary `hoverValue` empty. */ useEffect(() => { const selectValueLength = selectedDates.length; const doneSelected = selectValueLength === 0 || selectValueLength === 2; doneSelected && setHoverDateRange(null); }, [selectedDates]); const onChangeCalendarMonth = useEventCallback((index, date) => { const calendarKey = index === 0 ? 'start' : 'end'; const nextCalendarDate = Array.from(calendarDateRange); nextCalendarDate[index] = date; // If allowSameMonth is true, the start and end dates should be the same if (allowSameMonth) { nextCalendarDate[0] = date; nextCalendarDate[1] = date; } setCalendarDateRange({ dateRange: nextCalendarDate, calendarKey, eventName: 'changeMonth' }); }); const onChangeCalendarTime = useEventCallback((index, date) => { const calendarKey = index === 0 ? 'start' : 'end'; const nextCalendarDate = Array.from(calendarDateRange); nextCalendarDate[index] = date; setCalendarDateRange({ dateRange: nextCalendarDate, calendarKey, eventName: 'changeTime' }); setSelectedDates(prev => { const next = [...prev]; // if next[index] is not empty, only update the time after aligning the year, month and day next[index] = next[index] ? copyTime({ from: date, to: next[index] }) : new Date(date.valueOf()); return next; }); }); const handleEnter = useEventCallback(() => { let nextCalendarDate; if (value && value.length) { const [startDate, endData] = value; nextCalendarDate = [startDate, isSameMonth(startDate, endData) ? addMonths(endData, calendarGap) : endData]; } else { // Reset the date on the calendar to the default date nextCalendarDate = getSafeCalendarDate({ value: defaultCalendarValue !== null && defaultCalendarValue !== void 0 ? defaultCalendarValue : null, allowSameMonth }); } setSelectedDates(value !== null && value !== void 0 ? value : []); setCalendarDateRange({ dateRange: nextCalendarDate }); }); const handleExit = useEventCallback(() => { setSelectedIdle(true); }); /** * Toolbar operation callback function */ const handleShortcutPageDate = useEventCallback((range, closeOverlay = false, event) => { const value = range.value; setCalendarDateRange({ dateRange: value, eventName: 'shortcutSelection' }); if (closeOverlay) { setDateRange(event, value, closeOverlay); } else { setSelectedDates(value !== null && value !== void 0 ? value : []); } onShortcutClick === null || onShortcutClick === void 0 || onShortcutClick(range, event); // End unfinished selections. setSelectedIdle(true); }); const calculateDateRange = () => { const [start = calendarDateRange[0], end = calendarDateRange[1]] = selectedDates; if (onlyShowTime) { return [start, end]; } return selectedDates; }; const handleClickOK = useEventCallback(event => { const nextValue = calculateDateRange(); setDateRange(event, nextValue); onOk === null || onOk === void 0 || onOk(nextValue, event); }); const handleClean = useEventCallback(event => { setCalendarDateRange({ dateRange: null }); setDateRange(event, null); onClean === null || onClean === void 0 || onClean(event); event.stopPropagation(); }); /** * Callback after the input box value is changed. */ const handleInputChange = useEventCallback((value, event) => { if (!value) { return; } const [startDate, endDate] = value; const selectValue = [startDate, endDate]; setHoverDateRange(selectValue); setSelectedDates(selectValue); setCalendarDateRange({ dateRange: selectValue }); setDateRange(event, selectValue, false); }); /** * Check if the date is disabled */ const isDateDisabled = useDateDisabled({ shouldDisableDate }); /** * Check if a date range is disabled */ const isRangeDisabled = (start, end, target) => { if (isDateDisabled) { // If the date is between the start and the end the button is disabled while (isBefore(start, end) || isSameDay(start, end)) { if (isDateDisabled(start, { selectDate: selectedDates, selectedDone: isSelectedIdle, target })) { return true; } start = addDays(start, 1); } } return false; }; /** * Determine if the OK button should be disabled */ const shouldDisableOkButton = () => { const [startDate, endDate] = calculateDateRange(); // Check if start or end dates are missing if (!startDate || !endDate) { return true; } // Additional condition if only showing time if (!onlyShowTime && !isSelectedIdle) { return true; } // Check if there is any error in the selected date range if (isErrorValue([startDate, endDate])) { return true; } return false; }; /** * Check if a shortcut is disabled based on the selected date range */ const shouldDisableShortcut = (selectedDates = []) => { if (selectedDates === null) { return false; } const [startDate, endDate] = selectedDates; // Disable if either start or end date is missing if (!startDate || !endDate) { return true; } // Check if the date range is disabled for the shortcut return isRangeDisabled(startDate, endDate, TARGET.TOOLBAR_SHORTCUT); }; const handleClose = useEventCallback(() => { var _trigger$current, _trigger$current$clos; (_trigger$current = trigger.current) === null || _trigger$current === void 0 || (_trigger$current$clos = _trigger$current.close) === null || _trigger$current$clos === void 0 || _trigger$current$clos.call(_trigger$current); }); const handleInputKeyDown = useEventCallback(event => { onMenuKeyDown(event, { esc: handleClose, enter: () => { var _trigger$current2; const { open } = ((_trigger$current2 = trigger.current) === null || _trigger$current2 === void 0 ? void 0 : _trigger$current2.getState()) || {}; if (!open) { var _trigger$current3; (_trigger$current3 = trigger.current) === null || _trigger$current3 === void 0 || _trigger$current3.open(); } } }); }); const disableCalendarDate = isDateDisabled ? (date, values, type) => { return isDateDisabled(date, { selectDate: values, selectedDone: isSelectedIdle, target: type }); } : undefined; const renderCalendarOverlay = (positionProps, speakerRef) => { const { className } = positionProps; const classes = merge(className, popupClassName, prefix('popup-daterange')); const panelClasses = prefix('daterange-panel', { 'daterange-panel-show-one-calendar': showOneCalendar, 'daterange-panel-only-time': onlyShowTime }); /** * Set a min-width (528px) when there are two calendars * @see https://github.com/rsuite/rsuite/issues/3522 */ const panelStyles = { minWidth: showOneCalendar || onlyShowTime ? 'auto' : 528 }; const calendarProps = { locale, isoWeek, weekStart, limitEndYear, showMeridiem, calendarDateRange, limitStartYear, showWeekNumbers, format: formatStr, value: selectedDates, monthDropdownProps, hoverRangeValue: hoverDateRange !== null && hoverDateRange !== void 0 ? hoverDateRange : undefined, hideHours, hideMinutes, hideSeconds, disabledHours: shouldDisableHour, disabledMinutes: shouldDisableMinute, disabledSeconds: shouldDisableSecond, disabledDate: disableCalendarDate, onSelect: handleSelectDate, onChangeCalendarMonth, onChangeCalendarTime, onMouseMove, renderTitle, renderCellOnPicker: renderCell }; const getCalendars = () => { if (showOneCalendar) { return /*#__PURE__*/React.createElement(Calendar, _extends({ index: activeCalendarKey === 'end' ? 1 : 0 }, calendarProps)); } return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Calendar, _extends({ index: 0 }, calendarProps)), /*#__PURE__*/React.createElement(Calendar, _extends({ index: 1 }, calendarProps))); }; const { sideRanges, bottomRanges } = splitRanges(ranges); return /*#__PURE__*/React.createElement(PickerPopup, { role: "dialog", "aria-labelledby": label ? `${id}-label` : undefined, tabIndex: -1, className: classes, ref: mergeRefs(overlay, speakerRef), target: trigger, style: popupStyle }, /*#__PURE__*/React.createElement("div", { className: panelClasses, style: panelStyles }, /*#__PURE__*/React.createElement(Stack, { align: "flex-start", h: "100%" }, sideRanges && sideRanges.length > 0 && /*#__PURE__*/React.createElement(PredefinedRanges, { direction: "column", spacing: 0, className: prefix('daterange-predefined'), ranges: sideRanges, calendarDate: calendarDateRange, locale: locale, disableShortcut: shouldDisableShortcut, onShortcutClick: handleShortcutPageDate, "data-testid": "daterange-predefined-side" }), /*#__PURE__*/React.createElement(Box, { className: prefix('box') }, /*#__PURE__*/React.createElement("div", { className: prefix('daterange-content') }, showHeader && /*#__PURE__*/React.createElement(Header, { value: isSelectedIdle ? selectedDates : hoverDateRange, formatStr: formatStr, character: character, clickable: showOneCalendar, activeKey: activeCalendarKey, onSelect: setActiveCalendarKey }), /*#__PURE__*/React.createElement("div", { className: prefix(`daterange-calendar-${showOneCalendar ? 'single' : 'group'}`) }, /*#__PURE__*/React.createElement(DateRangePickerProvider, { value: { isSelectedIdle } }, getCalendars()))), /*#__PURE__*/React.createElement(Toolbar, { locale: locale, calendarDate: selectedDates, disableOkBtn: shouldDisableOkButton, disableShortcut: shouldDisableShortcut, hideOkBtn: oneTap, onOk: handleClickOK, onShortcutClick: handleShortcutPageDate, ranges: bottomRanges }))))); }; const hasValue = !isNil(value) && value.length > 1; const caretAs = useMemo(() => { if (caretAsProp === null) { return null; } return caretAsProp || (onlyShowTime ? TimeIcon : CalendarIcon); }, [caretAsProp, onlyShowTime]); const isErrorValue = value => { if (!value) { return false; } // If the value is an empty array, it is not an error value. if (Array.isArray(value) && value.length === 0) { return false; } const [startDate, endDate] = value; if (!isValid(startDate) || !isValid(endDate)) { return true; } if (isBefore(endDate, startDate)) { return true; } const disabledOptions = { selectDate: value, selectedDone: isSelectedIdle, target: TARGET.INPUT }; if (isDateDisabled !== null && isDateDisabled !== void 0 && isDateDisabled(startDate, disabledOptions) || isDateDisabled !== null && isDateDisabled !== void 0 && isDateDisabled(endDate, disabledOptions)) { return true; } return false; }; const [ariaProps, rest] = partitionHTMLProps(restProps, { htmlProps: [], includeAria: true }); const showCleanButton = cleanable && hasValue && !readOnly; const invalidValue = value && isErrorValue(value); const { customValue, inputReadOnly, Input, events } = useCustomizedInput({ mode: 'dateRange', value, formatStr, renderValue, readOnly, editable, loading }); const triggerProps = { ...pick(props, triggerPropKeys), onEnter: createChainedFunction(events.onActive, handleEnter, onEnter), onExit: createChainedFunction(events.onInactive, handleExit, onExit) }; return /*#__PURE__*/React.createElement(PickerToggleTrigger, { as: as, pickerType: "date-range", classPrefix: classPrefix, className: merge(className, { [prefix('error')]: invalidValue }), block: block, disabled: disabled, appearance: appearance, style: style, rootRef: root, ref: trigger, placement: placement, trigger: "active", triggerProps: triggerProps, speaker: renderCalendarOverlay, "data-cleanable": cleanable }, plaintext ? /*#__PURE__*/React.createElement(DateRangeInput, { value: value, format: formatStr, plaintext: plaintext }) : /*#__PURE__*/React.createElement(InputGroup, _extends({}, omit(rest, [...calendarOnlyProps, ...triggerPropKeys]), { inside: true, className: prefix`input-group`, disabled: disabled, size: size }), /*#__PURE__*/React.createElement(PickerLabel, { className: prefix`label`, id: `${id}-label` }, label), /*#__PURE__*/React.createElement(Input, _extends({ "aria-haspopup": "dialog", "aria-invalid": invalidValue, "aria-labelledby": label ? `${id}-label` : undefined }, ariaProps, { ref: target, id: id, value: customValue || value, character: character, format: formatStr, placeholder: placeholder ? placeholder : rangeFormatStr, disabled: disabled, readOnly: inputReadOnly, htmlSize: getInputHtmlSize(), onChange: handleInputChange, onKeyDown: handleInputKeyDown })), /*#__PURE__*/React.createElement(PickerIndicator, { size: size, loading: loading, caretAs: caretAs, onClose: handleClean, showCleanButton: showCleanButton }))); }, StaticMethods); DateRangePicker.displayName = 'DateRangePicker'; export default DateRangePicker;