diff --git a/.eslintrc.js b/.eslintrc.js
index f1b8be4f8..9b2ea05f3 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -10,6 +10,7 @@ module.exports = {
'react/sort-comp': 0,
'@typescript-eslint/no-explicit-any': 0,
'default-case': 0,
+ 'no-confusing-arrow': 0,
'jsx-a11y/no-autofocus': 0,
'import/no-extraneous-dependencies': [
'error',
diff --git a/assets/index.less b/assets/index.less
index 726862f77..c52685ff5 100644
--- a/assets/index.less
+++ b/assets/index.less
@@ -1,11 +1,13 @@
@prefix-cls: rc-picker;
+@background-color: rgb(255, 240, 255);
+
.@{prefix-cls} {
display: inline-flex;
&-panel {
border: 1px solid #666;
- background: rgb(255, 240, 255);
+ background: @background-color;
display: inline-block;
vertical-align: top;
@@ -33,6 +35,7 @@
table {
text-align: center;
+ border-collapse: collapse;
}
}
@@ -91,9 +94,36 @@
&-in-range > &-inner {
background: fade(blue, 5%);
}
+ &-range-hover-start,
+ &-range-hover-end,
+ &-range-hover {
+ position: relative;
+ &::after {
+ content: '';
+ position: absolute;
+ top: 3px;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ border: 1px solid green;
+ border-left: 0;
+ border-right: 0;
+ pointer-events: none;
+ }
+ }
+
+ &-range-hover-start::after {
+ border-left: 1px solid green!important;
+ }
+ &-range-hover-end::after {
+ border-right: 1px solid green!important;
+ }
+
&-today > &-inner {
border: 1px solid blue;
}
+ &-range-start > &-inner,
+ &-range-end > &-inner,
&-selected > &-inner {
background: fade(blue, 20%);
}
@@ -179,6 +209,7 @@
> li {
padding: 0;
margin: 0;
+ cursor: pointer;
.@{prefix-cls}-time-panel-cell-inner {
color: #333;
@@ -245,11 +276,51 @@
// ======================= Dropdown =======================
&-dropdown {
position: absolute;
- box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
+ box-shadow: 0 0 1px red;
&-hidden {
display: none;
}
+
+ // Panel
+ @arrow-size: 10px;
+ .@{prefix-cls}-range-arrow {
+ position: absolute;
+ width: @arrow-size;
+ height: @arrow-size;
+ z-index: 1;
+ transform: rotate(-45deg);
+ top: @arrow-size / 2 + 1px;
+ left: @arrow-size;
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ box-sizing: border-box;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ &::before {
+ width: @arrow-size;
+ height: @arrow-size;
+ border: @arrow-size / 2 solid blue;
+ border-color: blue blue transparent transparent;
+ }
+ &::after {
+ width: @arrow-size - 2px;
+ height: @arrow-size - 2px;
+ border: (@arrow-size - 2px) / 2 solid blue;
+ border-color: @background-color @background-color transparent
+ transparent;
+ }
+ }
+
+ .@{prefix-cls}-panel {
+ margin: 10px 0;
+ }
}
// ========================================================
@@ -257,5 +328,6 @@
// ========================================================
&-range {
display: inline-flex;
+ position: relative;
}
}
diff --git a/examples/common.less b/examples/common.less
new file mode 100644
index 000000000..c84387c88
--- /dev/null
+++ b/examples/common.less
@@ -0,0 +1,7 @@
+h1,
+h2,
+h3,
+h4 {
+ margin: 0;
+ line-height: 200%;
+}
diff --git a/examples/range.tsx b/examples/range.tsx
index 0c7d08c2f..8b73d1019 100644
--- a/examples/range.tsx
+++ b/examples/range.tsx
@@ -4,6 +4,7 @@ import RangePicker from '../src/RangePicker';
import momentGenerateConfig from '../src/generate/moment';
import zhCN from '../src/locale/zh_CN';
import '../assets/index.less';
+import './common.less';
const defaultStartValue = moment('2019-09-03 05:02:03');
const defaultEndValue = moment('2019-11-28 01:02:03');
@@ -43,12 +44,30 @@ export default () => {
return (
-
+
Value:{' '}
{value ? `${formatDate(value[0])} ~ ${formatDate(value[1])}` : 'null'}
-
+
+
+
Basic
+
+ {...sharedProps}
+ value={undefined}
+ locale={zhCN}
+ allowClear
+ ref={rangePickerRef}
+ />
+
+ {...sharedProps}
+ locale={zhCN}
+ allowClear
+ ref={rangePickerRef}
+ showTime
+ />
+
+
Basic
@@ -56,6 +75,7 @@ export default () => {
locale={zhCN}
allowClear
ref={rangePickerRef}
+ // style={{ width: 500 }}
/>
+
+
+
Start disabled
+
+ {...sharedProps}
+ locale={zhCN}
+ allowClear
+ disabled={[true, false]}
+ />
+
+
+
End disabled
+
+ {...sharedProps}
+ locale={zhCN}
+ allowClear
+ disabled={[false, true]}
/>
diff --git a/src/PanelContext.tsx b/src/PanelContext.tsx
index 1c8e67548..796bcdfdb 100644
--- a/src/PanelContext.tsx
+++ b/src/PanelContext.tsx
@@ -10,6 +10,10 @@ export interface PanelContextProps {
/** Only work with time panel */
hideHeader?: boolean;
panelRef?: React.Ref
;
+ hidePrevBtn?: boolean;
+ hideNextBtn?: boolean;
+ onDateMouseEnter?: (date: any) => void;
+ onDateMouseLeave?: (date: any) => void;
}
const PanelContext = React.createContext({});
diff --git a/src/Picker.tsx b/src/Picker.tsx
index a5f49cfcd..6ec53d179 100644
--- a/src/Picker.tsx
+++ b/src/Picker.tsx
@@ -12,7 +12,6 @@
*/
import * as React from 'react';
-import KeyCode from 'rc-util/lib/KeyCode';
import classNames from 'classnames';
import { AlignType } from 'rc-trigger/lib/interface';
import PickerPanel, {
@@ -25,16 +24,15 @@ import { isEqual } from './utils/dateUtil';
import getDataOrAriaProps, { toArray } from './utils/miscUtil';
import PanelContext, { ContextOperationRefProps } from './PanelContext';
import { PickerMode } from './interface';
-import {
- getDefaultFormat,
- getInputSize,
- addGlobalMouseDownEvent,
-} from './utils/uiUtil';
+import { getDefaultFormat, getInputSize } from './utils/uiUtil';
+import usePickerInput from './hooks/usePickerInput';
+import useTextValueMapping from './hooks/useTextValueMapping';
+import useMergedState from './hooks/useMergeState';
+import useValueTexts from './hooks/useValueTexts';
export interface PickerRefConfig {
focus: () => void;
blur: () => void;
- open: () => void;
}
export interface PickerSharedProps extends React.AriaAttributes {
@@ -47,6 +45,7 @@ export interface PickerSharedProps extends React.AriaAttributes {
autoFocus?: boolean;
disabled?: boolean;
open?: boolean;
+ defaultOpen?: boolean;
/** Make input readOnly to avoid popup keyboard in mobile */
inputReadOnly?: boolean;
@@ -83,20 +82,22 @@ export interface PickerSharedProps extends React.AriaAttributes {
name?: string;
}
+type OmitPanelProps = Omit<
+ Props,
+ 'onChange' | 'hideHeader' | 'pickerValue' | 'onPickerValueChange'
+>;
+
export interface PickerBaseProps
extends PickerSharedProps,
- Omit, 'onChange' | 'hideHeader'> {}
+ OmitPanelProps> {}
export interface PickerDateProps
extends PickerSharedProps,
- Omit, 'onChange' | 'hideHeader'> {}
+ OmitPanelProps> {}
export interface PickerTimeProps
extends PickerSharedProps,
- Omit<
- PickerPanelTimeProps,
- 'onChange' | 'format' | 'hideHeader'
- > {}
+ Omit>, 'format'> {}
export type PickerProps =
| PickerBaseProps
@@ -134,6 +135,7 @@ function InnerPicker(props: PickerProps) {
value,
defaultValue,
open,
+ defaultOpen,
suffixIcon,
clearIcon,
disabled,
@@ -165,61 +167,29 @@ function InnerPicker(props: PickerProps) {
const inputDivRef = React.useRef(null);
// Real value
- const [innerValue, setInnerValue] = React.useState(() => {
- if (value !== undefined) {
- return value;
- }
- if (defaultValue !== undefined) {
- return defaultValue;
- }
- return null;
+ const [mergedValue, setInnerValue] = useMergedState({
+ value,
+ defaultValue,
+ defaultStateValue: null,
});
- const mergedValue = value !== undefined ? value : innerValue;
// Selected value
- const [
- selectedValue,
- setInternalSelectedValue,
- ] = React.useState(mergedValue);
-
- // Text
- const [textValue, setTextValue] = React.useState(
- selectedValue
- ? generateConfig.locale.format(
- locale.locale,
- selectedValue,
- formatList[0],
- )
- : '',
+ const [selectedValue, setSelectedValue] = React.useState(
+ mergedValue,
);
- const [typing, setTyping] = React.useState(false);
-
- /** Similar as `setTextValue` but accept `DateType` and convert into string */
- const setDateText = (date: DateType | null) => {
- setTextValue(
- date === null
- ? ''
- : generateConfig.locale.format(locale.locale, date, formatList[0]),
- );
- };
// Operation ref
const operationRef: React.MutableRefObject = React.useRef<
ContextOperationRefProps
>(null);
- // Trigger
- const [innerOpen, setInnerOpen] = React.useState(false);
- let mergedOpen: boolean;
- if (disabled) {
- mergedOpen = false;
- } else {
- mergedOpen = typeof open === 'boolean' ? open : innerOpen;
- }
-
- const triggerOpen = (newOpen: boolean) => {
- if (mergedOpen !== newOpen) {
- setInnerOpen(newOpen);
+ // Open
+ const [mergedOpen, triggerInnerOpen] = useMergedState({
+ value: open,
+ defaultValue: defaultOpen,
+ defaultStateValue: false,
+ postState: postOpen => (disabled ? false : postOpen),
+ onChange: newOpen => {
if (onOpenChange) {
onOpenChange(newOpen);
}
@@ -227,52 +197,8 @@ function InnerPicker(props: PickerProps) {
if (!newOpen && operationRef.current && operationRef.current.onClose) {
operationRef.current.onClose();
}
- }
- };
-
- // Focus
- const [focused, setFocused] = React.useState(false);
-
- // ============================= Value =============================
- const isSameTextDate = (text: string, date: DateType | null) => {
- if (date === null) {
- return !text;
- }
-
- const inputDate = generateConfig.locale.parse(
- locale.locale,
- text,
- formatList,
- );
- return isEqual(generateConfig, inputDate, date);
- };
-
- // =========================== Formatter ===========================
- const setSelectedValue = (newDate: DateType | null) => {
- if (!isSameTextDate(textValue, newDate)) {
- setDateText(newDate);
- }
- setInternalSelectedValue(newDate);
- };
-
- const onInputMouseDown: React.MouseEventHandler = () => {
- triggerOpen(true);
- setTyping(true);
- };
-
- const onInputChange: React.ChangeEventHandler = e => {
- const text = e.target.value;
- setTextValue(text);
-
- const inputDate = generateConfig.locale.parse(
- locale.locale,
- text,
- formatList,
- );
- if (inputDate && (!disabledDate || !disabledDate(inputDate))) {
- setSelectedValue(inputDate);
- }
- };
+ },
+ });
// ============================ Trigger ============================
const triggerChange = (newValue: DateType | null) => {
@@ -289,141 +215,86 @@ function InnerPicker(props: PickerProps) {
}
};
+ const triggerOpen = (
+ newOpen: boolean,
+ preventChangeEvent: boolean = false,
+ ) => {
+ triggerInnerOpen(newOpen);
+ if (!newOpen && !preventChangeEvent) {
+ triggerChange(selectedValue);
+ }
+ };
+
const forwardKeyDown = (e: React.KeyboardEvent) => {
- if (
- !typing &&
- mergedOpen &&
- operationRef.current &&
- operationRef.current.onKeyDown
- ) {
+ if (mergedOpen && operationRef.current && operationRef.current.onKeyDown) {
// Let popup panel handle keyboard
return operationRef.current.onKeyDown(e);
}
return false;
};
- const onInputKeyDown: React.KeyboardEventHandler = e => {
- switch (e.which) {
- case KeyCode.ENTER: {
- if (!mergedOpen) {
- triggerOpen(true);
- } else {
- triggerChange(selectedValue);
- triggerOpen(false);
- setTyping(true);
- }
- return;
- }
-
- case KeyCode.TAB: {
- if (typing && mergedOpen && !e.shiftKey) {
- setTyping(false);
- e.preventDefault();
- } else if (!typing && mergedOpen) {
- if (!forwardKeyDown(e) && e.shiftKey) {
- setTyping(true);
- e.preventDefault();
- }
- }
- return;
- }
+ // ============================= Text ==============================
+ const valueTexts = useValueTexts(selectedValue, {
+ formatList,
+ generateConfig,
+ locale,
+ });
- case KeyCode.ESC: {
- triggerChange(mergedValue);
- setSelectedValue(mergedValue);
- triggerOpen(false);
- setTyping(true);
- return;
+ const [text, triggerTextChange, resetText] = useTextValueMapping({
+ valueTexts,
+ onTextChange: newText => {
+ const inputDate = generateConfig.locale.parse(
+ locale.locale,
+ newText,
+ formatList,
+ );
+ if (inputDate && (!disabledDate || !disabledDate(inputDate))) {
+ setSelectedValue(inputDate);
}
- }
-
- if (!mergedOpen && ![KeyCode.SHIFT].includes(e.which)) {
- triggerOpen(true);
- } else {
- // Let popup panel handle keyboard
- forwardKeyDown(e);
- }
- };
-
- const onInputFocus: React.FocusEventHandler = e => {
- setTyping(true);
- setFocused(true);
-
- if (onFocus) {
- onFocus(e);
- }
- };
-
- /**
- * We will prevent blur to handle open event when user click outside,
- * since this will repeat trigger `onOpenChange` event.
- */
- const preventBlurRef = React.useRef(false);
-
- const triggerClose = () => {
- triggerOpen(false);
- setInnerValue(selectedValue);
- triggerChange(selectedValue);
- };
-
- const onInputBlur: React.FocusEventHandler = e => {
- if (preventBlurRef.current) {
- preventBlurRef.current = false;
- return;
- }
-
- triggerClose();
- setFocused(false);
+ },
+ });
- if (onBlur) {
- onBlur(e);
- }
- };
+ // ============================= Input =============================
+ const [inputProps, { focused, typing }] = usePickerInput({
+ open: mergedOpen,
+ triggerOpen,
+ forwardKeyDown,
+ isClickOutside: target =>
+ !!(
+ panelDivRef.current &&
+ !panelDivRef.current.contains(target as Node) &&
+ inputDivRef.current &&
+ !inputDivRef.current.contains(target as Node) &&
+ onOpenChange
+ ),
+ onSubmit: () => {
+ triggerChange(selectedValue);
+ triggerOpen(false, true);
+ resetText();
+ },
+ onCancel: () => {
+ triggerOpen(false, true);
+ setSelectedValue(mergedValue);
+ resetText();
+ },
+ onFocus,
+ onBlur,
+ });
// ============================= Sync ==============================
// Close should sync back with text value
React.useEffect(() => {
- if (!mergedOpen && !isSameTextDate(textValue, mergedValue)) {
- setDateText(mergedValue);
+ if (!mergedOpen) {
+ setSelectedValue(mergedValue);
}
}, [mergedOpen]);
// Sync innerValue with control mode
React.useEffect(() => {
- if (!isEqual(generateConfig, mergedValue, innerValue)) {
- // Sync inner & select value
- setInnerValue(mergedValue);
- setSelectedValue(mergedValue);
- }
-
- // Sync text
- if (!isSameTextDate(textValue, mergedValue)) {
- setDateText(mergedValue);
- }
+ // Sync select value
+ setSelectedValue(mergedValue);
}, [mergedValue]);
- // Global click handler
- React.useEffect(() =>
- addGlobalMouseDownEvent(({ target }: MouseEvent) => {
- if (
- mergedOpen &&
- panelDivRef.current &&
- !panelDivRef.current.contains(target as Node) &&
- inputDivRef.current &&
- !inputDivRef.current.contains(target as Node) &&
- onOpenChange
- ) {
- preventBlurRef.current = true;
- triggerClose();
-
- // Always set back in case `onBlur` prevented by user
- window.setTimeout(() => {
- preventBlurRef.current = false;
- }, 0);
- }
- }),
- );
-
// ============================ Private ============================
if (pickerRef) {
pickerRef.current = {
@@ -437,9 +308,6 @@ function InnerPicker(props: PickerProps) {
inputRef.current.blur();
}
},
- open: () => {
- triggerOpen(true);
- },
};
}
@@ -449,6 +317,8 @@ function InnerPicker(props: PickerProps) {
...(props as Omit, 'picker' | 'format'>),
className: undefined,
style: undefined,
+ pickerValue: undefined,
+ onPickerValueChange: undefined,
};
const panel = (
@@ -523,15 +393,12 @@ function InnerPicker(props: PickerProps) {
@@ -560,12 +427,6 @@ class Picker extends React.Component> {
}
};
- open = () => {
- if (this.pickerRef.current) {
- this.pickerRef.current.open();
- }
- };
-
render() {
return (
diff --git a/src/PickerPanel.tsx b/src/PickerPanel.tsx
index 38319dce6..c1dd70cec 100644
--- a/src/PickerPanel.tsx
+++ b/src/PickerPanel.tsx
@@ -1,5 +1,12 @@
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
+/**
+ * Logic:
+ * When `mode` === `picker`,
+ * click will trigger `onSelect` (if value changed trigger `onChange` also).
+ * Panel change will not trigger `onSelect` but trigger `onPanelChange`
+ */
+
import * as React from 'react';
import classNames from 'classnames';
import KeyCode from 'rc-util/lib/KeyCode';
@@ -26,6 +33,7 @@ import { DateRender } from './panels/DatePanel/DateBody';
import { PickerModeMap } from './utils/uiUtil';
import { MonthCellRender } from './panels/MonthPanel/MonthBody';
import RangeContext, { FooterSelection } from './RangeContext';
+import useMergedState from './hooks/useMergeState';
export interface PickerPanelSharedProps {
prefixCls?: string;
@@ -43,6 +51,8 @@ export interface PickerPanelSharedProps {
value?: DateType | null;
defaultValue?: DateType;
/** [Legacy] Set default display picker view date */
+ pickerValue?: DateType;
+ /** [Legacy] Set default display picker view date */
defaultPickerValue?: DateType;
// Date
@@ -61,6 +71,8 @@ export interface PickerPanelSharedProps {
/** @private This is internal usage. Do not use in your production env */
hideHeader?: boolean;
+ /** @private This is internal usage. Do not use in your production env */
+ onPickerValueChange?: (date: DateType) => void;
}
export interface PickerPanelBaseProps
@@ -108,6 +120,7 @@ function PickerPanel(props: PickerPanelProps) {
generateConfig,
value,
defaultValue,
+ pickerValue,
defaultPickerValue,
mode,
picker = 'date',
@@ -120,6 +133,7 @@ function PickerPanel(props: PickerPanelProps) {
onChange,
onPanelChange,
onMouseDown,
+ onPickerValueChange,
} = props as MergedPickerPanelProps;
if (process.env.NODE_ENV !== 'production') {
@@ -138,7 +152,9 @@ function PickerPanel(props: PickerPanelProps) {
const panelContext = React.useContext(PanelContext);
const { operationRef, panelRef: panelDivRef } = panelContext;
- const { extraFooterSelections, inRange } = React.useContext(RangeContext);
+ const { extraFooterSelections, inRange, panelPosition } = React.useContext(
+ RangeContext,
+ );
const panelRef = React.useRef({});
// Handle init logic
@@ -158,9 +174,22 @@ function PickerPanel(props: PickerPanelProps) {
const mergedValue = value !== undefined ? value : innerValue;
// View date control
- const [viewDate, setViewDate] = React.useState(
- () => defaultPickerValue || mergedValue || generateConfig.getNow(),
- );
+ const [viewDate, setInnerViewDate] = useMergedState<
+ DateType | null,
+ DateType
+ >({
+ value: pickerValue,
+ defaultValue: defaultPickerValue || mergedValue,
+ defaultStateValue: null,
+ postState: date => date || generateConfig.getNow(),
+ });
+
+ const setViewDate = (date: DateType) => {
+ setInnerViewDate(date);
+ if (onPickerValueChange) {
+ onPickerValueChange(date);
+ }
+ };
// Panel control
const getInternalNextMode = (nextMode: PanelMode): PanelMode => {
@@ -169,9 +198,6 @@ function PickerPanel(props: PickerPanelProps) {
return getNextMode(nextMode);
}
- if (nextMode === 'date' && showTime) {
- return 'datetime';
- }
return nextMode;
};
@@ -181,8 +207,17 @@ function PickerPanel(props: PickerPanelProps) {
}
return getInternalNextMode('date');
});
+
const mergedMode: PanelMode = mode || innerMode;
+ // const mergedMode: PanelMode = React.useMemo(() => {
+ // const newMode = mode || innerMode;
+ // if (newMode === 'date' && showTime) {
+ // return 'datetime';
+ // }
+ // return newMode;
+ // }, [mode || innerMode]);
+
const onInternalPanelChange = (newMode: PanelMode, viewValue: DateType) => {
const nextMode = getInternalNextMode(newMode);
setInnerMode(nextMode);
@@ -192,15 +227,20 @@ function PickerPanel(props: PickerPanelProps) {
}
};
- const triggerSelect = (date: DateType) => {
- setInnerValue(date);
+ const triggerSelect = (
+ date: DateType,
+ forceTriggerSelect: boolean = false,
+ ) => {
+ if (mergedMode === picker || forceTriggerSelect) {
+ setInnerValue(date);
- if (onSelect) {
- onSelect(date);
- }
+ if (onSelect) {
+ onSelect(date);
+ }
- if (onChange && !isEqual(generateConfig, date, mergedValue)) {
- onChange(date);
+ if (onChange && !isEqual(generateConfig, date, mergedValue)) {
+ onChange(date);
+ }
}
};
@@ -255,7 +295,7 @@ function PickerPanel(props: PickerPanelProps) {
// ============================ Effect ============================
React.useEffect(() => {
if (value && !initRef.current) {
- setViewDate(value);
+ setInnerViewDate(value);
}
}, [value]);
@@ -327,18 +367,6 @@ function PickerPanel(props: PickerPanelProps) {
);
break;
- case 'datetime':
- panelNode = (
- {
- setViewDate(date);
- triggerSelect(date);
- }}
- />
- );
- break;
-
case 'time':
delete pickerProps.showTime;
panelNode = (
@@ -354,15 +382,27 @@ function PickerPanel(props: PickerPanelProps) {
break;
default:
- panelNode = (
-
- {...pickerProps}
- onSelect={date => {
- setViewDate(date);
- triggerSelect(date);
- }}
- />
- );
+ if (showTime) {
+ panelNode = (
+ {
+ setViewDate(date);
+ triggerSelect(date);
+ }}
+ />
+ );
+ } else {
+ panelNode = (
+
+ {...pickerProps}
+ onSelect={date => {
+ setViewDate(date);
+ triggerSelect(date);
+ }}
+ />
+ );
+ }
}
// ============================ Footer ============================
@@ -381,7 +421,7 @@ function PickerPanel(props: PickerPanelProps) {
{
- triggerSelect(generateConfig.getNow());
+ triggerSelect(generateConfig.getNow(), true);
}}
>
{locale.today}
@@ -398,7 +438,7 @@ function PickerPanel(props: PickerPanelProps) {
mergedSelections.push({
label: locale.now,
onClick: () => {
- triggerSelect(generateConfig.getNow());
+ triggerSelect(generateConfig.getNow(), true);
},
});
}
@@ -421,6 +461,8 @@ function PickerPanel(props: PickerPanelProps) {
...panelContext,
hideHeader:
'hideHeader' in props ? hideHeader : panelContext.hideHeader,
+ hidePrevBtn: inRange && panelPosition === 'right',
+ hideNextBtn: inRange && panelPosition === 'left',
}}
>
, NullableDateType
];
+ hoverRangedValue?: RangeValue;
inRange?: boolean;
+ panelPosition?: 'left' | 'right' | false;
}
const RangeContext = React.createContext({});
diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx
index 8ed74f1b2..8a98365de 100644
--- a/src/RangePicker.tsx
+++ b/src/RangePicker.tsx
@@ -1,41 +1,79 @@
-/**
- * TODO:
- * - Highlight range when hover the ranges value
- * - Click ranges value will go to the related panel
- */
-
import * as React from 'react';
import classNames from 'classnames';
-import Picker, {
+import {
+ DisabledTimes,
+ PanelMode,
+ PickerMode,
+ RangeValue,
+ EventValue,
+} from './interface';
+import {
PickerBaseProps,
PickerDateProps,
PickerTimeProps,
PickerRefConfig,
} from './Picker';
+import { SharedTimeProps } from './panels/TimePanel';
+import useMergedState from './hooks/useMergeState';
+import PickerTrigger from './PickerTrigger';
+import PickerPanel from './PickerPanel';
+import usePickerInput from './hooks/usePickerInput';
+import getDataOrAriaProps, {
+ toArray,
+ getValue,
+ updateValues,
+} from './utils/miscUtil';
+import { getDefaultFormat, getInputSize } from './utils/uiUtil';
+import PanelContext, { ContextOperationRefProps } from './PanelContext';
import {
- NullableDateType,
- DisabledTimes,
- DisabledTime,
- PickerMode,
- PanelMode,
- OnPanelChange,
-} from './interface';
-import { toArray } from './utils/miscUtil';
+ isEqual,
+ getClosingViewDate,
+ isSameMonth,
+ isSameYear,
+} from './utils/dateUtil';
+import useValueTexts from './hooks/useValueTexts';
+import useTextValueMapping from './hooks/useTextValueMapping';
+import { GenerateConfig } from './generate';
+import { PickerPanelProps } from '.';
import RangeContext from './RangeContext';
-import { isSameDate } from './utils/dateUtil';
-import { getDefaultFormat } from './utils/uiUtil';
-import { SharedTimeProps } from './panels/TimePanel';
+import useRangeDisabled from './hooks/useRangeDisabled';
+
+function reorderValues(
+ values: RangeValue,
+ generateConfig: GenerateConfig,
+): RangeValue {
+ if (
+ values &&
+ values[0] &&
+ values[1] &&
+ generateConfig.isAfter(values[0], values[1])
+ ) {
+ return [values[1], values[0]];
+ }
-type EventValue = DateType | null;
-type RangeValue = [EventValue, EventValue] | null;
+ return values;
+}
-function canTriggerChange(
- dates: [EventValue, EventValue],
- allowEmpty?: [boolean, boolean],
+function canValueTrigger(
+ value: EventValue,
+ index: number,
+ disabledList: [boolean, boolean],
+ allowEmpty?: [boolean, boolean] | null,
): boolean {
- const passStart = dates[0] || (allowEmpty && allowEmpty[0]);
- const passEnd = dates[1] || (allowEmpty && allowEmpty[1]);
- return !!(passStart && passEnd);
+ if (value) {
+ return true;
+ }
+
+ if (allowEmpty && allowEmpty[index]) {
+ return true;
+ }
+
+ // If another one is disabled, this can be trigger
+ if (disabledList[(index + 1) % 2]) {
+ return true;
+ }
+
+ return false;
}
export interface RangePickerSharedProps {
@@ -43,6 +81,7 @@ export interface RangePickerSharedProps {
defaultValue?: RangeValue;
defaultPickerValue?: [DateType, DateType];
placeholder?: [string, string];
+ disabled?: boolean | [boolean, boolean];
disabledTime?: (
date: EventValue,
type: 'start' | 'end',
@@ -54,7 +93,6 @@ export interface RangePickerSharedProps {
>;
separator?: React.ReactNode;
allowEmpty?: [boolean, boolean];
- selectable?: [boolean, boolean];
mode?: [PanelMode, PanelMode];
onChange?: (
values: RangeValue,
@@ -78,6 +116,7 @@ type OmitPickerProps = Omit<
| 'defaultValue'
| 'defaultPickerValue'
| 'placeholder'
+ | 'disabled'
| 'disabledTime'
| 'showToday'
| 'showTime'
@@ -85,6 +124,8 @@ type OmitPickerProps = Omit<
| 'onChange'
| 'onSelect'
| 'onPanelChange'
+ | 'pickerValue'
+ | 'onPickerValueChange'
>;
export interface RangePickerBaseProps
@@ -120,365 +161,659 @@ interface MergedRangePickerProps
picker?: PickerMode;
}
-function InternalRangePicker(
- props: RangePickerProps & {
- pickerRef: React.Ref;
- },
-) {
+function InnerRangePicker(props: RangePickerProps) {
const {
prefixCls = 'rc-picker',
- className,
style,
- value,
- defaultValue,
- defaultPickerValue,
- separator = '~',
- mode,
- picker,
- pickerRef,
- locale,
+ className,
+ popupStyle,
+ dropdownClassName,
+ transitionName,
+ dropdownAlign,
+ getPopupContainer,
generateConfig,
+ locale,
placeholder,
+ autoFocus,
+ disabled,
+ format,
+ picker = 'date',
showTime,
use12Hours,
- disabledTime,
- ranges,
- format,
+ separator = '~',
+ value,
+ defaultValue,
+ defaultPickerValue,
+ open,
+ defaultOpen,
+ disabledDate,
allowEmpty,
- selectable,
- disabled,
+ allowClear,
+ suffixIcon,
+ clearIcon,
+ pickerRef,
+ inputReadOnly,
+ mode,
onChange,
- onCalendarChange,
+ onOpenChange,
onPanelChange,
onFocus,
onBlur,
- } = props as MergedRangePickerProps & {
- pickerRef: React.MutableRefObject;
- };
+ } = props as MergedRangePickerProps;
+
+ const containerRef = React.useRef(null);
+ const panelDivRef = React.useRef(null);
+ const startInputDivRef = React.useRef(null);
+ const endInputDivRef = React.useRef(null);
+ const startInputRef = React.useRef(null);
+ const endInputRef = React.useRef(null);
+ // ============================= Misc ==============================
const formatList = toArray(
getDefaultFormat(format, picker, showTime, use12Hours),
);
- const [startShowTime, endShowTime] = React.useMemo(() => {
- if (showTime && typeof showTime === 'object' && showTime.defaultValue) {
- return [
- { ...showTime, defaultValue: showTime.defaultValue[0] },
- { ...showTime, defaultValue: showTime.defaultValue[1] },
- ];
+ // Active picker
+ const [activePickerIndex, setActivePickerIndex] = React.useState<0 | 1>(0);
+
+ // Operation ref
+ const operationRef: React.MutableRefObject = React.useRef<
+ ContextOperationRefProps
+ >(null);
+
+ const mergedDisabled = React.useMemo<[boolean, boolean]>(() => {
+ if (Array.isArray(disabled)) {
+ return disabled;
}
- return [showTime, showTime];
- }, [showTime]);
-
- const mergedSelectable = React.useMemo<
- [boolean | undefined, boolean | undefined]
- >(() => [selectable && selectable[0], selectable && selectable[1]], [
- selectable,
- ]);
-
- // ============================= Values =============================
- const [innerValue, setInnerValue] = React.useState>(
- () => {
- if (value !== undefined) {
- return value;
- }
- if (defaultValue !== undefined) {
- return defaultValue;
- }
- return null;
- },
- );
- const mergedValue = value !== undefined ? value : innerValue;
+ return [disabled || false, disabled || false];
+ }, [disabled]);
- // Get picker value, should order this internally
- const [value1, value2] = React.useMemo(() => {
- let val1 = mergedValue ? mergedValue[0] : null;
- let val2 = mergedValue ? mergedValue[1] : null;
+ // ============================= Value =============================
+ const [mergedValue, setInnerValue] = useMergedState>({
+ value,
+ defaultValue,
+ defaultStateValue: null,
+ postState: values => reorderValues(values, generateConfig),
+ });
- // Exchange
- if (val1 && val2 && generateConfig.isAfter(val1, val2)) {
- const tmp = val1;
- val1 = val2;
- val2 = tmp;
+ // =========================== View Date ===========================
+ /**
+ * End view date is use right panel by default.
+ * But when they in same month (date picker) or year (month picker), will both use left panel.
+ */
+ function getEndViewDate(viewDate: DateType, values: RangeValue) {
+ let compareFunc: (
+ generateConfig: GenerateConfig,
+ date1: DateType | null,
+ date2: DateType | null,
+ ) => boolean = isSameMonth;
+
+ if (picker === 'month') {
+ compareFunc = isSameYear;
}
- return [val1, val2];
- }, [mergedValue]);
+ if (compareFunc(generateConfig, getValue(values, 0), getValue(values, 1))) {
+ return viewDate;
+ }
+ return getClosingViewDate(viewDate, picker, generateConfig, -1);
+ }
- // Select value: used for click to update ranged value. Must set in pair
- const [selectedValues, setSelectedValues] = React.useState<
- [DateType | null, DateType | null] | undefined
- >(undefined);
+ // Config view panel
+ const [viewDates, setViewDates] = useMergedState<
+ RangeValue,
+ [DateType, DateType]
+ >({
+ defaultValue: () =>
+ defaultPickerValue ||
+ updateValues(
+ mergedValue,
+ (viewDate: DateType) => getEndViewDate(viewDate, mergedValue),
+ 1,
+ ),
+ defaultStateValue: null,
+ postState: postViewDates =>
+ postViewDates || [
+ getValue(mergedValue, 0) || generateConfig.getNow(),
+ getValue(mergedValue, 0) || generateConfig.getNow(),
+ ],
+ });
- React.useEffect(() => {
- setSelectedValues([value1, value2]);
- }, [value1, value2]);
+ // ========================= Select Values =========================
+ const [selectedValue, setSelectedValue] = useMergedState({
+ defaultStateValue: mergedValue,
+ postState: values => {
+ let postValues = values;
+ for (let i = 0; i < 2; i += 1) {
+ if (mergedDisabled[i] && !getValue(postValues, i)) {
+ postValues = updateValues(postValues, generateConfig.getNow(), i);
+ }
+ }
+ return postValues;
+ },
+ });
- const onStartSelect = (date: DateType) => {
- setSelectedValues([date, value2]);
- };
+ // ========================== Hover Range ==========================
+ const [hoverRangedValue, setHoverRangedValue] = React.useState<
+ RangeValue
+ >(null);
- const onEndSelect = (date: DateType) => {
- setSelectedValues([value1, date]);
+ const onDateMouseEnter = (date: DateType) => {
+ setHoverRangedValue(updateValues(selectedValue, date, activePickerIndex));
};
-
- // ============================= Change =============================
- const formatDate = (date: NullableDateType) => {
- if (date) {
- return generateConfig.locale.format(locale.locale, date, formatList[0]);
- }
- return '';
+ const onDateMouseLeave = () => {
+ setHoverRangedValue(updateValues(selectedValue, null, activePickerIndex));
};
- const onInternalChange = (
- values: NullableDateType[],
- changedByStartTime: boolean,
+ // ============================= Modes =============================
+ const [mergedModes, setInnerModes] = useMergedState<[PanelMode, PanelMode]>({
+ value: mode,
+ defaultStateValue: [picker, picker],
+ });
+
+ const triggerModesChange = (
+ modes: [PanelMode, PanelMode],
+ values: RangeValue,
) => {
- const startDate: DateType | null = values[0] || null;
- let endDate: DateType | null = values[1] || null;
-
- // If user change start time is after end time, should reset end time to null
- if (
- startDate &&
- endDate &&
- !isSameDate(generateConfig, startDate, endDate) &&
- generateConfig.isAfter(startDate, endDate) &&
- changedByStartTime
- ) {
- endDate = null;
+ setInnerModes(modes);
+
+ if (onPanelChange) {
+ onPanelChange(values, modes);
}
+ };
- setInnerValue([startDate, endDate]);
+ // ========================= Disable Date ==========================
+ const [disabledStartDate, disabledEndDate] = useRangeDisabled({
+ selectedValue,
+ disabled: mergedDisabled,
+ disabledDate,
+ generateConfig,
+ });
+
+ // ============================= Open ==============================
+ const [mergedOpen, triggerInnerOpen] = useMergedState({
+ value: open,
+ defaultValue: defaultOpen,
+ defaultStateValue: false,
+ postState: postOpen =>
+ mergedDisabled[activePickerIndex] ? false : postOpen,
+ onChange: newOpen => {
+ if (onOpenChange) {
+ onOpenChange(newOpen);
+ }
+
+ if (!newOpen && operationRef.current && operationRef.current.onClose) {
+ operationRef.current.onClose();
+ }
+ },
+ });
- const startStr = formatDate(startDate);
- const endStr = formatDate(endDate);
+ const startOpen = mergedOpen && activePickerIndex === 0;
+ const endOpen = mergedOpen && activePickerIndex === 1;
- if (onChange && canTriggerChange([startDate, endDate], allowEmpty)) {
- onChange([startDate, endDate], [startStr, endStr]);
+ // ============================= Popup =============================
+ // Popup min width
+ const [popupMinWidth, setPopupMinWidth] = React.useState(0);
+ React.useEffect(() => {
+ if (!mergedOpen && containerRef.current) {
+ setPopupMinWidth(containerRef.current.offsetWidth);
}
+ }, [mergedOpen]);
+
+ // ============================ Trigger ============================
+ let triggerOpen: (
+ newOpen: boolean,
+ index: 0 | 1,
+ preventChangeEvent?: boolean,
+ ) => void;
+
+ const triggerChange = (
+ newValue: RangeValue,
+ forceInput: boolean = true,
+ ) => {
+ const values = reorderValues(newValue, generateConfig);
- if (onCalendarChange) {
- onCalendarChange([startDate, endDate], [startStr, endStr]);
+ setSelectedValue(values);
+
+ const startValue = getValue(values, 0);
+ const endValue = getValue(values, 1);
+
+ const canStartValueTrigger = canValueTrigger(
+ startValue,
+ 0,
+ mergedDisabled,
+ allowEmpty,
+ );
+ const canEndValueTrigger = canValueTrigger(
+ endValue,
+ 1,
+ mergedDisabled,
+ allowEmpty,
+ );
+
+ const canTrigger =
+ values === null || (canStartValueTrigger && canEndValueTrigger);
+
+ if (canTrigger) {
+ // Trigger onChange only when value is validate
+ setInnerValue(values);
+ triggerOpen(false, activePickerIndex, true);
+
+ if (
+ onChange &&
+ (!isEqual(generateConfig, getValue(mergedValue, 0), startValue) ||
+ !isEqual(generateConfig, getValue(mergedValue, 1), endValue))
+ ) {
+ onChange(values, [
+ startValue
+ ? generateConfig.locale.format(
+ locale.locale,
+ startValue,
+ formatList[0],
+ )
+ : '',
+ endValue
+ ? generateConfig.locale.format(
+ locale.locale,
+ endValue,
+ formatList[0],
+ )
+ : '',
+ ]);
+ }
+ } else if (forceInput) {
+ // Open miss value panel to force user input
+ const missingValueIndex = canStartValueTrigger ? 1 : 0;
+
+ // Same index means user choice to close picker
+ if (missingValueIndex === activePickerIndex) {
+ return;
+ }
+
+ triggerOpen(true, missingValueIndex);
+
+ // Delay to focus to avoid input blur trigger expired selectedValues
+ setTimeout(() => {
+ const inputRef = [startInputRef, endInputRef][missingValueIndex];
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, 0);
}
};
- // ============================== Open ==============================
- const startPickerRef = React.useRef>(null);
- const endPickerRef = React.useRef>(null);
- const lastOpenIdRef = React.useRef();
-
- const onStartOpenChange = (open: boolean) => {
- if (!open && selectedValues && selectedValues[0]) {
- lastOpenIdRef.current = window.setTimeout(() => {
- if (endPickerRef.current) {
- endPickerRef.current!.focus();
- endPickerRef.current!.open();
- }
- }, 100);
+ triggerOpen = (
+ newOpen: boolean,
+ index: 0 | 1,
+ preventChangeEvent: boolean = false,
+ ) => {
+ if (newOpen) {
+ setActivePickerIndex(index);
+ triggerInnerOpen(newOpen);
+ } else if (activePickerIndex === index) {
+ triggerInnerOpen(newOpen);
+ if (!preventChangeEvent) {
+ triggerChange(selectedValue);
+ }
}
+ };
- if (props.onOpenChange) {
- props.onOpenChange(open);
+ const forwardKeyDown = (e: React.KeyboardEvent) => {
+ if (mergedOpen && operationRef.current && operationRef.current.onKeyDown) {
+ // Let popup panel handle keyboard
+ return operationRef.current.onKeyDown(e);
}
+ return false;
};
- React.useEffect(
- () => () => {
- window.clearTimeout(lastOpenIdRef.current);
- },
- [],
+ // ============================= Text ==============================
+ const sharedTextHooksProps = {
+ formatList,
+ generateConfig,
+ locale,
+ };
+
+ const startValueTexts = useValueTexts(
+ getValue(selectedValue, 0),
+ sharedTextHooksProps,
);
- if (pickerRef) {
- pickerRef.current = startPickerRef.current as any;
- }
+ const endValueTexts = useValueTexts(
+ getValue(selectedValue, 1),
+ sharedTextHooksProps,
+ );
- // ============================== Mode ==============================
- /**
- * [Legacy] handle internal `onPanelChange`
- */
- const [innerModes, setInnerModes] = React.useState((): [
- PanelMode,
- PanelMode,
- ] => {
- if (mode) {
- return mode;
- }
- if (picker) {
- return [picker, picker];
+ const onTextChange = (newText: string, index: 0 | 1) => {
+ const inputDate = generateConfig.locale.parse(
+ locale.locale,
+ newText,
+ formatList,
+ );
+
+ const disabledFunc = index === 0 ? disabledStartDate : disabledEndDate;
+
+ if (inputDate && !disabledFunc(inputDate)) {
+ setSelectedValue(updateValues(selectedValue, inputDate, index));
+ setViewDates(updateValues(viewDates, inputDate, index));
}
- return showTime ? ['datetime', 'datetime'] : ['date', 'date'];
+ };
+
+ const [
+ startText,
+ triggerStartTextChange,
+ resetStartText,
+ ] = useTextValueMapping({
+ valueTexts: startValueTexts,
+ onTextChange: newText => onTextChange(newText, 0),
+ });
+
+ const [endText, triggerEndTextChange, resetEndText] = useTextValueMapping<
+ DateType
+ >({
+ valueTexts: endValueTexts,
+ onTextChange: newText => onTextChange(newText, 1),
});
- const [onStartPanelChange, onEndPanelChange] = React.useMemo<
- [OnPanelChange | undefined, OnPanelChange | undefined]
- >(() => {
- const onInternalPanelChange = (
- newValue: DateType,
- newMode: PanelMode,
- source: 'start' | 'end',
- ) => {
- const values: [EventValue, EventValue] = [
- ...(mergedValue || []),
- ] as [EventValue, EventValue];
- const modes: [PanelMode, PanelMode] = [...innerModes] as [
- PanelMode,
- PanelMode,
- ];
-
- if (source === 'start') {
- values[0] = newValue;
- modes[0] = newMode;
- } else {
- values[1] = newValue;
- modes[1] = newMode;
- }
- setInnerModes(modes);
- if (onPanelChange) {
- onPanelChange(values, modes);
+ // ============================= Input =============================
+ const getSharedInputHookProps = (
+ index: 0 | 1,
+ inputDivRef: React.RefObject,
+ resetText: () => void,
+ ) => ({
+ forwardKeyDown,
+ onBlur,
+ isClickOutside: (target: EventTarget | null) =>
+ !!(
+ panelDivRef.current &&
+ !panelDivRef.current.contains(target as Node) &&
+ inputDivRef.current &&
+ !inputDivRef.current.contains(target as Node) &&
+ onOpenChange
+ ),
+ onFocus: (e: React.FocusEvent) => {
+ setActivePickerIndex(index);
+ if (onFocus) {
+ onFocus(e);
}
- };
+ },
+ triggerOpen: (newOpen: boolean) => triggerOpen(newOpen, index),
+ onSubmit: () => {
+ triggerChange(selectedValue);
+ triggerOpen(false, index, true);
+ resetText();
+ },
+ onCancel: () => {
+ triggerOpen(false, index, true);
+ setSelectedValue(mergedValue);
+ resetText();
+ },
+ });
- return [
- (newVal: DateType, newMode: PanelMode) => {
- onInternalPanelChange(newVal, newMode, 'start');
- },
- (newVal: DateType, newMode: PanelMode) => {
- onInternalPanelChange(newVal, newMode, 'end');
- },
- ];
- }, [onPanelChange, mode, picker]);
+ const [
+ startInputProps,
+ { focused: startFocused, typing: startTyping },
+ ] = usePickerInput({
+ ...getSharedInputHookProps(0, startInputDivRef, resetStartText),
+ open: startOpen,
+ });
+ const [
+ endInputProps,
+ { focused: endFocused, typing: endTyping },
+ ] = usePickerInput({
+ ...getSharedInputHookProps(1, endInputDivRef, resetEndText),
+ open: endOpen,
+ });
+
+ // ============================= Sync ==============================
+ // Close should sync back with text value
React.useEffect(() => {
- if (mode) {
- setInnerModes(mode);
+ if (!mergedOpen) {
+ setSelectedValue(mergedValue);
}
- }, [mode]);
-
- // ============================= Render =============================
- const pickerProps = {
- ...props,
- defaultValue: undefined,
- defaultPickerValue: undefined,
- className: undefined,
- style: undefined,
- placeholder: undefined,
- disabledTime: undefined,
- onPanelChange: undefined,
- };
-
- // Time
- let disabledStartTime: DisabledTime | undefined;
- let disabledEndTime: DisabledTime | undefined;
+ }, [mergedOpen]);
- if (disabledTime) {
- disabledStartTime = (date: DateType | null) => disabledTime(date, 'start');
- disabledEndTime = (date: DateType | null) => disabledTime(date, 'end');
- }
+ // Sync innerValue with control mode
+ React.useEffect(() => {
+ // Sync select value
+ setSelectedValue(mergedValue);
+ }, [mergedValue]);
- // Ranges
- let extraFooterSelections:
- | {
- label: string;
- onClick: React.MouseEventHandler;
- }[]
- | undefined;
- if (ranges) {
- extraFooterSelections = Object.keys(ranges).map(label => ({
- label,
- onClick: () => {
- const rangedValue = ranges[label];
- onInternalChange(
- typeof rangedValue === 'function' ? rangedValue() : rangedValue,
- false,
- );
+ // ============================ Private ============================
+ if (pickerRef) {
+ pickerRef.current = {
+ focus: () => {
+ if (startInputRef.current) {
+ startInputRef.current.focus();
+ }
+ },
+ blur: () => {
+ if (startInputRef.current) {
+ startInputRef.current.blur();
+ }
+ if (endInputRef.current) {
+ endInputRef.current.blur();
+ }
},
- }));
+ };
}
- // End date should disabled before start date
- const { disabledDate } = pickerProps;
+ // ============================= Panel =============================
+ function renderPanel(
+ panelPosition: 'left' | 'right' | false = false,
+ panelProps: Partial> = {},
+ ) {
+ let panelHoverRangedValue: RangeValue = null;
+ if (hoverRangedValue && hoverRangedValue[0] && hoverRangedValue[1]) {
+ panelHoverRangedValue = hoverRangedValue;
+ }
- const disabledStartDate = (date: DateType) => {
- let mergedDisabled = disabledDate ? disabledDate(date) : false;
+ return (
+
+
+ {...(props as any)}
+ {...panelProps}
+ mode={mergedModes[activePickerIndex]}
+ generateConfig={generateConfig}
+ style={undefined}
+ disabledDate={
+ activePickerIndex === 0 ? disabledStartDate : disabledEndDate
+ }
+ className={classNames({
+ [`${prefixCls}-panel-focused`]: !startTyping && !endTyping,
+ })}
+ value={getValue(selectedValue, activePickerIndex)}
+ locale={locale}
+ tabIndex={-1}
+ onMouseDown={e => {
+ e.preventDefault();
+ }}
+ onSelect={date => {
+ const values = updateValues(selectedValue, date, activePickerIndex);
+
+ if (picker === 'date' && showTime) {
+ setSelectedValue(values);
+ } else {
+ // triggerChange will also update selected values
+ triggerChange(values);
+ }
+ }}
+ onPanelChange={(date, newMode) => {
+ triggerModesChange(
+ updateValues(mergedModes, newMode, activePickerIndex),
+ updateValues(selectedValue, date, activePickerIndex),
+ );
+
+ setViewDates(updateValues(viewDates, date, activePickerIndex));
+ }}
+ onChange={undefined}
+ defaultValue={undefined}
+ defaultPickerValue={undefined}
+ />
+
+ );
+ }
- if (mergedSelectable[1] === false && value2) {
- mergedDisabled =
- !isSameDate(generateConfig, date, value2) &&
- generateConfig.isAfter(date, value2);
+ function renderPanels() {
+ if (picker !== 'time' && !showTime) {
+ const viewDate = viewDates[activePickerIndex];
+ const nextViewDate = getClosingViewDate(viewDate, picker, generateConfig);
+ const currentMode = mergedModes[activePickerIndex];
+
+ const showDoublePanel = currentMode === picker;
+
+ return (
+ <>
+ {renderPanel(showDoublePanel ? 'left' : false, {
+ pickerValue: viewDate,
+ onPickerValueChange: (newViewDate: DateType) => {
+ setViewDates(
+ updateValues(viewDates, newViewDate, activePickerIndex),
+ );
+ },
+ })}
+ {showDoublePanel &&
+ renderPanel('right', {
+ pickerValue: nextViewDate,
+ onPickerValueChange: newViewDate => {
+ setViewDates(
+ updateValues(
+ viewDates,
+ getClosingViewDate(newViewDate, picker, generateConfig, -1),
+ activePickerIndex,
+ ),
+ );
+ },
+ })}
+ >
+ );
}
+ return renderPanel();
+ }
- return mergedDisabled;
- };
+ const rangePanel = (
+
+ );
- const disabledEndDate = (date: DateType) => {
- let mergedDisabled = disabledDate ? disabledDate(date) : false;
+ let suffixNode: React.ReactNode;
+ if (suffixIcon) {
+ suffixNode = {suffixIcon};
+ }
- if (!mergedDisabled && value1) {
- // Can be the same date
- mergedDisabled =
- !isSameDate(generateConfig, value1, date) &&
- generateConfig.isAfter(value1, date);
- }
+ let clearNode: React.ReactNode;
+ if (
+ allowClear &&
+ ((getValue(mergedValue, 0) && !mergedDisabled[0]) ||
+ (getValue(mergedValue, 1) && !mergedDisabled[1]))
+ ) {
+ clearNode = (
+ {
+ e.stopPropagation();
+ let values = mergedValue;
+
+ if (!mergedDisabled[0]) {
+ values = updateValues(values, null, 0);
+ }
+ if (!mergedDisabled[1]) {
+ values = updateValues(values, null, 1);
+ }
+
+ triggerChange(values, false);
+ }}
+ className={`${prefixCls}-clear`}
+ >
+ {clearIcon || }
+
+ );
+ }
- return mergedDisabled;
+ const inputSharedProps = {
+ size: getInputSize(picker, formatList[0]),
};
return (
-
-
-
- {...pickerProps}
- ref={startPickerRef}
- prefixCls={prefixCls}
- value={value1}
- placeholder={placeholder && placeholder[0]}
- defaultPickerValue={defaultPickerValue && defaultPickerValue[0]}
- {...{ disabledTime: disabledStartTime, showTime: startShowTime }} // Fix ts define
- mode={mode && mode[0]}
- disabled={disabled || mergedSelectable[0] === false}
- disabledDate={disabledStartDate}
- onChange={date => {
- onInternalChange([date, value2], true);
- }}
- onSelect={onStartSelect}
- onFocus={onFocus}
- onBlur={onBlur}
- onPanelChange={onStartPanelChange}
- onOpenChange={onStartOpenChange}
- />
- {separator}
-
- {...pickerProps}
- ref={endPickerRef}
- prefixCls={prefixCls}
- value={value2}
- placeholder={placeholder && placeholder[1]}
- defaultPickerValue={defaultPickerValue && defaultPickerValue[1]}
- {...{ disabledTime: disabledEndTime, showTime: endShowTime }} // Fix ts define
- mode={mode && mode[1]}
- disabled={disabled || mergedSelectable[1] === false}
- disabledDate={disabledEndDate}
- onChange={date => {
- onInternalChange([value1, date], false);
- }}
- onSelect={onEndSelect}
- onFocus={onFocus}
- onBlur={onBlur}
- onPanelChange={onEndPanelChange}
- />
-
-
+
+
+
);
}
@@ -502,7 +837,7 @@ class RangePicker extends React.Component<
render() {
return (
-
+
{...this.props}
pickerRef={this.pickerRef as React.MutableRefObject}
/>
diff --git a/src/hooks/useMergeState.ts b/src/hooks/useMergeState.ts
new file mode 100644
index 000000000..5a00eb488
--- /dev/null
+++ b/src/hooks/useMergeState.ts
@@ -0,0 +1,44 @@
+import * as React from 'react';
+
+export default function useMergedState({
+ value,
+ defaultValue,
+ defaultStateValue,
+ onChange,
+ postState,
+}: {
+ value?: T;
+ defaultValue?: T | (() => T);
+ defaultStateValue: T | (() => T);
+ onChange?: (value: T, prevValue: T) => void;
+ postState?: (value: T) => T;
+}): [R, (value: T) => void] {
+ const [innerValue, setInnerValue] = React.useState(() => {
+ if (value !== undefined) {
+ return value;
+ }
+ if (defaultValue !== undefined) {
+ return typeof defaultValue === 'function'
+ ? (defaultValue as any)()
+ : defaultValue;
+ }
+ return typeof defaultStateValue === 'function'
+ ? (defaultStateValue as any)()
+ : defaultStateValue;
+ });
+
+ let mergedValue = value !== undefined ? value : innerValue;
+ if (postState) {
+ mergedValue = postState(mergedValue);
+ }
+
+ function triggerChange(newValue: T) {
+ setInnerValue(newValue);
+
+ if (mergedValue !== newValue && onChange) {
+ onChange(newValue, mergedValue);
+ }
+ }
+
+ return [(mergedValue as unknown) as R, triggerChange];
+}
diff --git a/src/hooks/usePickerInput.ts b/src/hooks/usePickerInput.ts
new file mode 100644
index 000000000..48a47b68c
--- /dev/null
+++ b/src/hooks/usePickerInput.ts
@@ -0,0 +1,121 @@
+import * as React from 'react';
+import KeyCode from 'rc-util/lib/KeyCode';
+import { addGlobalMouseDownEvent } from '../utils/uiUtil';
+
+export default function usePickerInput({
+ open,
+ isClickOutside,
+ triggerOpen,
+ forwardKeyDown,
+ onSubmit,
+ onCancel,
+ onFocus,
+ onBlur,
+}: {
+ open: boolean;
+ isClickOutside: (clickElement: EventTarget | null) => boolean;
+ triggerOpen: (open: boolean) => void;
+ forwardKeyDown: (e: React.KeyboardEvent) => boolean;
+ onSubmit: () => void;
+ onCancel: () => void;
+ onFocus?: React.FocusEventHandler;
+ onBlur?: React.FocusEventHandler;
+}): [
+ React.DOMAttributes,
+ { focused: boolean; typing: boolean },
+] {
+ const [typing, setTyping] = React.useState(false);
+ const [focused, setFocused] = React.useState(false);
+
+ /**
+ * We will prevent blur to handle open event when user click outside,
+ * since this will repeat trigger `onOpenChange` event.
+ */
+ const preventBlurRef = React.useRef(false);
+
+ const inputProps: React.DOMAttributes = {
+ onMouseDown: () => {
+ setTyping(true);
+ triggerOpen(true);
+ },
+ onKeyDown: e => {
+ switch (e.which) {
+ case KeyCode.ENTER: {
+ if (!open) {
+ triggerOpen(true);
+ } else {
+ onSubmit();
+ setTyping(true);
+ }
+ return;
+ }
+
+ case KeyCode.TAB: {
+ if (typing && open && !e.shiftKey) {
+ setTyping(false);
+ e.preventDefault();
+ } else if (!typing && open) {
+ if (!forwardKeyDown(e) && e.shiftKey) {
+ setTyping(true);
+ e.preventDefault();
+ }
+ }
+ return;
+ }
+
+ case KeyCode.ESC: {
+ setTyping(true);
+ onCancel();
+ return;
+ }
+ }
+
+ if (!open && ![KeyCode.SHIFT].includes(e.which)) {
+ triggerOpen(true);
+ } else if (!typing) {
+ // Let popup panel handle keyboard
+ forwardKeyDown(e);
+ }
+ },
+
+ onFocus: e => {
+ setTyping(true);
+ setFocused(true);
+
+ if (onFocus) {
+ onFocus(e);
+ }
+ },
+
+ onBlur: e => {
+ if (preventBlurRef.current) {
+ preventBlurRef.current = false;
+ return;
+ }
+
+ triggerOpen(false);
+ setFocused(false);
+
+ if (onBlur) {
+ onBlur(e);
+ }
+ },
+ };
+
+ // Global click handler
+ React.useEffect(() =>
+ addGlobalMouseDownEvent(({ target }: MouseEvent) => {
+ if (open && isClickOutside(target)) {
+ preventBlurRef.current = true;
+ triggerOpen(false);
+
+ // Always set back in case `onBlur` prevented by user
+ window.setTimeout(() => {
+ preventBlurRef.current = false;
+ }, 0);
+ }
+ }),
+ );
+
+ return [inputProps, { focused, typing }];
+}
diff --git a/src/hooks/useRangeDisabled.ts b/src/hooks/useRangeDisabled.ts
new file mode 100644
index 000000000..75d367a71
--- /dev/null
+++ b/src/hooks/useRangeDisabled.ts
@@ -0,0 +1,51 @@
+import { RangeValue } from '../interface';
+import { getValue } from '../utils/miscUtil';
+import { GenerateConfig } from '../generate';
+import { isSameDate } from '../utils/dateUtil';
+
+export default function useRangeDisabled({
+ selectedValue,
+ disabledDate,
+ disabled,
+ generateConfig,
+}: {
+ selectedValue: RangeValue;
+ disabledDate?: (date: DateType) => boolean;
+ disabled: [boolean, boolean];
+ generateConfig: GenerateConfig;
+}) {
+ const startDate = getValue(selectedValue, 0);
+ const endDate = getValue(selectedValue, 1);
+
+ function disabledStartDate(date: DateType) {
+ if (disabledDate && disabledDate(date)) {
+ return true;
+ }
+
+ if (disabled[1] && endDate) {
+ return (
+ !isSameDate(generateConfig, date, endDate) &&
+ generateConfig.isAfter(date, endDate)
+ );
+ }
+
+ return false;
+ }
+
+ function disableEndDate(date: DateType) {
+ if (disabledDate && disabledDate(date)) {
+ return true;
+ }
+
+ if (startDate) {
+ return (
+ !isSameDate(generateConfig, date, startDate) &&
+ generateConfig.isAfter(startDate, date)
+ );
+ }
+
+ return false;
+ }
+
+ return [disabledStartDate, disableEndDate];
+}
diff --git a/src/hooks/useTextValueMapping.ts b/src/hooks/useTextValueMapping.ts
new file mode 100644
index 000000000..bb6f35c44
--- /dev/null
+++ b/src/hooks/useTextValueMapping.ts
@@ -0,0 +1,31 @@
+import * as React from 'react';
+
+export default function useTextValueMapping({
+ valueTexts,
+ onTextChange,
+}: {
+ /** Must useMemo, to assume that `valueTexts` only match on the first change */
+ valueTexts: string[];
+ onTextChange: (text: string) => void;
+}): [string, React.ChangeEventHandler, () => void] {
+ const [text, setInnerText] = React.useState('');
+
+ function triggerTextChange({
+ target: { value },
+ }: React.ChangeEvent) {
+ setInnerText(value);
+ onTextChange(value);
+ }
+
+ function resetText() {
+ setInnerText(valueTexts[0]);
+ }
+
+ React.useEffect(() => {
+ if (valueTexts.every(valText => valText !== text)) {
+ resetText();
+ }
+ }, [valueTexts.join('||')]);
+
+ return [text, triggerTextChange, resetText];
+}
diff --git a/src/hooks/useValueTexts.ts b/src/hooks/useValueTexts.ts
new file mode 100644
index 000000000..e16bd0b94
--- /dev/null
+++ b/src/hooks/useValueTexts.ts
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { GenerateConfig } from '../generate';
+import { Locale } from '../interface';
+
+export default function useValueTexts(
+ value: DateType | null,
+ {
+ formatList,
+ generateConfig,
+ locale,
+ }: {
+ formatList: string[];
+ generateConfig: GenerateConfig;
+ locale: Locale;
+ },
+) {
+ return React.useMemo(() => {
+ if (!value) {
+ return [''];
+ }
+ return formatList.map(subFormat =>
+ generateConfig.locale.format(locale.locale, value, subFormat),
+ );
+ }, [value]);
+}
diff --git a/src/interface.ts b/src/interface.ts
index 817e55a05..ae038eec0 100644
--- a/src/interface.ts
+++ b/src/interface.ts
@@ -39,14 +39,7 @@ export interface Locale {
shortMonths?: string[];
}
-export type PanelMode =
- | 'time'
- | 'datetime'
- | 'date'
- | 'week'
- | 'month'
- | 'year'
- | 'decade';
+export type PanelMode = 'time' | 'date' | 'week' | 'month' | 'year' | 'decade';
export type PickerMode = Exclude;
@@ -97,3 +90,8 @@ export type OnPanelChange = (
value: DateType,
mode: PanelMode,
) => void;
+
+export type EventValue = DateType | null;
+export type RangeValue =
+ | [EventValue, EventValue]
+ | null;
diff --git a/src/panels/DatePanel/DateBody.tsx b/src/panels/DatePanel/DateBody.tsx
index 8a80282e9..b45036a3f 100644
--- a/src/panels/DatePanel/DateBody.tsx
+++ b/src/panels/DatePanel/DateBody.tsx
@@ -10,6 +10,7 @@ import {
} from '../../utils/dateUtil';
import { Locale } from '../../interface';
import RangeContext from '../../RangeContext';
+import PanelContext from '../../PanelContext';
export type DateRender = (
currentDate: DateType,
@@ -48,7 +49,8 @@ function DateBody({
dateRender,
onSelect,
}: DateBodyProps) {
- const { rangedValue } = React.useContext(RangeContext);
+ const { rangedValue, hoverRangedValue } = React.useContext(RangeContext);
+ const { onDateMouseEnter, onDateMouseLeave } = React.useContext(PanelContext);
const datePrefixCls = `${prefixCls}-cell`;
const weekFirstDay = generateConfig.locale.getWeekFirstDay(locale.locale);
@@ -95,10 +97,19 @@ function DateBody({
'YYYY-MM-DD',
)}
onClick={() => {
- if (disabled) {
- return;
+ if (!disabled) {
+ onSelect(currentDate);
+ }
+ }}
+ onMouseEnter={() => {
+ if (!disabled && onDateMouseEnter) {
+ onDateMouseEnter(currentDate);
+ }
+ }}
+ onMouseLeave={() => {
+ if (!disabled && onDateMouseLeave) {
+ onDateMouseLeave(currentDate);
}
- onSelect(currentDate);
}}
className={classNames(datePrefixCls, {
[`${datePrefixCls}-disabled`]: disabled,
@@ -123,6 +134,22 @@ function DateBody({
rangedValue && rangedValue[1],
currentDate,
),
+ [`${datePrefixCls}-range-hover`]: isInRange(
+ generateConfig,
+ hoverRangedValue && hoverRangedValue[0],
+ hoverRangedValue && hoverRangedValue[1],
+ currentDate,
+ ),
+ [`${datePrefixCls}-range-hover-start`]: isSameDate(
+ generateConfig,
+ hoverRangedValue && hoverRangedValue[0],
+ currentDate,
+ ),
+ [`${datePrefixCls}-range-hover-end`]: isSameDate(
+ generateConfig,
+ hoverRangedValue && hoverRangedValue[1],
+ currentDate,
+ ),
[`${datePrefixCls}-today`]: isSameDate(
generateConfig,
today,
diff --git a/src/panels/DatePanel/index.tsx b/src/panels/DatePanel/index.tsx
index 4da2a649c..65812135a 100644
--- a/src/panels/DatePanel/index.tsx
+++ b/src/panels/DatePanel/index.tsx
@@ -90,10 +90,10 @@ function DatePanel(props: DatePanelProps) {
onMonthChange(1);
}}
onMonthClick={() => {
- onPanelChange('month', value || viewDate);
+ onPanelChange('month', viewDate);
}}
onYearClick={() => {
- onPanelChange('year', value || viewDate);
+ onPanelChange('year', viewDate);
}}
/>
{onSuperPrev && (
@@ -41,6 +48,7 @@ function Header({
onClick={onSuperPrev}
tabIndex={-1}
className={`${prefixCls}-super-prev-btn`}
+ style={hidePrevBtn ? HIDDEN_STYLE : {}}
>
{superPrevIcon}
@@ -51,6 +59,7 @@ function Header({
onClick={onPrev}
tabIndex={-1}
className={`${prefixCls}-prev-btn`}
+ style={hidePrevBtn ? HIDDEN_STYLE : {}}
>
{prevIcon}
@@ -62,6 +71,7 @@ function Header({
onClick={onNext}
tabIndex={-1}
className={`${prefixCls}-next-btn`}
+ style={hideNextBtn ? HIDDEN_STYLE : {}}
>
{nextIcon}
@@ -72,6 +82,7 @@ function Header({
onClick={onSuperNext}
tabIndex={-1}
className={`${prefixCls}-super-next-btn`}
+ style={hideNextBtn ? HIDDEN_STYLE : {}}
>
{superNextIcon}
diff --git a/src/panels/MonthPanel/MonthBody.tsx b/src/panels/MonthPanel/MonthBody.tsx
index c6b8eacd9..bc78c63db 100644
--- a/src/panels/MonthPanel/MonthBody.tsx
+++ b/src/panels/MonthPanel/MonthBody.tsx
@@ -3,6 +3,7 @@ import classNames from 'classnames';
import { GenerateConfig } from '../../generate';
import { Locale } from '../../interface';
import { isSameMonth } from '../../utils/dateUtil';
+import PanelContext from '../../PanelContext';
export const MONTH_COL_COUNT = 3;
const MONTH_ROW_COUNT = 4;
@@ -33,6 +34,8 @@ function MonthBody({
monthCellRender,
onSelect,
}: MonthBodyProps) {
+ const { onDateMouseEnter, onDateMouseLeave } = React.useContext(PanelContext);
+
const monthPrefixCls = `${prefixCls}-cell`;
const rows: React.ReactNode[] = [];
@@ -70,10 +73,19 @@ function MonthBody({
),
})}
onClick={() => {
- if (disabled) {
- return;
+ if (!disabled) {
+ onSelect(monthDate);
+ }
+ }}
+ onMouseEnter={() => {
+ if (!disabled && onDateMouseEnter) {
+ onDateMouseEnter(monthDate);
+ }
+ }}
+ onMouseLeave={() => {
+ if (!disabled && onDateMouseLeave) {
+ onDateMouseLeave(monthDate);
}
- onSelect(monthDate);
}}
>
{monthCellRender ? (
diff --git a/src/panels/MonthPanel/index.tsx b/src/panels/MonthPanel/index.tsx
index 281642a7f..324ad75f1 100644
--- a/src/panels/MonthPanel/index.tsx
+++ b/src/panels/MonthPanel/index.tsx
@@ -60,7 +60,7 @@ function MonthPanel(props: MonthPanelProps) {
onYearChange(1);
}}
onYearClick={() => {
- onPanelChange('year', value || viewDate);
+ onPanelChange('year', viewDate);
}}
/>
diff --git a/src/panels/YearPanel/YearBody.tsx b/src/panels/YearPanel/YearBody.tsx
index 69f3c62c9..b1ff05adf 100644
--- a/src/panels/YearPanel/YearBody.tsx
+++ b/src/panels/YearPanel/YearBody.tsx
@@ -2,7 +2,8 @@ import * as React from 'react';
import classNames from 'classnames';
import { GenerateConfig } from '../../generate';
import { YEAR_DECADE_COUNT } from '.';
-import { Locale } from '../../interface';
+import { Locale, NullableDateType } from '../../interface';
+import PanelContext from '../../PanelContext';
export const YEAR_COL_COUNT = 3;
const YEAR_ROW_COUNT = 4;
@@ -11,6 +12,7 @@ export interface YearBodyProps {
prefixCls: string;
locale: Locale;
generateConfig: GenerateConfig;
+ value: NullableDateType;
viewDate: DateType;
disabledDate?: (date: DateType) => boolean;
onSelect: (value: DateType) => void;
@@ -18,15 +20,19 @@ export interface YearBodyProps {
function YearBody({
prefixCls,
+ value,
viewDate,
locale,
generateConfig,
disabledDate,
onSelect,
}: YearBodyProps) {
+ const { onDateMouseEnter, onDateMouseLeave } = React.useContext(PanelContext);
+
const yearPrefixCls = `${prefixCls}-cell`;
const rows: React.ReactNode[] = [];
+ const valueYearNumber = value ? generateConfig.getYear(value) : null;
const yearNumber = generateConfig.getYear(viewDate);
const startYear =
Math.floor(yearNumber / YEAR_DECADE_COUNT) * YEAR_DECADE_COUNT;
@@ -54,13 +60,23 @@ function YearBody({
[`${yearPrefixCls}-disabled`]: disabled,
[`${yearPrefixCls}-in-view`]:
startYear <= currentYearNumber && currentYearNumber <= endYear,
- [`${yearPrefixCls}-selected`]: currentYearNumber === yearNumber,
+ [`${yearPrefixCls}-selected`]:
+ currentYearNumber === valueYearNumber,
})}
onClick={() => {
- if (disabled) {
- return;
+ if (!disabled) {
+ onSelect(yearDate);
+ }
+ }}
+ onMouseEnter={() => {
+ if (!disabled && onDateMouseEnter) {
+ onDateMouseEnter(yearDate);
+ }
+ }}
+ onMouseLeave={() => {
+ if (!disabled && onDateMouseLeave) {
+ onDateMouseLeave(yearDate);
}
- onSelect(yearDate);
}}
>
{currentYearNumber}
diff --git a/src/panels/YearPanel/index.tsx b/src/panels/YearPanel/index.tsx
index 8f9b263ac..23b88ef23 100644
--- a/src/panels/YearPanel/index.tsx
+++ b/src/panels/YearPanel/index.tsx
@@ -62,7 +62,7 @@ function YearPanel(props: YearPanelProps) {
onDecadeChange(1);
}}
onDecadeClick={() => {
- onPanelChange('decade', value || viewDate);
+ onPanelChange('decade', viewDate);
}}
/>
(value1: T, value2: T): boolean | undefined {
return undefined;
}
+export function isSameYear(
+ generateConfig: GenerateConfig,
+ year1: NullableDateType,
+ year2: NullableDateType,
+) {
+ const equal = isNullEqual(year1, year2);
+ if (typeof equal === 'boolean') {
+ return equal;
+ }
+
+ return generateConfig.getYear(year1!) === generateConfig.getYear(year2!);
+}
+
export function isSameMonth(
generateConfig: GenerateConfig,
month1: NullableDateType,
@@ -25,7 +38,7 @@ export function isSameMonth(
}
return (
- generateConfig.getYear(month1!) === generateConfig.getYear(month2!) &&
+ isSameYear(generateConfig, month1, month2) &&
generateConfig.getMonth(month1!) === generateConfig.getMonth(month2!)
);
}
@@ -136,3 +149,19 @@ export function getWeekStartDate(
return value;
}
}
+
+export function getClosingViewDate(
+ viewDate: DateType,
+ picker: PickerMode,
+ generateConfig: GenerateConfig,
+ offset: number = 1,
+): DateType {
+ switch (picker) {
+ case 'year':
+ return generateConfig.addYear(viewDate, offset * 10);
+ case 'month':
+ return generateConfig.addYear(viewDate, offset);
+ default:
+ return generateConfig.addMonth(viewDate, offset);
+ }
+}
diff --git a/src/utils/miscUtil.ts b/src/utils/miscUtil.ts
index badd058a0..ed2a203cb 100644
--- a/src/utils/miscUtil.ts
+++ b/src/utils/miscUtil.ts
@@ -37,3 +37,34 @@ export default function getDataOrAriaProps(props: any) {
return retProps;
}
+
+export function getValue(
+ values: null | undefined | [T | null, T | null],
+ index: number,
+): T | null {
+ return values ? values[index] : null;
+}
+
+type UpdateValue = (prev: T) => T;
+
+export function updateValues(
+ values: [T | null, T | null] | null,
+ value: T | UpdateValue,
+ index: number,
+): R {
+ const newValues: [T | null, T | null] = [
+ getValue(values, 0),
+ getValue(values, 1),
+ ];
+
+ newValues[index] =
+ typeof value === 'function'
+ ? (value as UpdateValue)(newValues[index])
+ : value;
+
+ if (!newValues[0] && !newValues[1]) {
+ return (null as unknown) as R;
+ }
+
+ return (newValues as unknown) as R;
+}
diff --git a/src/utils/uiUtil.ts b/src/utils/uiUtil.ts
index 2d1aa2b78..5030b5ff0 100644
--- a/src/utils/uiUtil.ts
+++ b/src/utils/uiUtil.ts
@@ -214,4 +214,4 @@ export const PickerModeMap: Record<
week: getWeekNextMode,
time: null,
date: null,
-};
+};
\ No newline at end of file