rsuite
Version:
A suite of react components
505 lines (487 loc) • 15.8 kB
JavaScript
'use client';
import _extends from "@babel/runtime/helpers/esm/extends";
import React, { useMemo } from 'react';
import mapValues from 'lodash/mapValues';
import pick from 'lodash/pick';
import CalenderSimpleIcon from '@rsuite/icons/CalenderSimple';
import TimeIcon from '@rsuite/icons/Time';
import CalendarContainer from "../Calendar/CalendarContainer.js";
import Toolbar from "./Toolbar.js";
import Stack from "../Stack/index.js";
import PredefinedRanges from "./PredefinedRanges.js";
import DateInput from "../DateInput/index.js";
import InputGroup from "../InputGroup/index.js";
import useMonthView from "./hooks/useMonthView.js";
import useFocus from "./hooks/useFocus.js";
import useCustomizedInput from "./hooks/useCustomizedInput.js";
import Box from "../internals/Box/index.js";
import { useCalendarDate } from "../Calendar/hooks/index.js";
import { isEveryDateInMonth } from "../Calendar/utils/index.js";
import { forwardRef, mergeRefs, partitionHTMLProps, createChainedFunction } from "../internals/utils/index.js";
import { useStyles, useCustom, useControlled, useUniqueId, useEventCallback } from "../internals/hooks/index.js";
import { isValid, copyTime, disableTime, DateMode, useDateMode, calendarOnlyProps } from "../internals/utils/date/index.js";
import { PickerPopup, PickerLabel, PickerIndicator, PickerToggleTrigger, triggerPropKeys, usePickerRef, onMenuKeyDown } from "../internals/Picker/index.js";
import { OverlayCloseCause } from "../internals/Overlay/OverlayTrigger.js";
import { splitRanges, getRestProps } from "./utils.js";
import { startOfToday } from "../internals/utils/date/index.js";
/**
* A date picker allows users to select a date from a calendar.
*
* @see https://rsuitejs.com/components/date-picker
*/
const DatePicker = forwardRef((props, ref) => {
const {
propsWithDefaults
} = useCustom('DatePicker', props);
const {
as,
block,
className,
classPrefix = 'picker',
calendarDefaultDate,
cleanable = true,
caretAs: caretAsProp,
editable = true,
defaultValue,
disabled,
readOnly: readOnly,
plaintext,
format,
id: idProp,
isoWeek,
weekStart,
limitEndYear = 1000,
limitStartYear,
locale,
loading,
label,
popupClassName,
popupStyle,
appearance = 'default',
placement = 'bottomStart',
oneTap,
placeholder = '',
ranges,
value: valueProp,
showMeridiem,
showWeekNumbers,
style,
size,
monthDropdownProps,
shouldDisableDate,
shouldDisableHour,
shouldDisableMinute,
shouldDisableSecond,
onChange,
onChangeCalendarDate,
onClean,
onEnter,
onExit,
onNextMonth,
onOk,
onPrevMonth,
onSelect,
onToggleMonthDropdown,
onToggleTimeDropdown,
onShortcutClick,
renderCell,
renderValue,
...restProps
} = propsWithDefaults;
const id = useUniqueId('rs-', idProp);
const {
trigger,
root,
target,
overlay
} = usePickerRef(ref);
const formatStr = format || (locale === null || locale === void 0 ? void 0 : locale.shortDateFormat) || 'yyyy-MM-dd';
const {
merge,
prefix
} = useStyles(classPrefix);
const [value, setValue] = useControlled(valueProp, defaultValue);
const {
calendarDate,
setCalendarDate,
resetCalendarDate
} = useCalendarDate(value, calendarDefaultDate);
const {
setMonthView,
monthView,
toggleMonthView
} = useMonthView({
onToggleMonthDropdown
});
const {
mode
} = useDateMode(formatStr);
// Show only the calendar month panel. formatStr = 'yyyy-MM'
const showMonth = mode === DateMode.Month || monthView;
const {
focusInput,
focusSelectedDate,
onKeyFocusEvent
} = useFocus({
target,
showMonth,
id,
locale
});
/**
* Check whether the date is disabled.
*/
const isDateDisabled = date => {
if (typeof shouldDisableDate === 'function') {
return shouldDisableDate(date);
}
return false;
};
/**
* Check whether the time is within the time range of the shortcut option in the toolbar.
*/
const isDatetimeDisabled = date => {
return (isDateDisabled === null || isDateDisabled === void 0 ? void 0 : isDateDisabled(date)) || disableTime(props, date);
};
/**
* Check whether the month is disabled.
* If any day in the month is disabled, the entire month is disabled
*/
const isMonthDisabled = date => {
return isEveryDateInMonth(date.getFullYear(), date.getMonth(), isDateDisabled);
};
/**
* Whether "OK" button is disabled
*
* - If format is date, disable ok button if selected date is disabled
* - If format is month, disable ok button if all dates in the month of selected date are disabled
*/
const isOkButtonDisabled = selectedDate => {
if (mode === DateMode.Month) {
return isMonthDisabled(selectedDate);
}
return isDatetimeDisabled(selectedDate);
};
const isErrorValue = value => {
if (!isValid(value)) {
return true;
} else if (value && isDateDisabled(value)) {
return true;
}
return false;
};
/**
* Switch to the callback triggered after the next month.
*/
const handleMoveForward = useEventCallback(nextPageDate => {
setCalendarDate(nextPageDate);
onNextMonth === null || onNextMonth === void 0 || onNextMonth(nextPageDate);
onChangeCalendarDate === null || onChangeCalendarDate === void 0 || onChangeCalendarDate(nextPageDate);
});
/**
* Switch to the callback triggered after the previous month.
*/
const handleMoveBackward = useEventCallback(nextPageDate => {
setCalendarDate(nextPageDate);
onPrevMonth === null || onPrevMonth === void 0 || onPrevMonth(nextPageDate);
onChangeCalendarDate === null || onChangeCalendarDate === void 0 || onChangeCalendarDate(nextPageDate);
});
/**
* The callback triggered when the date changes.
*/
const handleDateChange = useEventCallback((nextValue, event) => {
onSelect === null || onSelect === void 0 || onSelect(nextValue, event);
onChangeCalendarDate === null || onChangeCalendarDate === void 0 || onChangeCalendarDate(nextValue, event);
});
/**
* A callback triggered when the time on the calendar changes.
*/
const handleChangeTime = useEventCallback(nextPageTime => {
setCalendarDate(nextPageTime);
handleDateChange(nextPageTime);
});
/**
* Close the calendar panel.
*/
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 updateValue = (event, date, closeOverlay = true) => {
const nextValue = typeof date !== 'undefined' ? date : calendarDate;
setCalendarDate(nextValue || startOfToday());
setValue(nextValue);
if (nextValue !== value) {
onChange === null || onChange === void 0 || onChange(nextValue, event);
}
// `closeOverlay` default value is `true`
if (closeOverlay !== false) {
handleClose();
}
};
/**
* The callback triggered after the date in the shortcut area is clicked.
*/
const handleShortcutPageDate = useEventCallback((range, closeOverlay, event) => {
const value = range.value;
updateValue(event, value, closeOverlay);
handleDateChange(value, event);
onShortcutClick === null || onShortcutClick === void 0 || onShortcutClick(range, event);
});
/**
* The callback triggered after clicking the OK button.
*/
const handleOK = useEventCallback(event => {
updateValue(event);
onOk === null || onOk === void 0 || onOk(calendarDate, event);
focusInput();
});
/**
* Callback after clicking the clear button.
*/
const handleClean = useEventCallback(event => {
event === null || event === void 0 || event.stopPropagation();
updateValue(event, null);
resetCalendarDate(null);
onClean === null || onClean === void 0 || onClean(event);
});
const handlePickerPopupKeyDown = useEventCallback(event => {
onKeyFocusEvent(event, {
date: calendarDate,
callback: setCalendarDate
});
if (event.key === 'Enter') {
handleOK(event);
}
});
const handleClick = useEventCallback(() => {
if (editable) {
return;
}
focusSelectedDate();
});
/**
* Callback after the date is selected.
*/
const handleCalendarSelect = useEventCallback((date, event, updatableValue = true) => {
const nextValue = copyTime({
from: calendarDate,
to: date
});
setCalendarDate(nextValue);
handleDateChange(nextValue);
if (oneTap && updatableValue) {
updateValue(event, nextValue);
focusInput();
}
});
/**
* A callback triggered when the date on the calendar changes.
*/
const handleChangeMonth = useEventCallback((nextPageDate, event) => {
setCalendarDate(nextPageDate);
handleDateChange(nextPageDate);
focusSelectedDate();
if (oneTap && mode === DateMode.Month) {
updateValue(event, nextPageDate);
focusInput();
}
});
/**
* Callback after the input box value is changed.
*/
const handleInputChange = useEventCallback((value, event) => {
if (!isErrorValue(value)) {
handleCalendarSelect(value, event);
}
updateValue(event, value, false);
});
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) {
if (isValid(calendarDate) && !isDateDisabled(calendarDate)) {
updateValue(event);
focusInput();
}
} else {
var _trigger$current3;
(_trigger$current3 = trigger.current) === null || _trigger$current3 === void 0 || _trigger$current3.open();
}
}
});
});
const calendarProps = mapValues(pick(props, calendarOnlyProps), func => (next, date) => {
var _func;
return (_func = func === null || func === void 0 ? void 0 : func(next, date)) !== null && _func !== void 0 ? _func : false;
});
const {
sideRanges,
bottomRanges
} = splitRanges(ranges);
const renderCalendarOverlay = (positionProps, speakerRef) => {
const {
className
} = positionProps;
const classes = merge(popupClassName, className, prefix('popup-date'));
return /*#__PURE__*/React.createElement(PickerPopup, {
role: "dialog",
"aria-labelledby": label ? `${id}-label` : undefined,
tabIndex: -1,
className: classes,
ref: mergeRefs(overlay, speakerRef),
style: popupStyle,
target: trigger,
onKeyDown: handlePickerPopupKeyDown
}, /*#__PURE__*/React.createElement(Stack, {
align: "flex-start",
h: "100%"
}, sideRanges && sideRanges.length > 0 && /*#__PURE__*/React.createElement(PredefinedRanges, {
direction: "column",
spacing: 0,
className: prefix('date-predefined'),
ranges: sideRanges,
calendarDate: calendarDate,
locale: locale,
disableShortcut: isDatetimeDisabled,
onShortcutClick: handleShortcutPageDate
}), /*#__PURE__*/React.createElement(Box, {
className: prefix('box')
}, /*#__PURE__*/React.createElement(CalendarContainer, _extends({}, calendarProps, {
targetId: id,
locale: locale,
showWeekNumbers: showWeekNumbers,
showMeridiem: showMeridiem,
disabledDate: isDateDisabled,
disabledHours: shouldDisableHour,
disabledMinutes: shouldDisableMinute,
disabledSeconds: shouldDisableSecond,
limitEndYear: limitEndYear,
limitStartYear: limitStartYear,
format: formatStr,
isoWeek: isoWeek,
weekStart: weekStart,
calendarDate: calendarDate,
monthDropdownProps: monthDropdownProps,
renderCellOnPicker: renderCell,
onMoveForward: handleMoveForward,
onMoveBackward: handleMoveBackward,
onSelect: handleCalendarSelect,
onToggleMonthDropdown: toggleMonthView,
onToggleTimeDropdown: onToggleTimeDropdown,
onChangeMonth: handleChangeMonth,
onChangeTime: handleChangeTime
})), /*#__PURE__*/React.createElement(Toolbar, {
locale: locale,
ranges: bottomRanges,
calendarDate: calendarDate,
disableOkBtn: isOkButtonDisabled,
disableShortcut: isDatetimeDisabled,
onShortcutClick: handleShortcutPageDate,
onOk: handleOK,
hideOkBtn: oneTap
}))));
};
const hasValue = isValid(value);
const caretAs = useMemo(() => {
if (caretAsProp === null) {
return null;
}
return caretAsProp || (mode === DateMode.Time ? TimeIcon : CalenderSimpleIcon);
}, [caretAsProp, mode]);
const handleTriggerClose = useEventCallback(cause => {
var _props$onClose;
// Unless overlay is closing on user clicking "OK" button,
// reset the selected date on calendar panel
if (cause !== OverlayCloseCause.ImperativeHandle) {
resetCalendarDate();
}
setMonthView(false);
(_props$onClose = props.onClose) === null || _props$onClose === void 0 || _props$onClose.call(props);
});
const showCleanButton = cleanable && hasValue && !readOnly;
const [ariaProps, rest] = partitionHTMLProps(restProps, {
htmlProps: [],
includeAria: true
});
const invalidValue = value && isErrorValue(value);
const customizedProps = {
value,
formatStr,
renderValue,
readOnly,
editable,
loading
};
const {
customValue,
inputReadOnly,
Input,
events
} = useCustomizedInput(customizedProps);
const triggerProps = {
...pick(props, triggerPropKeys),
onClose: handleTriggerClose,
onEnter: createChainedFunction(events.onActive, onEnter),
onExit: createChainedFunction(events.onInactive, onExit)
};
return /*#__PURE__*/React.createElement(PickerToggleTrigger, {
as: as,
pickerType: "date",
classPrefix: classPrefix,
className: merge(className, {
[prefix('error')]: invalidValue
}),
block: block,
disabled: disabled,
appearance: appearance,
style: style,
rootRef: root,
trigger: "active",
triggerProps: triggerProps,
ref: trigger,
placement: placement,
speaker: renderCalendarOverlay,
"data-cleanable": cleanable
}, plaintext ? /*#__PURE__*/React.createElement(DateInput, {
value: value,
format: formatStr,
plaintext: plaintext
}) : /*#__PURE__*/React.createElement(InputGroup, _extends({}, getRestProps(rest), {
inside: true,
size: size,
disabled: disabled,
className: prefix`input-group`,
onClick: handleClick
}), /*#__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,
format: formatStr,
placeholder: placeholder ? placeholder : formatStr,
disabled: disabled,
readOnly: inputReadOnly,
onChange: handleInputChange,
onKeyDown: handleInputKeyDown
})), /*#__PURE__*/React.createElement(PickerIndicator, {
size: size,
loading: loading,
caretAs: caretAs,
onClose: handleClean,
showCleanButton: showCleanButton
})));
});
DatePicker.displayName = 'DatePicker';
export default DatePicker;