From 2726430634944f971e27923ac95ab969293e4b22 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: Sat, 8 Feb 2025 18:07:30 +0800 Subject: [PATCH 1/4] fix: use Rect for logic --- assets/index.less | 6 +++ src/PickerInput/Popup/index.tsx | 44 ++++++++++++++-------- src/PickerInput/RangePicker.tsx | 9 +++-- src/PickerInput/Selector/RangeSelector.tsx | 39 ++++++++++++------- src/utils/uiUtil.ts | 6 +-- 5 files changed, 69 insertions(+), 35 deletions(-) diff --git a/assets/index.less b/assets/index.less index a23ce0b82..d5db8413b 100644 --- a/assets/index.less +++ b/assets/index.less @@ -37,6 +37,12 @@ &-invalid { box-shadow: 0 0 2px red; } + + &-panels { + display: flex; + flex-wrap: nowrap; + } + &-panel { display: inline-block; vertical-align: top; diff --git a/src/PickerInput/Popup/index.tsx b/src/PickerInput/Popup/index.tsx index 5482ada9d..0b77cf054 100644 --- a/src/PickerInput/Popup/index.tsx +++ b/src/PickerInput/Popup/index.tsx @@ -8,7 +8,7 @@ import type { ValueDate, } from '../../interface'; import { toArray } from '../../utils/miscUtil'; -import { getRealPlacement, getoffsetUnit } from '../../utils/uiUtil'; +import { getRealPlacement, getOffsetUnit } from '../../utils/uiUtil'; import PickerContext from '../context'; import Footer, { type FooterProps } from './Footer'; import PopupPanel, { type PopupPanelProps } from './PopupPanel'; @@ -32,7 +32,7 @@ export interface PopupProps void; // Range - activeOffset?: number; + activeInfo?: [activeInputLeft: number, activeInputRight: number, selectorWidth: number]; placement?: string; // Direction direction?: 'ltr' | 'rtl'; @@ -59,7 +59,7 @@ export default function Popup(props: PopupProps(props: PopupProps(0); const [containerOffset, setContainerOffset] = React.useState(0); + const [arrowOffset, setArrowOffset] = React.useState(0); const onResize: ResizeObserverProps['onResize'] = (info) => { if (info.offsetWidth) { @@ -103,21 +104,38 @@ export default function Popup(props: PopupProps { // `activeOffset` is always align with the active input element // So we need only check container contains the `activeOffset` - if (range) { + if (range && wrapperRef.current) { // Offset in case container has border radius const arrowWidth = arrowRef.current?.offsetWidth || 0; - const maxOffset = containerWidth - arrowWidth; - if (activeOffset <= maxOffset) { - setContainerOffset(0); + // Arrow Offset + const wrapperRect = wrapperRef.current.getBoundingClientRect(); + const nextArrowOffset = rtl + ? activeInputRight - arrowWidth + : activeInputLeft - wrapperRect.left; + setArrowOffset(nextArrowOffset); + + // Container Offset + if (containerWidth < selectorWidth) { + if (rtl) { + const offset = wrapperRect.right - (activeInputRight - arrowWidth + containerWidth); + const safeOffset = Math.max(0, offset); + setContainerOffset(safeOffset); + } else { + const offset = activeInputLeft + arrowWidth - wrapperRect.left - containerWidth; + const safeOffset = Math.max(0, offset); + setContainerOffset(safeOffset); + } } else { - setContainerOffset(activeOffset + arrowWidth - containerWidth); + setContainerOffset(0); } } - }, [containerWidth, activeOffset, range]); + }, [rtl, containerWidth, activeInputLeft, activeInputRight, selectorWidth, range]); // ======================== Custom ======================== function filterEmpty(list: T[]) { @@ -213,19 +231,13 @@ export default function Popup(props: PopupProps -
+
{/* Watch for container size */} {renderNode} diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index 5c01e303d..51bdeedb2 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -473,7 +473,10 @@ function RangePicker( // == Panels == // ======================================================== // Save the offset with active bar position - const [activeOffset, setActiveOffset] = React.useState(0); + // const [activeOffset, setActiveOffset] = React.useState(0); + const [activeInfo, setActiveInfo] = React.useState< + [activeInputLeft: number, activeInputRight: number, selectorWidth: number] + >([0, 0, 0]); // ======================= Presets ======================== const presetList = usePresets(presets, ranges); @@ -574,7 +577,7 @@ function RangePicker( // Range range multiplePanel={multiplePanel} - activeOffset={activeOffset} + activeInfo={activeInfo} placement={placement} // Disabled disabledDate={mergedDisabledDate} @@ -796,7 +799,7 @@ function RangePicker( invalid={submitInvalidates} onInvalid={onSelectorInvalid} // Offset - onActiveOffset={setActiveOffset} + onActiveInfo={setActiveInfo} /> diff --git a/src/PickerInput/Selector/RangeSelector.tsx b/src/PickerInput/Selector/RangeSelector.tsx index 70894a011..d0e9e3996 100644 --- a/src/PickerInput/Selector/RangeSelector.tsx +++ b/src/PickerInput/Selector/RangeSelector.tsx @@ -8,7 +8,7 @@ import useInputProps from './hooks/useInputProps'; import useRootProps from './hooks/useRootProps'; import Icon, { ClearIcon } from './Icon'; import Input, { type InputRef } from './Input'; -import { getoffsetUnit, getRealPlacement } from '../../utils/uiUtil'; +// import { getOffsetUnit, getRealPlacement } from '../../utils/uiUtil'; export type SelectorIdType = | string @@ -42,7 +42,9 @@ export interface RangeSelectorProps extends SelectorProps void; + onActiveInfo: ( + info: [activeInputLeft: number, activeInputRight: number, selectorWidth: number], + ) => void; } function RangeSelector( @@ -102,7 +104,7 @@ function RangeSelector( onOpenChange, // Offset - onActiveOffset, + onActiveInfo, placement, // Native @@ -173,9 +175,9 @@ function RangeSelector( }); // ====================== ActiveBar ======================= - const realPlacement = getRealPlacement(placement, rtl); - const offsetUnit = getoffsetUnit(realPlacement, rtl); - const placementRight = realPlacement?.toLowerCase().endsWith('right'); + // const realPlacement = getRealPlacement(placement, rtl); + // const offsetUnit = getOffsetUnit(realPlacement, rtl); + // const placementRight = realPlacement?.toLowerCase().endsWith('right'); const [activeBarStyle, setActiveBarStyle] = React.useState({ position: 'absolute', width: 0, @@ -184,15 +186,26 @@ function RangeSelector( const syncActiveOffset = useEvent(() => { const input = getInput(activeIndex); if (input) { - const { offsetWidth, offsetLeft, offsetParent } = input.nativeElement; - const parentWidth = (offsetParent as HTMLElement)?.offsetWidth || 0; - const activeOffset = placementRight ? (parentWidth - offsetWidth - offsetLeft) : offsetLeft; - setActiveBarStyle(({ insetInlineStart, insetInlineEnd, ...rest }) => ({ - ...rest, + const { offsetWidth, offsetParent } = input.nativeElement; + // const parentWidth = (offsetParent as HTMLElement)?.offsetWidth || 0; + // const activeOffset = placementRight ? parentWidth - offsetWidth - offsetLeft : offsetLeft; + // setActiveBarStyle(({ insetInlineStart, insetInlineEnd, ...rest }) => ({ + // ...rest, + // width: offsetWidth, + // [offsetUnit]: activeOffset + // })); + // onActiveOffset(activeOffset); + + const inputRect = input.nativeElement.getBoundingClientRect(); + const parentRect = rootRef.current.getBoundingClientRect(); + + const rectOffset = inputRect.left - parentRect.left; + setActiveBarStyle((ori) => ({ + ...ori, width: offsetWidth, - [offsetUnit]: activeOffset + left: rectOffset, })); - onActiveOffset(activeOffset); + onActiveInfo([inputRect.left, inputRect.right, parentRect.width]); } }); diff --git a/src/utils/uiUtil.ts b/src/utils/uiUtil.ts index 00f92d46d..508a526e6 100644 --- a/src/utils/uiUtil.ts +++ b/src/utils/uiUtil.ts @@ -195,12 +195,12 @@ export function getRealPlacement(placement: string, rtl: boolean) { return rtl ? 'bottomRight' : 'bottomLeft'; } -export function getoffsetUnit(placement: string, rtl: boolean) { +export function getOffsetUnit(placement: string, rtl: boolean) { const realPlacement = getRealPlacement(placement, rtl); const placementRight = realPlacement?.toLowerCase().endsWith('right'); let offsetUnit = placementRight ? 'insetInlineEnd' : 'insetInlineStart'; if (rtl) { - offsetUnit = ['insetInlineStart', 'insetInlineEnd'].find(unit => unit !== offsetUnit); + offsetUnit = ['insetInlineStart', 'insetInlineEnd'].find((unit) => unit !== offsetUnit); } return offsetUnit; -} \ No newline at end of file +} From 433adacae0595b0cbaae58f6e52320fb1bbcae1b 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: Sat, 8 Feb 2025 18:15:28 +0800 Subject: [PATCH 2/4] test: update snapshot --- tests/__snapshots__/range.spec.tsx.snap | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/__snapshots__/range.spec.tsx.snap b/tests/__snapshots__/range.spec.tsx.snap index 86b7282fe..6e3dc963c 100644 --- a/tests/__snapshots__/range.spec.tsx.snap +++ b/tests/__snapshots__/range.spec.tsx.snap @@ -89,7 +89,7 @@ exports[`Picker.Range onPanelChange is array args should render correctly in pla
@@ -170,7 +170,7 @@ exports[`Picker.Range panelRender 1`] = `
@@ -184,7 +184,7 @@ exports[`Picker.Range panelRender 1`] = ` >
@@ -250,7 +250,7 @@ exports[`Picker.Range use dateRender and monthCellRender in date range picker 1` >
@@ -1307,7 +1307,7 @@ exports[`Picker.Range use dateRender and monthCellRender in month range picker 1 >
Date: Sat, 8 Feb 2025 18:56:01 +0800 Subject: [PATCH 3/4] chore: clean up --- src/PickerInput/Popup/index.tsx | 22 +-- src/PickerInput/RangePicker.tsx | 2 - src/PickerInput/Selector/RangeSelector.tsx | 16 +- src/utils/uiUtil.ts | 206 --------------------- tests/__snapshots__/range.spec.tsx.snap | 4 +- tests/range.spec.tsx | 64 ++++++- 6 files changed, 67 insertions(+), 247 deletions(-) delete mode 100644 src/utils/uiUtil.ts diff --git a/src/PickerInput/Popup/index.tsx b/src/PickerInput/Popup/index.tsx index 0b77cf054..4b6c26fea 100644 --- a/src/PickerInput/Popup/index.tsx +++ b/src/PickerInput/Popup/index.tsx @@ -8,7 +8,6 @@ import type { ValueDate, } from '../../interface'; import { toArray } from '../../utils/miscUtil'; -import { getRealPlacement, getOffsetUnit } from '../../utils/uiUtil'; import PickerContext from '../context'; import Footer, { type FooterProps } from './Footer'; import PopupPanel, { type PopupPanelProps } from './PopupPanel'; @@ -33,7 +32,6 @@ export interface PopupProps(props: PopupProps(props: PopupProps(0); const onResize: ResizeObserverProps['onResize'] = (info) => { - if (info.offsetWidth) { - setContainerWidth(info.offsetWidth); + if (info.width) { + setContainerWidth(info.width); } }; @@ -122,15 +119,12 @@ export default function Popup(props: PopupProps( prefixCls, styles, classNames, - placement, // Value defaultValue, @@ -578,7 +577,6 @@ function RangePicker( range multiplePanel={multiplePanel} activeInfo={activeInfo} - placement={placement} // Disabled disabledDate={mergedDisabledDate} // Focus diff --git a/src/PickerInput/Selector/RangeSelector.tsx b/src/PickerInput/Selector/RangeSelector.tsx index d0e9e3996..36328733d 100644 --- a/src/PickerInput/Selector/RangeSelector.tsx +++ b/src/PickerInput/Selector/RangeSelector.tsx @@ -8,7 +8,6 @@ import useInputProps from './hooks/useInputProps'; import useRootProps from './hooks/useRootProps'; import Icon, { ClearIcon } from './Icon'; import Input, { type InputRef } from './Input'; -// import { getOffsetUnit, getRealPlacement } from '../../utils/uiUtil'; export type SelectorIdType = | string @@ -175,9 +174,6 @@ function RangeSelector( }); // ====================== ActiveBar ======================= - // const realPlacement = getRealPlacement(placement, rtl); - // const offsetUnit = getOffsetUnit(realPlacement, rtl); - // const placementRight = realPlacement?.toLowerCase().endsWith('right'); const [activeBarStyle, setActiveBarStyle] = React.useState({ position: 'absolute', width: 0, @@ -186,23 +182,13 @@ function RangeSelector( const syncActiveOffset = useEvent(() => { const input = getInput(activeIndex); if (input) { - const { offsetWidth, offsetParent } = input.nativeElement; - // const parentWidth = (offsetParent as HTMLElement)?.offsetWidth || 0; - // const activeOffset = placementRight ? parentWidth - offsetWidth - offsetLeft : offsetLeft; - // setActiveBarStyle(({ insetInlineStart, insetInlineEnd, ...rest }) => ({ - // ...rest, - // width: offsetWidth, - // [offsetUnit]: activeOffset - // })); - // onActiveOffset(activeOffset); - const inputRect = input.nativeElement.getBoundingClientRect(); const parentRect = rootRef.current.getBoundingClientRect(); const rectOffset = inputRect.left - parentRect.left; setActiveBarStyle((ori) => ({ ...ori, - width: offsetWidth, + width: inputRect.width, left: rectOffset, })); onActiveInfo([inputRect.left, inputRect.right, parentRect.width]); diff --git a/src/utils/uiUtil.ts b/src/utils/uiUtil.ts deleted file mode 100644 index 508a526e6..000000000 --- a/src/utils/uiUtil.ts +++ /dev/null @@ -1,206 +0,0 @@ -import isVisible from 'rc-util/lib/Dom/isVisible'; -import KeyCode from 'rc-util/lib/KeyCode'; -import raf from 'rc-util/lib/raf'; -import type { CustomFormat, PickerMode } from '../interface'; - -const scrollIds = new Map(); - -/** Trigger when element is visible in view */ -export function waitElementReady(element: HTMLElement, callback: () => void): () => void { - let id: number; - - function tryOrNextFrame() { - if (isVisible(element)) { - callback(); - } else { - id = raf(() => { - tryOrNextFrame(); - }); - } - } - - tryOrNextFrame(); - - return () => { - raf.cancel(id); - }; -} - -/* eslint-disable no-param-reassign */ -export function scrollTo(element: HTMLElement, to: number, duration: number) { - if (scrollIds.get(element)) { - cancelAnimationFrame(scrollIds.get(element)!); - } - - // jump to target if duration zero - if (duration <= 0) { - scrollIds.set( - element, - requestAnimationFrame(() => { - element.scrollTop = to; - }), - ); - - return; - } - const difference = to - element.scrollTop; - const perTick = (difference / duration) * 10; - - scrollIds.set( - element, - requestAnimationFrame(() => { - element.scrollTop += perTick; - if (element.scrollTop !== to) { - scrollTo(element, to, duration - 10); - } - }), - ); -} -/* eslint-enable */ - -export type KeyboardConfig = { - onLeftRight?: ((diff: number) => void) | null; - onCtrlLeftRight?: ((diff: number) => void) | null; - onUpDown?: ((diff: number) => void) | null; - onPageUpDown?: ((diff: number) => void) | null; - onEnter?: (() => void) | null; -}; -export function createKeyDownHandler( - event: React.KeyboardEvent, - { onLeftRight, onCtrlLeftRight, onUpDown, onPageUpDown, onEnter }: KeyboardConfig, -): boolean { - const { which, ctrlKey, metaKey } = event; - - switch (which) { - case KeyCode.LEFT: - if (ctrlKey || metaKey) { - if (onCtrlLeftRight) { - onCtrlLeftRight(-1); - return true; - } - } else if (onLeftRight) { - onLeftRight(-1); - return true; - } - /* istanbul ignore next */ - break; - - case KeyCode.RIGHT: - if (ctrlKey || metaKey) { - if (onCtrlLeftRight) { - onCtrlLeftRight(1); - return true; - } - } else if (onLeftRight) { - onLeftRight(1); - return true; - } - /* istanbul ignore next */ - break; - - case KeyCode.UP: - if (onUpDown) { - onUpDown(-1); - return true; - } - /* istanbul ignore next */ - break; - - case KeyCode.DOWN: - if (onUpDown) { - onUpDown(1); - return true; - } - /* istanbul ignore next */ - break; - - case KeyCode.PAGE_UP: - if (onPageUpDown) { - onPageUpDown(-1); - return true; - } - /* istanbul ignore next */ - break; - - case KeyCode.PAGE_DOWN: - if (onPageUpDown) { - onPageUpDown(1); - return true; - } - /* istanbul ignore next */ - break; - - case KeyCode.ENTER: - if (onEnter) { - onEnter(); - return true; - } - /* istanbul ignore next */ - break; - } - - return false; -} - -// ===================== Format ===================== -export function getDefaultFormat( - format: string | CustomFormat | (string | CustomFormat)[] | undefined, - picker: PickerMode | undefined, - showTime: boolean | object | undefined, - use12Hours: boolean | undefined, -) { - let mergedFormat = format; - if (!mergedFormat) { - switch (picker) { - case 'time': - mergedFormat = use12Hours ? 'hh:mm:ss a' : 'HH:mm:ss'; - break; - - case 'week': - mergedFormat = 'gggg-wo'; - break; - - case 'month': - mergedFormat = 'YYYY-MM'; - break; - - case 'quarter': - mergedFormat = 'YYYY-[Q]Q'; - break; - - case 'year': - mergedFormat = 'YYYY'; - break; - - default: - mergedFormat = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'; - } - } - - return mergedFormat; -} - -// ====================== Mode ====================== -export function elementsContains( - elements: (HTMLElement | undefined | null)[], - target: HTMLElement, -) { - return elements.some((ele) => ele && ele.contains(target)); -} - -export function getRealPlacement(placement: string, rtl: boolean) { - if (placement !== undefined) { - return placement; - } - return rtl ? 'bottomRight' : 'bottomLeft'; -} - -export function getOffsetUnit(placement: string, rtl: boolean) { - const realPlacement = getRealPlacement(placement, rtl); - const placementRight = realPlacement?.toLowerCase().endsWith('right'); - let offsetUnit = placementRight ? 'insetInlineEnd' : 'insetInlineStart'; - if (rtl) { - offsetUnit = ['insetInlineStart', 'insetInlineEnd'].find((unit) => unit !== offsetUnit); - } - return offsetUnit; -} diff --git a/tests/__snapshots__/range.spec.tsx.snap b/tests/__snapshots__/range.spec.tsx.snap index 6e3dc963c..7d51c4223 100644 --- a/tests/__snapshots__/range.spec.tsx.snap +++ b/tests/__snapshots__/range.spec.tsx.snap @@ -236,7 +236,7 @@ exports[`Picker.Range use dateRender and monthCellRender in date range picker 1`
@@ -1293,7 +1293,7 @@ exports[`Picker.Range use dateRender and monthCellRender in month range picker 1
diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index 791a7e501..79bef79d5 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -10,6 +10,14 @@ import { resetWarned } from 'rc-util/lib/warning'; import React from 'react'; import type { PickerRef, RangePickerProps } from '../src'; import type { PickerMode } from '../src/interface'; +import { _rs } from 'rc-resize-observer'; + +const triggerResize = (target: Element) => { + act(() => { + _rs([{ target } as ResizeObserverEntry]); + }); +}; + import { DayRangePicker, clearValue, @@ -1788,20 +1796,52 @@ describe('Picker.Range', () => { mock.mockRestore(); }); - it('panel should be stable: arrow right and panel right', () => { + it('panel should be stable: arrow right and panel right', async () => { + const mockMatch = (element: HTMLElement, values: Record) => { + const keys = Object.keys(values); + for (const key of keys) { + if (element.classList.contains(key)) { + return values[key]; + } + } + return 0; + }; + + const getWidth = (element: HTMLElement) => { + return mockMatch(element, { + 'rc-picker-range': 200, + 'rc-picker-panel-container': 50, + 'rc-picker-range-wrapper': 200, + 'rc-picker-input': 100, + 'rc-picker-range-arrow': 0, + }); + }; + const getLeft = (element: HTMLElement) => { + return mockMatch(element, { + 'rc-picker-range-wrapper': 0, + 'rc-picker-input': 100, + }); + }; + const mock = spyElementPrototypes(HTMLElement, { + getBoundingClientRect() { + const left = getLeft(this); + const width = getWidth(this); + + return { + width, + left, + right: left + width, + }; + }, offsetWidth: { get() { - if (this.className.includes('rc-picker-range-wrapper')) { - return 200; - } + return getWidth(this); }, }, offsetLeft: { get() { - if (this.className.includes('rc-picker-input')) { - return 100; - } + return getLeft(this); }, }, }); @@ -1814,8 +1854,16 @@ describe('Picker.Range', () => { />, ); openPicker(container, 1); + + triggerResize(container.querySelector('.rc-picker-range')); + triggerResize(document.body.querySelector('.rc-picker-panel-container')); + + await act(async () => { + await Promise.resolve(); + }); + expect(document.querySelector('.rc-picker-panel-container')).toHaveStyle({ - marginLeft: '100px', + marginLeft: '50px', }); mock.mockRestore(); }); From fc3d53a98e1d9f2243bd0302c4a6c75cb85e325e 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: Sun, 9 Feb 2025 13:52:10 +0800 Subject: [PATCH 4/4] chore: fix build --- src/utils/uiUtil.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/utils/uiUtil.ts diff --git a/src/utils/uiUtil.ts b/src/utils/uiUtil.ts new file mode 100644 index 000000000..5eb0c3445 --- /dev/null +++ b/src/utils/uiUtil.ts @@ -0,0 +1,7 @@ +// ====================== Mode ====================== +export function getRealPlacement(placement: string, rtl: boolean) { + if (placement !== undefined) { + return placement; + } + return rtl ? 'bottomRight' : 'bottomLeft'; +}