Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions docs-site/src/components/Examples/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
60 changes: 60 additions & 0 deletions docs-site/src/examples/ts/popperTargetRef.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement | null>;
};

const CustomInputWithButton: React.FC<CustomInputWithButtonProps> = ({
value,
onClick,
buttonRef,
}) => (
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "8px",
border: "1px solid #ccc",
borderRadius: "4px",
width: "250px",
}}
>
<span style={{ flex: 1 }}>{value || "Select a date"}</span>
<button
ref={buttonRef}
onClick={onClick}
type="button"
style={{
padding: "4px 8px",
cursor: "pointer",
}}
>
📅
</button>
</div>
);

const PopperTargetRef = () => {
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
const buttonRef = useRef<HTMLButtonElement>(null);

return (
<DatePicker
selected={selectedDate}
onChange={setSelectedDate}
customInput={<CustomInputWithButton buttonRef={buttonRef} />}
popperTargetRef={buttonRef}
/>
);
};

render(PopperTargetRef);
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export type DatePickerProps = OmitUnion<
onKeyDown?: (event: React.KeyboardEvent<HTMLElement>) => void;
popperClassName?: PopperComponentProps["className"];
showPopperArrow?: PopperComponentProps["showArrow"];
popperTargetRef?: React.RefObject<HTMLElement | null>;
open?: boolean;
disabled?: boolean;
readOnly?: boolean;
Expand Down
13 changes: 12 additions & 1 deletion src/popper_component.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -28,6 +28,7 @@ interface PopperComponentProps
popperOnKeyDown: React.KeyboardEventHandler<HTMLDivElement>;
showArrow?: boolean;
portalId?: PortalProps["portalId"];
popperTargetRef?: React.RefObject<HTMLElement | null>;
monthHeaderPosition?: "top" | "middle" | "bottom";
}

Expand All @@ -45,9 +46,19 @@ export const PopperComponent: React.FC<PopperComponentProps> = (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) {
Expand Down
46 changes: 46 additions & 0 deletions src/test/datepicker_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLButtonElement>();

/* 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<HTMLButtonElement | null>;
}> = (props) => {
return (
<div style={{ display: "flex", width: "300px" }}>
<input value={props.value || ""} readOnly onClick={props.onClick} />
<button
ref={props.buttonRef}
onClick={props.onClick}
data-testid="custom-button"
>
Open
</button>
</div>
);
};
/* eslint-enable react-hooks/refs */

const { container } = render(
<DatePicker
customInput={<CustomInputWithButton buttonRef={buttonRef} />}
popperTargetRef={buttonRef}
/>,
);

const button = safeQuerySelector<HTMLButtonElement>(
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(<DatePicker />);

Expand Down
Loading