Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix:RangePicker the triangle sign was not positioned correctly in some cases #886

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
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
16 changes: 15 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 Expand Up @@ -739,6 +752,7 @@ function RangePicker<DateType extends object = any>(
// Visible
visible={mergedOpen}
onClose={onPopupClose}
alignedPlacement={alignedPlacement}
// Range
range
>
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
21 changes: 17 additions & 4 deletions src/PickerTrigger/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export type PickerTriggerProps = {
placement?: string;
builtinPlacements?: BuildInPlacements;
direction?: 'ltr' | 'rtl';

alignedPlacement: string;
// Visible
visible: boolean;
onClose: () => void;
Expand All @@ -72,15 +72,15 @@ function PickerTrigger({
placement,
builtinPlacements = BUILT_IN_PLACEMENTS,
direction,

alignedPlacement,
// Visible
visible,
onClose,
}: PickerTriggerProps) {
const { prefixCls } = React.useContext(PickerContext);
const { prefixCls, setAlignedPlacement } = React.useContext(PickerContext);
const dropdownPrefixCls = `${prefixCls}-dropdown`;

const realPlacement = getRealPlacement(placement, direction === 'rtl');
const realPlacement = getRealPlacement(alignedPlacement || placement, direction === 'rtl');

return (
<Trigger
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
137 changes: 135 additions & 2 deletions tests/new-range.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { act, fireEvent, render } from '@testing-library/react';
import dayjs, { type Dayjs } from 'dayjs';
import 'dayjs/locale/ar';
import { spyElementPrototype } from 'rc-util/lib/test/domHook';
import { spyElementPrototype, spyElementPrototypes } from 'rc-util/lib/test/domHook';
import { resetWarned } from 'rc-util/lib/warning';
import React from 'react';
import type { RangePickerProps } from '../src';
Expand All @@ -25,6 +25,7 @@ jest.mock('rc-util/lib/Dom/isVisible', () => {
});

describe('NewPicker.Range', () => {
let rangeRect = { x: 0, y: 0, width: 0, height: 0 };
beforeEach(() => {
resetWarned();
jest.useFakeTimers().setSystemTime(getDay('1990-09-03 00:00:00').valueOf());
Expand All @@ -36,13 +37,92 @@ describe('NewPicker.Range', () => {
return childList.indexOf(this) * 30;
},
});

// =============== handle trigger align ===============
rangeRect = {
x: 0,
y: 0,
width: 200,
height: 100,
};

document.documentElement.scrollLeft = 0;
});

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

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;
}
if (this.className.includes('rc-picker')) {
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;
}
},
},
});
});

describe('PickerValue', () => {
it('defaultPickerValue should reset every time when opened', () => {
const { container } = render(
Expand Down Expand Up @@ -1098,7 +1178,7 @@ describe('NewPicker.Range', () => {
it('pass tabIndex', () => {
const { container } = render(
<div>
<DayRangePicker tabIndex={-1}/>
<DayRangePicker tabIndex={-1} />
</div>,
);

Expand Down Expand Up @@ -1338,4 +1418,57 @@ describe('NewPicker.Range', () => {
}
expect(existed).toBeTruthy();
});

describe('pupop aligned position', () => {
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();
});
});
});