Skip to content

Commit ff23ce8

Browse files
Merge pull request #6102 from Hacker0x01/feature/popper-target-ref
feat: add popperTargetRef prop for custom popper positioning
2 parents 0e13929 + c7076d3 commit ff23ce8

File tree

5 files changed

+126
-1
lines changed

5 files changed

+126
-1
lines changed

docs-site/src/components/Examples/config.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import CloseOnScroll from "../../examples/ts/closeOnScroll?raw";
1515
import CloseOnScrollCallback from "../../examples/ts/closeOnScrollCallback?raw";
1616
import ConfigureFloatingUI from "../../examples/ts/configureFloatingUI?raw";
1717
import CustomInput from "../../examples/ts/customInput?raw";
18+
import PopperTargetRef from "../../examples/ts/popperTargetRef?raw";
1819
import RenderCustomHeader from "../../examples/ts/renderCustomHeader?raw";
1920
import RenderCustomHeaderTwoMonths from "../../examples/ts/renderCustomHeaderTwoMonths?raw";
2021
import RenderCustomDayName from "../../examples/ts/renderCustomDayName?raw";
@@ -178,6 +179,12 @@ export const EXAMPLE_CONFIG: IExampleConfig[] = [
178179
title: "Custom input",
179180
component: CustomInput,
180181
},
182+
{
183+
title: "Custom input with popper positioning",
184+
component: PopperTargetRef,
185+
description:
186+
"Use popperTargetRef to position the calendar relative to a specific element within your custom input, rather than the wrapper div.",
187+
},
181188
{
182189
title: "Custom header",
183190
component: RenderCustomHeader,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* When using a customInput with multiple elements (like a text display and a button),
3+
* you can use popperTargetRef to control which element the calendar positions relative to.
4+
*
5+
* Without popperTargetRef, the calendar positions relative to the wrapper div.
6+
* With popperTargetRef, it positions relative to the specific element you choose.
7+
*/
8+
9+
type CustomInputWithButtonProps = {
10+
value?: string;
11+
onClick?: () => void;
12+
buttonRef?: React.RefObject<HTMLButtonElement | null>;
13+
};
14+
15+
const CustomInputWithButton: React.FC<CustomInputWithButtonProps> = ({
16+
value,
17+
onClick,
18+
buttonRef,
19+
}) => (
20+
<div
21+
style={{
22+
display: "flex",
23+
alignItems: "center",
24+
gap: "8px",
25+
padding: "8px",
26+
border: "1px solid #ccc",
27+
borderRadius: "4px",
28+
width: "250px",
29+
}}
30+
>
31+
<span style={{ flex: 1 }}>{value || "Select a date"}</span>
32+
<button
33+
ref={buttonRef}
34+
onClick={onClick}
35+
type="button"
36+
style={{
37+
padding: "4px 8px",
38+
cursor: "pointer",
39+
}}
40+
>
41+
📅
42+
</button>
43+
</div>
44+
);
45+
46+
const PopperTargetRef = () => {
47+
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
48+
const buttonRef = useRef<HTMLButtonElement>(null);
49+
50+
return (
51+
<DatePicker
52+
selected={selectedDate}
53+
onChange={setSelectedDate}
54+
customInput={<CustomInputWithButton buttonRef={buttonRef} />}
55+
popperTargetRef={buttonRef}
56+
/>
57+
);
58+
};
59+
60+
render(PopperTargetRef);

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export type DatePickerProps = OmitUnion<
149149
onKeyDown?: (event: React.KeyboardEvent<HTMLElement>) => void;
150150
popperClassName?: PopperComponentProps["className"];
151151
showPopperArrow?: PopperComponentProps["showArrow"];
152+
popperTargetRef?: React.RefObject<HTMLElement | null>;
152153
open?: boolean;
153154
disabled?: boolean;
154155
readOnly?: boolean;

src/popper_component.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FloatingArrow } from "@floating-ui/react";
22
import { clsx } from "clsx";
3-
import React, { createElement } from "react";
3+
import React, { createElement, useEffect } from "react";
44

55
import Portal from "./portal";
66
import TabLoop from "./tab_loop";
@@ -28,6 +28,7 @@ interface PopperComponentProps
2828
popperOnKeyDown: React.KeyboardEventHandler<HTMLDivElement>;
2929
showArrow?: boolean;
3030
portalId?: PortalProps["portalId"];
31+
popperTargetRef?: React.RefObject<HTMLElement | null>;
3132
monthHeaderPosition?: "top" | "middle" | "bottom";
3233
}
3334

@@ -45,9 +46,19 @@ export const PopperComponent: React.FC<PopperComponentProps> = (props) => {
4546
portalHost,
4647
popperProps,
4748
showArrow,
49+
popperTargetRef,
4850
monthHeaderPosition,
4951
} = props;
5052

53+
// When a custom popperTargetRef is provided, use it as the position reference
54+
// This allows the popper to be positioned relative to a specific element
55+
// within the custom input, rather than the wrapper div
56+
useEffect(() => {
57+
if (popperTargetRef?.current) {
58+
popperProps.refs.setPositionReference(popperTargetRef.current);
59+
}
60+
}, [popperTargetRef, popperProps.refs]);
61+
5162
let popper: React.ReactElement | undefined = undefined;
5263

5364
if (!hidePopper) {

src/test/datepicker_test.test.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,52 @@ describe("DatePicker", () => {
293293
expect(popper[0]?.classList.contains("some-class-name")).toBe(true);
294294
});
295295

296+
it("should use popperTargetRef for positioning when provided", () => {
297+
const buttonRef = React.createRef<HTMLButtonElement>();
298+
299+
/* eslint-disable react-hooks/refs -- passing ref object and callbacks as props, not accessing .current */
300+
// Custom input component that exposes a button ref separately from the main input ref
301+
const CustomInputWithButton: React.FC<{
302+
value?: string;
303+
onClick?: () => void;
304+
buttonRef?: React.RefObject<HTMLButtonElement | null>;
305+
}> = (props) => {
306+
return (
307+
<div style={{ display: "flex", width: "300px" }}>
308+
<input value={props.value || ""} readOnly onClick={props.onClick} />
309+
<button
310+
ref={props.buttonRef}
311+
onClick={props.onClick}
312+
data-testid="custom-button"
313+
>
314+
Open
315+
</button>
316+
</div>
317+
);
318+
};
319+
/* eslint-enable react-hooks/refs */
320+
321+
const { container } = render(
322+
<DatePicker
323+
customInput={<CustomInputWithButton buttonRef={buttonRef} />}
324+
popperTargetRef={buttonRef}
325+
/>,
326+
);
327+
328+
const button = safeQuerySelector<HTMLButtonElement>(
329+
container,
330+
'[data-testid="custom-button"]',
331+
);
332+
fireEvent.click(button);
333+
334+
// Verify the popper is shown
335+
const popper = container.querySelector(".react-datepicker-popper");
336+
expect(popper).not.toBeNull();
337+
338+
// Verify the button ref was properly set
339+
expect(buttonRef.current).toBe(button);
340+
});
341+
296342
it("should show the calendar when clicking on the date input", () => {
297343
const { container } = render(<DatePicker />);
298344

0 commit comments

Comments
 (0)