diff --git a/docs-site/src/components/Examples/config.tsx b/docs-site/src/components/Examples/config.tsx index 914aba936..10326ba58 100644 --- a/docs-site/src/components/Examples/config.tsx +++ b/docs-site/src/components/Examples/config.tsx @@ -15,6 +15,7 @@ import CloseOnScroll from "../../examples/ts/closeOnScroll?raw"; import CloseOnScrollCallback from "../../examples/ts/closeOnScrollCallback?raw"; import ConfigureFloatingUI from "../../examples/ts/configureFloatingUI?raw"; import CustomInput from "../../examples/ts/customInput?raw"; +import PopperTargetRef from "../../examples/ts/popperTargetRef?raw"; import RenderCustomHeader from "../../examples/ts/renderCustomHeader?raw"; import RenderCustomHeaderTwoMonths from "../../examples/ts/renderCustomHeaderTwoMonths?raw"; import RenderCustomDayName from "../../examples/ts/renderCustomDayName?raw"; @@ -178,6 +179,12 @@ export const EXAMPLE_CONFIG: IExampleConfig[] = [ title: "Custom input", component: CustomInput, }, + { + title: "Custom input with popper positioning", + component: PopperTargetRef, + description: + "Use popperTargetRef to position the calendar relative to a specific element within your custom input, rather than the wrapper div.", + }, { title: "Custom header", component: RenderCustomHeader, diff --git a/docs-site/src/examples/ts/popperTargetRef.tsx b/docs-site/src/examples/ts/popperTargetRef.tsx new file mode 100644 index 000000000..da43597ca --- /dev/null +++ b/docs-site/src/examples/ts/popperTargetRef.tsx @@ -0,0 +1,60 @@ +/** + * When using a customInput with multiple elements (like a text display and a button), + * you can use popperTargetRef to control which element the calendar positions relative to. + * + * Without popperTargetRef, the calendar positions relative to the wrapper div. + * With popperTargetRef, it positions relative to the specific element you choose. + */ + +type CustomInputWithButtonProps = { + value?: string; + onClick?: () => void; + buttonRef?: React.RefObject; +}; + +const CustomInputWithButton: React.FC = ({ + value, + onClick, + buttonRef, +}) => ( +
+ {value || "Select a date"} + +
+); + +const PopperTargetRef = () => { + const [selectedDate, setSelectedDate] = useState(new Date()); + const buttonRef = useRef(null); + + return ( + } + popperTargetRef={buttonRef} + /> + ); +}; + +render(PopperTargetRef); diff --git a/src/index.tsx b/src/index.tsx index 43f2fbbb5..86d242f72 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -149,6 +149,7 @@ export type DatePickerProps = OmitUnion< onKeyDown?: (event: React.KeyboardEvent) => void; popperClassName?: PopperComponentProps["className"]; showPopperArrow?: PopperComponentProps["showArrow"]; + popperTargetRef?: React.RefObject; open?: boolean; disabled?: boolean; readOnly?: boolean; diff --git a/src/popper_component.tsx b/src/popper_component.tsx index 36c769bec..5a16a7b73 100644 --- a/src/popper_component.tsx +++ b/src/popper_component.tsx @@ -1,6 +1,6 @@ import { FloatingArrow } from "@floating-ui/react"; import { clsx } from "clsx"; -import React, { createElement } from "react"; +import React, { createElement, useEffect } from "react"; import Portal from "./portal"; import TabLoop from "./tab_loop"; @@ -28,6 +28,7 @@ interface PopperComponentProps popperOnKeyDown: React.KeyboardEventHandler; showArrow?: boolean; portalId?: PortalProps["portalId"]; + popperTargetRef?: React.RefObject; monthHeaderPosition?: "top" | "middle" | "bottom"; } @@ -45,9 +46,19 @@ export const PopperComponent: React.FC = (props) => { portalHost, popperProps, showArrow, + popperTargetRef, monthHeaderPosition, } = props; + // When a custom popperTargetRef is provided, use it as the position reference + // This allows the popper to be positioned relative to a specific element + // within the custom input, rather than the wrapper div + useEffect(() => { + if (popperTargetRef?.current) { + popperProps.refs.setPositionReference(popperTargetRef.current); + } + }, [popperTargetRef, popperProps.refs]); + let popper: React.ReactElement | undefined = undefined; if (!hidePopper) { diff --git a/src/test/datepicker_test.test.tsx b/src/test/datepicker_test.test.tsx index ab65e5e56..0dd3c9a12 100644 --- a/src/test/datepicker_test.test.tsx +++ b/src/test/datepicker_test.test.tsx @@ -293,6 +293,52 @@ describe("DatePicker", () => { expect(popper[0]?.classList.contains("some-class-name")).toBe(true); }); + it("should use popperTargetRef for positioning when provided", () => { + const buttonRef = React.createRef(); + + /* eslint-disable react-hooks/refs -- passing ref object and callbacks as props, not accessing .current */ + // Custom input component that exposes a button ref separately from the main input ref + const CustomInputWithButton: React.FC<{ + value?: string; + onClick?: () => void; + buttonRef?: React.RefObject; + }> = (props) => { + return ( +
+ + +
+ ); + }; + /* eslint-enable react-hooks/refs */ + + const { container } = render( + } + popperTargetRef={buttonRef} + />, + ); + + const button = safeQuerySelector( + container, + '[data-testid="custom-button"]', + ); + fireEvent.click(button); + + // Verify the popper is shown + const popper = container.querySelector(".react-datepicker-popper"); + expect(popper).not.toBeNull(); + + // Verify the button ref was properly set + expect(buttonRef.current).toBe(button); + }); + it("should show the calendar when clicking on the date input", () => { const { container } = render();