From 04bb47278e2732b19fab80db9b3d92c8ec0d0f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 3 Sep 2026 12:00:10 +0800 Subject: [PATCH 1/7] fix: preserve pending range selection on hover --- src/PickerInput/Popup/PopupPanel.tsx | 3 +++ src/PickerInput/RangePicker.tsx | 8 +++++++- src/PickerInput/hooks/useRangeValueChange.ts | 5 +++++ tests/range.spec.tsx | 16 ++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/PickerInput/Popup/PopupPanel.tsx b/src/PickerInput/Popup/PopupPanel.tsx index e59b4fba3..76d797b29 100644 --- a/src/PickerInput/Popup/PopupPanel.tsx +++ b/src/PickerInput/Popup/PopupPanel.tsx @@ -14,6 +14,7 @@ export type PopupPanelProps = MustProp FooterProps & { multiplePanel?: boolean; range?: boolean; + cellHoverValue?: DateType[]; onPickerValueChange: (date: DateType) => void; }; @@ -30,6 +31,7 @@ export default function PopupPanel( onSubmit, range, hoverValue, + cellHoverValue, } = props; const { prefixCls, generateConfig } = React.useContext(PickerContext); @@ -72,6 +74,7 @@ export default function PopupPanel( if (range) { pickerProps.hoverRangeValue = hoverValue; + pickerProps.hoverValue = cellHoverValue; } else { pickerProps.hoverValue = hoverValue; } diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index 36c2dda2e..f70eec948 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -331,6 +331,7 @@ function RangePicker( triggeredFields, triggerRangeValueChange, resetRangeValueChange, + currentFieldModified, ] = useRangeValueChange( enabledFieldCount, needConfirm, @@ -502,6 +503,10 @@ function RangePicker( return internalHoverValues || calendarValue; }, [calendarValue, internalHoverValues]); + const keepCurrentSelection = needConfirm && currentFieldModified && hoverSource === 'cell'; + const panelHoverValues = keepCurrentSelection ? calendarValue : hoverValues; + const activeHoverValue = internalHoverValues?.[activeIndex]; + // Clean up `internalHoverValues` when closed React.useEffect(() => { if (!mergedOpen) { @@ -640,7 +645,8 @@ function RangePicker( defaultOpenValue={toArray(showTime?.defaultOpenValue)[activeIndex]} onPickerValueChange={setCurrentPickerValue} // Hover - hoverValue={hoverValues} + hoverValue={panelHoverValues} + cellHoverValue={keepCurrentSelection && activeHoverValue ? [activeHoverValue] : null} onHover={onPanelHover} // Submit needConfirm={needConfirm} diff --git a/src/PickerInput/hooks/useRangeValueChange.ts b/src/PickerInput/hooks/useRangeValueChange.ts index 6fad8d25a..dfdccfa7c 100644 --- a/src/PickerInput/hooks/useRangeValueChange.ts +++ b/src/PickerInput/hooks/useRangeValueChange.ts @@ -59,6 +59,7 @@ export type UseRangeValueChangeReturn = [ triggeredFields: number[], triggerChange: TriggerChange, reset: VoidFunction, + currentFieldModified: boolean, ]; interface TriggeredField { @@ -509,6 +510,9 @@ export default function useRangeValueChange( lastValidIndexRef.current = currentIndex ?? lastValidIndexRef.current ?? 0; const triggeredFields = triggeredFieldsRef.current.map((field) => field.index); + const currentFieldModified = triggeredFieldsRef.current.some( + (field) => field.index === currentIndex && field.modified, + ); return [ currentIndex, @@ -517,5 +521,6 @@ export default function useRangeValueChange( triggeredFields, triggerChange, reset, + currentFieldModified, ]; } diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index 43ebad195..639fcf49b 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -768,6 +768,22 @@ describe('Picker.Range', () => { expect(findCell(end)).not.toHaveClass('rc-picker-cell-range-end'); }); }); + + it('should keep the pending selection when hovering with confirmation', async () => { + const { container } = render(); + openPicker(container); + selectCell(11); + + expect(findCell(11)).toHaveClass('rc-picker-cell-range-start'); + + fireEvent.mouseEnter(findCell(22)); + await waitFakeTimer(); + + expect(container.querySelectorAll('input')[0]).toHaveValue('1990-09-22 00:00:00'); + expect(findCell(11)).toHaveClass('rc-picker-cell-range-start'); + expect(findCell(22)).toHaveClass('rc-picker-cell-hover'); + expect(findCell(22)).not.toHaveClass('rc-picker-cell-range-start'); + }); }); it('should close when user focus out', () => { From a5eec7a38eae6ad9f94df1fca97bc7df8245541a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 4 Sep 2026 16:46:19 +0800 Subject: [PATCH 2/7] refactor: separate range and cell hover values --- src/PickerInput/Popup/PopupPanel.tsx | 22 +--------------------- src/PickerInput/RangePicker.tsx | 5 ++--- tests/range.spec.tsx | 3 ++- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/PickerInput/Popup/PopupPanel.tsx b/src/PickerInput/Popup/PopupPanel.tsx index 76d797b29..27779142c 100644 --- a/src/PickerInput/Popup/PopupPanel.tsx +++ b/src/PickerInput/Popup/PopupPanel.tsx @@ -14,7 +14,6 @@ export type PopupPanelProps = MustProp FooterProps & { multiplePanel?: boolean; range?: boolean; - cellHoverValue?: DateType[]; onPickerValueChange: (date: DateType) => void; }; @@ -22,17 +21,7 @@ export type PopupPanelProps = MustProp export default function PopupPanel( props: PopupPanelProps, ) { - const { - picker, - multiplePanel, - pickerValue, - onPickerValueChange, - needConfirm, - onSubmit, - range, - hoverValue, - cellHoverValue, - } = props; + const { picker, multiplePanel, pickerValue, onPickerValueChange, needConfirm, onSubmit } = props; const { prefixCls, generateConfig } = React.useContext(PickerContext); // ======================== Offset ======================== @@ -67,18 +56,9 @@ export default function PopupPanel( // ======================== Props ========================= const pickerProps = { ...props, - hoverValue: null, - hoverRangeValue: null, hideHeader, }; - if (range) { - pickerProps.hoverRangeValue = hoverValue; - pickerProps.hoverValue = cellHoverValue; - } else { - pickerProps.hoverValue = hoverValue; - } - // ======================== Render ======================== // Multiple if (multiplePanel) { diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index f70eec948..8f4f1b270 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -504,7 +504,6 @@ function RangePicker( }, [calendarValue, internalHoverValues]); const keepCurrentSelection = needConfirm && currentFieldModified && hoverSource === 'cell'; - const panelHoverValues = keepCurrentSelection ? calendarValue : hoverValues; const activeHoverValue = internalHoverValues?.[activeIndex]; // Clean up `internalHoverValues` when closed @@ -645,8 +644,8 @@ function RangePicker( defaultOpenValue={toArray(showTime?.defaultOpenValue)[activeIndex]} onPickerValueChange={setCurrentPickerValue} // Hover - hoverValue={panelHoverValues} - cellHoverValue={keepCurrentSelection && activeHoverValue ? [activeHoverValue] : null} + hoverValue={keepCurrentSelection && activeHoverValue ? [activeHoverValue] : null} + hoverRangeValue={keepCurrentSelection ? null : hoverValues} onHover={onPanelHover} // Submit needConfirm={needConfirm} diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index 639fcf49b..2428fac15 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -780,7 +780,8 @@ describe('Picker.Range', () => { await waitFakeTimer(); expect(container.querySelectorAll('input')[0]).toHaveValue('1990-09-22 00:00:00'); - expect(findCell(11)).toHaveClass('rc-picker-cell-range-start'); + expect(findCell(11)).toHaveClass('rc-picker-cell-selected'); + expect(findCell(11)).not.toHaveClass('rc-picker-cell-range-start'); expect(findCell(22)).toHaveClass('rc-picker-cell-hover'); expect(findCell(22)).not.toHaveClass('rc-picker-cell-range-start'); }); From b26da4a726b9aea43f6957765ecb66502208c3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 4 Sep 2026 16:56:15 +0800 Subject: [PATCH 3/7] refactor: derive pending range from values --- src/PickerInput/Popup/PopupPanel.tsx | 19 ++++++++++++++++++- src/PickerInput/RangePicker.tsx | 7 +++++-- src/PickerInput/hooks/useRangeValueChange.ts | 5 ----- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/PickerInput/Popup/PopupPanel.tsx b/src/PickerInput/Popup/PopupPanel.tsx index 27779142c..26aad7765 100644 --- a/src/PickerInput/Popup/PopupPanel.tsx +++ b/src/PickerInput/Popup/PopupPanel.tsx @@ -21,7 +21,17 @@ export type PopupPanelProps = MustProp export default function PopupPanel( props: PopupPanelProps, ) { - const { picker, multiplePanel, pickerValue, onPickerValueChange, needConfirm, onSubmit } = props; + const { + picker, + multiplePanel, + pickerValue, + onPickerValueChange, + needConfirm, + onSubmit, + range, + hoverValue, + hoverRangeValue, + } = props; const { prefixCls, generateConfig } = React.useContext(PickerContext); // ======================== Offset ======================== @@ -56,9 +66,16 @@ export default function PopupPanel( // ======================== Props ========================= const pickerProps = { ...props, + hoverValue: null, + hoverRangeValue: null, hideHeader, }; + pickerProps.hoverValue = hoverValue; + if (range) { + pickerProps.hoverRangeValue = hoverRangeValue; + } + // ======================== Render ======================== // Multiple if (multiplePanel) { diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index 8f4f1b270..3f7dbfb19 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -24,6 +24,7 @@ import type { import type { PickerPanelProps } from '../PickerPanel'; import PickerTrigger from '../PickerTrigger'; import { pickTriggerProps } from '../PickerTrigger/util'; +import { isSameTimestamp } from '../utils/dateUtil'; import { fillIndex, getFromDate, toArray } from '../utils/miscUtil'; import PickerContext from './context'; import useCellRender from './hooks/useCellRender'; @@ -331,7 +332,6 @@ function RangePicker( triggeredFields, triggerRangeValueChange, resetRangeValueChange, - currentFieldModified, ] = useRangeValueChange( enabledFieldCount, needConfirm, @@ -503,7 +503,10 @@ function RangePicker( return internalHoverValues || calendarValue; }, [calendarValue, internalHoverValues]); - const keepCurrentSelection = needConfirm && currentFieldModified && hoverSource === 'cell'; + const keepCurrentSelection = + needConfirm && + hoverSource === 'cell' && + !isSameTimestamp(generateConfig, calendarValue[activeIndex], mergedValue[activeIndex]); const activeHoverValue = internalHoverValues?.[activeIndex]; // Clean up `internalHoverValues` when closed diff --git a/src/PickerInput/hooks/useRangeValueChange.ts b/src/PickerInput/hooks/useRangeValueChange.ts index dfdccfa7c..6fad8d25a 100644 --- a/src/PickerInput/hooks/useRangeValueChange.ts +++ b/src/PickerInput/hooks/useRangeValueChange.ts @@ -59,7 +59,6 @@ export type UseRangeValueChangeReturn = [ triggeredFields: number[], triggerChange: TriggerChange, reset: VoidFunction, - currentFieldModified: boolean, ]; interface TriggeredField { @@ -510,9 +509,6 @@ export default function useRangeValueChange( lastValidIndexRef.current = currentIndex ?? lastValidIndexRef.current ?? 0; const triggeredFields = triggeredFieldsRef.current.map((field) => field.index); - const currentFieldModified = triggeredFieldsRef.current.some( - (field) => field.index === currentIndex && field.modified, - ); return [ currentIndex, @@ -521,6 +517,5 @@ export default function useRangeValueChange( triggeredFields, triggerChange, reset, - currentFieldModified, ]; } From 4145b05f0d3943558c7b33de664b51a928388bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 4 Sep 2026 17:37:39 +0800 Subject: [PATCH 4/7] fix: preserve range hover after first confirmation --- src/PickerInput/RangePicker.tsx | 13 ++++++++++--- tests/range.spec.tsx | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index 3f7dbfb19..fc4050cd5 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -503,10 +503,17 @@ function RangePicker( return internalHoverValues || calendarValue; }, [calendarValue, internalHoverValues]); + const otherFieldValue = calendarValue[(activeIndex + 1) % 2]; + const currentFieldChanged = !isSameTimestamp( + generateConfig, + calendarValue[activeIndex], + mergedValue[activeIndex], + ); + + // Keep the pending date as a single selected cell only while choosing the first value. + // Once the other field has a value, the range hover takes precedence. const keepCurrentSelection = - needConfirm && - hoverSource === 'cell' && - !isSameTimestamp(generateConfig, calendarValue[activeIndex], mergedValue[activeIndex]); + needConfirm && hoverSource === 'cell' && !otherFieldValue && currentFieldChanged; const activeHoverValue = internalHoverValues?.[activeIndex]; // Clean up `internalHoverValues` when closed diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index 2428fac15..4f2afd6d7 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -785,6 +785,21 @@ describe('Picker.Range', () => { expect(findCell(22)).toHaveClass('rc-picker-cell-hover'); expect(findCell(22)).not.toHaveClass('rc-picker-cell-range-start'); }); + + it('should keep range hover after the first field is confirmed', async () => { + const { container } = render(); + openPicker(container); + selectCell(11); + fireEvent.click(document.querySelector('.rc-picker-ok button')); + selectCell(22); + + fireEvent.mouseEnter(findCell(25)); + await waitFakeTimer(); + + expect(findCell(11)).toHaveClass('rc-picker-cell-range-start'); + expect(findCell(15)).toHaveClass('rc-picker-cell-in-range'); + expect(findCell(25)).toHaveClass('rc-picker-cell-range-end'); + }); }); it('should close when user focus out', () => { From 04ed8569397abab10509fcb8ed0b302c20f848dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 4 Sep 2026 17:43:47 +0800 Subject: [PATCH 5/7] refactor: clarify range hover conditions --- src/PickerInput/RangePicker.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index fc4050cd5..96c8a6914 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -503,17 +503,16 @@ function RangePicker( return internalHoverValues || calendarValue; }, [calendarValue, internalHoverValues]); - const otherFieldValue = calendarValue[(activeIndex + 1) % 2]; - const currentFieldChanged = !isSameTimestamp( - generateConfig, - calendarValue[activeIndex], - mergedValue[activeIndex], - ); - - // Keep the pending date as a single selected cell only while choosing the first value. - // Once the other field has a value, the range hover takes precedence. + // Keep the pending date as a single selected cell only when choosing the first value. const keepCurrentSelection = - needConfirm && hoverSource === 'cell' && !otherFieldValue && currentFieldChanged; + // Confirmation mode keeps the clicked date pending. + needConfirm && + // Preset hover always previews the whole range. + hoverSource === 'cell' && + // Once the other field has a value, range hover takes precedence. + !calendarValue[(activeIndex + 1) % 2] && + // Only a changed active value needs to remain selected. + !isSameTimestamp(generateConfig, calendarValue[activeIndex], mergedValue[activeIndex]); const activeHoverValue = internalHoverValues?.[activeIndex]; // Clean up `internalHoverValues` when closed From f8d41dd0119883606c9ce42c066281e264e6416d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 4 Sep 2026 17:51:51 +0800 Subject: [PATCH 6/7] fix: preserve pending hover without confirmation --- src/PickerInput/RangePicker.tsx | 2 -- tests/range.spec.tsx | 29 ++++++++++++++++------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index 96c8a6914..a678988b1 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -505,8 +505,6 @@ function RangePicker( // Keep the pending date as a single selected cell only when choosing the first value. const keepCurrentSelection = - // Confirmation mode keeps the clicked date pending. - needConfirm && // Preset hover always previews the whole range. hoverSource === 'cell' && // Once the other field has a value, range hover takes precedence. diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index 4f2afd6d7..21e08a3f3 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -769,22 +769,25 @@ describe('Picker.Range', () => { }); }); - it('should keep the pending selection when hovering with confirmation', async () => { - const { container } = render(); - openPicker(container); - selectCell(11); + it.each([true, false])( + 'should keep the pending selection when hovering with needConfirm=%s', + async (needConfirm) => { + const { container } = render(); + openPicker(container); + selectCell(11); - expect(findCell(11)).toHaveClass('rc-picker-cell-range-start'); + expect(findCell(11)).toHaveClass('rc-picker-cell-range-start'); - fireEvent.mouseEnter(findCell(22)); - await waitFakeTimer(); + fireEvent.mouseEnter(findCell(22)); + await waitFakeTimer(); - expect(container.querySelectorAll('input')[0]).toHaveValue('1990-09-22 00:00:00'); - expect(findCell(11)).toHaveClass('rc-picker-cell-selected'); - expect(findCell(11)).not.toHaveClass('rc-picker-cell-range-start'); - expect(findCell(22)).toHaveClass('rc-picker-cell-hover'); - expect(findCell(22)).not.toHaveClass('rc-picker-cell-range-start'); - }); + expect(container.querySelectorAll('input')[0]).toHaveValue('1990-09-22 00:00:00'); + expect(findCell(11)).toHaveClass('rc-picker-cell-selected'); + expect(findCell(11)).not.toHaveClass('rc-picker-cell-range-start'); + expect(findCell(22)).toHaveClass('rc-picker-cell-hover'); + expect(findCell(22)).not.toHaveClass('rc-picker-cell-range-start'); + }, + ); it('should keep range hover after the first field is confirmed', async () => { const { container } = render(); From 8b853a446166ed44ae414d6c58697e00586a333b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 4 Sep 2026 17:53:38 +0800 Subject: [PATCH 7/7] refactor: clarify weak hover state --- src/PickerInput/RangePicker.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index a678988b1..d4540032e 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -503,13 +503,14 @@ function RangePicker( return internalHoverValues || calendarValue; }, [calendarValue, internalHoverValues]); - // Keep the pending date as a single selected cell only when choosing the first value. - const keepCurrentSelection = + // "Weak" hover only highlights the hovered cell instead of composing a range. + // Use it while choosing the first value so the pending selection remains selected. + const showWeakHover = // Preset hover always previews the whole range. hoverSource === 'cell' && // Once the other field has a value, range hover takes precedence. !calendarValue[(activeIndex + 1) % 2] && - // Only a changed active value needs to remain selected. + // Only a changed active value needs weak hover. !isSameTimestamp(generateConfig, calendarValue[activeIndex], mergedValue[activeIndex]); const activeHoverValue = internalHoverValues?.[activeIndex]; @@ -651,8 +652,8 @@ function RangePicker( defaultOpenValue={toArray(showTime?.defaultOpenValue)[activeIndex]} onPickerValueChange={setCurrentPickerValue} // Hover - hoverValue={keepCurrentSelection && activeHoverValue ? [activeHoverValue] : null} - hoverRangeValue={keepCurrentSelection ? null : hoverValues} + hoverValue={showWeakHover && activeHoverValue ? [activeHoverValue] : null} + hoverRangeValue={showWeakHover ? null : hoverValues} onHover={onPanelHover} // Submit needConfirm={needConfirm}