Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/PickerInput/Popup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default function Popup<DateType extends object = any>(props: PopupProps<D
onSubmit,
} = props;

const { prefixCls } = React.useContext(PickerContext);
const { prefixCls, alignedPlacement } = React.useContext(PickerContext);
const panelPrefixCls = `${prefixCls}-panel`;

const rtl = direction === 'rtl';
Expand Down Expand Up @@ -213,7 +213,7 @@ export default function Popup<DateType extends object = any>(props: PopupProps<D
);

if (range) {
const realPlacement = getRealPlacement(placement, rtl);
const realPlacement = getRealPlacement(alignedPlacement || placement, rtl);
const offsetUnit = getoffsetUnit(realPlacement, rtl);
renderNode = (
<div
Expand Down
15 changes: 14 additions & 1 deletion src/PickerInput/RangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,9 @@ function RangePicker<DateType extends object = any>(
onKeyDown?.(event, preventDefault);
};

// ======================= popup align =======================
const [alignedPlacement, setAlignedPlacement] = React.useState<string>();

// ======================= Context ========================
const context = React.useMemo(
() => ({
Expand All @@ -674,8 +677,18 @@ function RangePicker<DateType extends object = any>(
generateConfig,
button: components.button,
input: components.input,
alignedPlacement,
setAlignedPlacement,
}),
[prefixCls, locale, generateConfig, components.button, components.input],
[
prefixCls,
locale,
generateConfig,
components.button,
components.input,
alignedPlacement,
setAlignedPlacement,
],
);

// ======================== Effect ========================
Expand Down
12 changes: 6 additions & 6 deletions src/PickerInput/Selector/RangeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ function RangeSelector<DateType extends object = any>(
const rtl = direction === 'rtl';

// ======================== Prefix ========================
const { prefixCls } = React.useContext(PickerContext);
const { prefixCls, alignedPlacement } = React.useContext(PickerContext);

// ========================== Id ==========================
const ids = React.useMemo(() => {
Expand Down Expand Up @@ -173,7 +173,7 @@ function RangeSelector<DateType extends object = any>(
});

// ====================== ActiveBar =======================
const realPlacement = getRealPlacement(placement, rtl);
const realPlacement = getRealPlacement(alignedPlacement || placement, rtl);
const offsetUnit = getoffsetUnit(realPlacement, rtl);
const placementRight = realPlacement?.toLowerCase().endsWith('right');
const [activeBarStyle, setActiveBarStyle] = React.useState<React.CSSProperties>({
Expand All @@ -186,9 +186,9 @@ function RangeSelector<DateType extends object = any>(
if (input) {
const { offsetWidth, offsetLeft, offsetParent } = input.nativeElement;
const parentWidth = (offsetParent as HTMLElement)?.offsetWidth || 0;
const activeOffset = placementRight ? (parentWidth - offsetWidth - offsetLeft) : offsetLeft;
setActiveBarStyle((ori) => ({
...ori,
const activeOffset = placementRight ? parentWidth - offsetWidth - offsetLeft : offsetLeft;
setActiveBarStyle(({ position }) => ({
position,
width: offsetWidth,
[offsetUnit]: activeOffset,
}));
Expand All @@ -198,7 +198,7 @@ function RangeSelector<DateType extends object = any>(

React.useEffect(() => {
syncActiveOffset();
}, [activeIndex]);
}, [activeIndex, alignedPlacement]);

// ======================== Clear =========================
const showClear = clearIcon && ((value[0] && !disabled[0]) || (value[1] && !disabled[1]));
Expand Down
4 changes: 3 additions & 1 deletion src/PickerInput/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export interface PickerContextProps<DateType = any> {
/** Customize button component */
button?: Components['button'];
input?: Components['input'];

/** trigger will change placement while aligining */
alignedPlacement?: string;
setAlignedPlacement?: React.Dispatch<React.SetStateAction<string>>;
}

const PickerContext = React.createContext<PickerContextProps>(null!);
Expand Down
15 changes: 14 additions & 1 deletion src/PickerTrigger/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function PickerTrigger({
visible,
onClose,
}: PickerTriggerProps) {
const { prefixCls } = React.useContext(PickerContext);
const { prefixCls, setAlignedPlacement } = React.useContext(PickerContext);
const dropdownPrefixCls = `${prefixCls}-dropdown`;

const realPlacement = getRealPlacement(placement, direction === 'rtl');
Expand All @@ -100,6 +100,19 @@ function PickerTrigger({
popupStyle={popupStyle}
stretch="minWidth"
getPopupContainer={getPopupContainer}
onPopupAlign={(_, align) => {
if (!setAlignedPlacement) return;

const matchedKey = Object.keys(BUILT_IN_PLACEMENTS).find(
(key) =>
BUILT_IN_PLACEMENTS[key].points[0] === align.points[0] &&
BUILT_IN_PLACEMENTS[key].points[1] === align.points[1]
);

if (matchedKey) {
setAlignedPlacement(matchedKey);
}
}}
onPopupVisibleChange={(nextVisible) => {
if (!nextVisible) {
onClose();
Expand Down
142 changes: 142 additions & 0 deletions tests/range-align.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { act, cleanup, render } from '@testing-library/react';
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
import React from 'react';
import { DayRangePicker } from './util/commonUtil';

describe('the popup arrow should be placed in the correct position.', () => {
let rangeRect = { x: 0, y: 0, width: 0, height: 0 };

beforeEach(() => {
rangeRect = {
x: 0,
y: 0,
width: 200,
height: 100,
};

document.documentElement.scrollLeft = 0;
jest.useFakeTimers();
});

beforeAll(() => {
jest.spyOn(document.documentElement, 'scrollWidth', 'get').mockReturnValue(1000);

// Viewport size
spyElementPrototypes(HTMLElement, {
clientWidth: {
get: () => 400,
},
clientHeight: {
get: () => 400,
},
});

// Popup size
spyElementPrototypes(HTMLDivElement, {
getBoundingClientRect() {
if (this.className.includes('rc-picker-dropdown')) {
return {
x: 0,
y: 0,
width: 300,
height: 100,
};
}
if (this.className.includes('rc-picker-range')) {
return rangeRect;
}
},
offsetWidth: {
get() {
if (this.className.includes('rc-picker-range-wrapper')) {
return rangeRect.width;
}
if (this.className.includes('rc-picker-range-arrow')) {
return 10;
}
if (this.className.includes('rc-picker-input')) {
return 100;
}
if (this.className.includes('rc-picker-dropdown')) {
return 300;
}
},
},
offsetLeft: {
get() {
if (this.className.includes('rc-picker-input')) {
return 0;
}
},
},
});
spyElementPrototypes(HTMLElement, {
offsetParent: {
get: () => document.body,
},
offsetWidth: {
get() {
if (this.tagName === 'BODY') {
return 200;
}
},
},
});
});

afterEach(() => {
cleanup();
jest.useRealTimers();
});

it('the arrow should be set to `inset-inline-start` when the popup is aligned to `bottomLeft`.', async () => {
render(<DayRangePicker open />);

await act(async () => {
jest.runAllTimers();

await Promise.resolve();
});
expect(document.querySelector('.rc-picker-range-arrow')).toHaveStyle({
'inset-inline-start': '0',
});
});

it('the arrow should be set to `inset-inline-end` when the popup is aligned to `bottomRight`.', async () => {
const mock = spyElementPrototypes(HTMLDivElement, {
getBoundingClientRect() {
if (this.className.includes('rc-picker-dropdown')) {
return {
x: 0,
y: 0,
width: 300,
height: 100,
};
}
if (this.className.includes('rc-picker-range')) {
return {
...rangeRect,
x: 300,
};
}
},
});

render(<DayRangePicker open />);

await act(async () => {
jest.runAllTimers();

await Promise.resolve();
});
expect(document.querySelector('.rc-picker-range-arrow')).toHaveStyle({
'inset-inline-end': '100px',
});

expect(document.querySelector('.rc-picker-active-bar')).toHaveStyle({
'inset-inline-end': '100px',
});

mock.mockRestore();
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

建议扩展测试用例覆盖范围

当前测试用例仅覆盖了 bottomLeftbottomRight 两种情况。建议增加以下测试场景:

  1. 窗口大小变化时的箭头位置
  2. 滚动时的箭头位置
  3. 边界情况(如容器太小)
  4. 动态内容变化时的位置更新

示例测试用例:

it('应该在窗口调整大小时正确更新箭头位置', async () => {
  render(<DayRangePicker open />);
  
  // 模拟窗口调整
  act(() => {
    global.innerWidth = 800;
    global.dispatchEvent(new Event('resize'));
  });
  
  await act(async () => {
    jest.runAllTimers();
  });
  
  // 验证箭头位置是否更新
});

it('应该在容器空间不足时正确处理箭头位置', async () => {
  // 模拟一个很小的容器
  rangeRect.width = 50;
  
  render(<DayRangePicker open />);
  
  await act(async () => {
    jest.runAllTimers();
  });
  
  // 验证箭头位置处理
});