diff --git a/src/test/calendar_container.test.tsx b/src/test/calendar_container.test.tsx
new file mode 100644
index 000000000..a79514a2c
--- /dev/null
+++ b/src/test/calendar_container.test.tsx
@@ -0,0 +1,90 @@
+import { render } from "@testing-library/react";
+import React from "react";
+
+import CalendarContainer from "../calendar_container";
+
+describe("CalendarContainer", () => {
+ it("renders with default props", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog).toBeTruthy();
+ expect(dialog?.getAttribute("aria-label")).toBe("Choose Date");
+ expect(dialog?.getAttribute("aria-modal")).toBe("true");
+ expect(dialog?.textContent).toBe("Test Content");
+ });
+
+ it("renders with showTimeSelectOnly prop", () => {
+ const { container } = render(
+
+ Time Content
+ ,
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog?.getAttribute("aria-label")).toBe("Choose Time");
+ });
+
+ it("renders with showTime prop", () => {
+ const { container } = render(
+
+ Date and Time Content
+ ,
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog?.getAttribute("aria-label")).toBe("Choose Date and Time");
+ });
+
+ it("renders with both showTime and showTimeSelectOnly props", () => {
+ const { container } = render(
+
+ Content
+ ,
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ // showTimeSelectOnly takes precedence
+ expect(dialog?.getAttribute("aria-label")).toBe("Choose Time");
+ });
+
+ it("applies custom className", () => {
+ const { container } = render(
+
+ Content
+ ,
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog?.className).toBe("custom-class");
+ });
+
+ it("renders children correctly", () => {
+ const { container } = render(
+
+ Child 1
+ Child 2
+ ,
+ );
+
+ expect(container.querySelector('[data-testid="child-1"]')).toBeTruthy();
+ expect(container.querySelector('[data-testid="child-2"]')).toBeTruthy();
+ });
+
+ it("renders with proper ARIA attributes", () => {
+ const { container } = render(
+
+ Content
+ ,
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog?.getAttribute("role")).toBe("dialog");
+ expect(dialog?.getAttribute("aria-modal")).toBe("true");
+ expect(dialog?.getAttribute("aria-label")).toBe("Choose Date");
+ });
+});
diff --git a/src/test/click_outside_wrapper.test.tsx b/src/test/click_outside_wrapper.test.tsx
new file mode 100644
index 000000000..c5967d51b
--- /dev/null
+++ b/src/test/click_outside_wrapper.test.tsx
@@ -0,0 +1,169 @@
+import { render, fireEvent } from "@testing-library/react";
+import React from "react";
+
+import { ClickOutsideWrapper } from "../click_outside_wrapper";
+
+describe("ClickOutsideWrapper", () => {
+ let onClickOutsideMock: jest.Mock;
+
+ beforeEach(() => {
+ onClickOutsideMock = jest.fn();
+ });
+
+ afterEach(() => {
+ onClickOutsideMock.mockClear();
+ });
+
+ it("renders children correctly", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ expect(container.querySelector('[data-testid="child"]')).toBeTruthy();
+ });
+
+ it("calls onClickOutside when clicking outside the wrapper", () => {
+ const { container } = render(
+
,
+ );
+
+ const outsideElement = container.querySelector(
+ '[data-testid="outside"]',
+ ) as HTMLElement;
+ fireEvent.mouseDown(outsideElement);
+
+ expect(onClickOutsideMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("does not call onClickOutside when clicking inside the wrapper", () => {
+ const { container } = render(
+
+ Inside
+ ,
+ );
+
+ const insideElement = container.querySelector(
+ '[data-testid="inside"]',
+ ) as HTMLElement;
+ fireEvent.mouseDown(insideElement);
+
+ expect(onClickOutsideMock).not.toHaveBeenCalled();
+ });
+
+ it("applies custom className", () => {
+ const { container } = render(
+
+ Content
+ ,
+ );
+
+ const wrapper = container.firstChild as HTMLElement;
+ expect(wrapper.className).toBe("custom-class");
+ });
+
+ it("applies custom style", () => {
+ const customStyle = { backgroundColor: "red", padding: "10px" };
+ const { container } = render(
+
+ Content
+ ,
+ );
+
+ const wrapper = container.firstChild as HTMLElement;
+ expect(wrapper.style.backgroundColor).toBe("red");
+ expect(wrapper.style.padding).toBe("10px");
+ });
+
+ it("does not call onClickOutside when clicking on element with ignoreClass", () => {
+ const { container } = render(
+
+
+ Inside
+
+
+ Ignored
+
+
,
+ );
+
+ const ignoredElement = container.querySelector(
+ '[data-testid="ignored"]',
+ ) as HTMLElement;
+ fireEvent.mouseDown(ignoredElement);
+
+ expect(onClickOutsideMock).not.toHaveBeenCalled();
+ });
+
+ it("calls onClickOutside when clicking on element without ignoreClass", () => {
+ const { container } = render(
+
+
+ Inside
+
+
+ Not Ignored
+
+
,
+ );
+
+ const notIgnoredElement = container.querySelector(
+ '[data-testid="not-ignored"]',
+ ) as HTMLElement;
+ fireEvent.mouseDown(notIgnoredElement);
+
+ expect(onClickOutsideMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("uses containerRef when provided", () => {
+ const containerRef = React.createRef();
+ render(
+
+ Content
+ ,
+ );
+
+ expect(containerRef.current).toBeTruthy();
+ expect(containerRef.current?.tagName).toBe("DIV");
+ });
+
+ it("cleans up event listener on unmount", () => {
+ const removeEventListenerSpy = jest.spyOn(document, "removeEventListener");
+
+ const { unmount } = render(
+
+ Content
+ ,
+ );
+
+ unmount();
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ "mousedown",
+ expect.any(Function),
+ );
+
+ removeEventListenerSpy.mockRestore();
+ });
+});
diff --git a/src/test/input_time.test.tsx b/src/test/input_time.test.tsx
new file mode 100644
index 000000000..401b9ff8a
--- /dev/null
+++ b/src/test/input_time.test.tsx
@@ -0,0 +1,214 @@
+import { render, fireEvent } from "@testing-library/react";
+import React from "react";
+
+import InputTime from "../input_time";
+
+describe("InputTime", () => {
+ it("renders with default props", () => {
+ const { container } = render();
+
+ const timeInput = container.querySelector(
+ 'input[type="time"]',
+ ) as HTMLInputElement;
+ expect(timeInput).toBeTruthy();
+ expect(timeInput.className).toBe("react-datepicker-time__input");
+ expect(timeInput.placeholder).toBe("Time");
+ });
+
+ it("renders with timeString prop", () => {
+ const { container } = render();
+
+ const timeInput = container.querySelector(
+ 'input[type="time"]',
+ ) as HTMLInputElement;
+ expect(timeInput.value).toBe("14:30");
+ });
+
+ it("renders with timeInputLabel prop", () => {
+ const { container } = render();
+
+ const label = container.querySelector(
+ ".react-datepicker-time__caption",
+ ) as HTMLElement;
+ expect(label.textContent).toBe("Select Time");
+ });
+
+ it("calls onChange when time is changed", () => {
+ const onChangeMock = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const timeInput = container.querySelector(
+ 'input[type="time"]',
+ ) as HTMLInputElement;
+ fireEvent.change(timeInput, { target: { value: "15:45" } });
+
+ expect(onChangeMock).toHaveBeenCalledTimes(1);
+ const calledDate = onChangeMock.mock.calls[0][0];
+ expect(calledDate.getHours()).toBe(15);
+ expect(calledDate.getMinutes()).toBe(45);
+ });
+
+ it("updates state when timeString prop changes", () => {
+ const { container, rerender } = render();
+
+ let timeInput = container.querySelector(
+ 'input[type="time"]',
+ ) as HTMLInputElement;
+ expect(timeInput.value).toBe("10:00");
+
+ rerender();
+
+ timeInput = container.querySelector(
+ 'input[type="time"]',
+ ) as HTMLInputElement;
+ expect(timeInput.value).toBe("16:30");
+ });
+
+ it("uses provided date when onChange is called", () => {
+ const onChangeMock = jest.fn();
+ const testDate = new Date(2023, 5, 15, 10, 30);
+
+ const { container } = render(
+ ,
+ );
+
+ const timeInput = container.querySelector(
+ 'input[type="time"]',
+ ) as HTMLInputElement;
+ fireEvent.change(timeInput, { target: { value: "14:45" } });
+
+ expect(onChangeMock).toHaveBeenCalledTimes(1);
+ const calledDate = onChangeMock.mock.calls[0][0];
+ expect(calledDate.getFullYear()).toBe(2023);
+ expect(calledDate.getMonth()).toBe(5);
+ expect(calledDate.getDate()).toBe(15);
+ expect(calledDate.getHours()).toBe(14);
+ expect(calledDate.getMinutes()).toBe(45);
+ });
+
+ it("creates new date when no date prop is provided", () => {
+ const onChangeMock = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const timeInput = container.querySelector(
+ 'input[type="time"]',
+ ) as HTMLInputElement;
+ fireEvent.change(timeInput, { target: { value: "14:45" } });
+
+ expect(onChangeMock).toHaveBeenCalledTimes(1);
+ const calledDate = onChangeMock.mock.calls[0][0];
+ expect(calledDate).toBeInstanceOf(Date);
+ expect(calledDate.getHours()).toBe(14);
+ expect(calledDate.getMinutes()).toBe(45);
+ });
+
+ it("renders custom time input when provided", () => {
+ const CustomTimeInput = ({
+ value,
+ onChange,
+ }: {
+ value: string;
+ onChange: (time: string) => void;
+ }) => (
+ onChange(e.target.value)}
+ />
+ );
+
+ const { container } = render(
+ {}} />}
+ timeString="12:00"
+ />,
+ );
+
+ const customInput = container.querySelector(
+ '[data-testid="custom-time-input"]',
+ ) as HTMLInputElement;
+ expect(customInput).toBeTruthy();
+ expect(customInput.value).toBe("12:00");
+ });
+
+ it("calls onChange with custom time input", () => {
+ const onChangeMock = jest.fn();
+ const CustomTimeInput = ({
+ value,
+ onChange,
+ }: {
+ value: string;
+ onChange: (time: string) => void;
+ }) => (
+ onChange(e.target.value)}
+ />
+ );
+
+ const { container } = render(
+ {}} />}
+ timeString="12:00"
+ />,
+ );
+
+ const customInput = container.querySelector(
+ '[data-testid="custom-time-input"]',
+ ) as HTMLInputElement;
+ fireEvent.change(customInput, { target: { value: "18:30" } });
+
+ expect(onChangeMock).toHaveBeenCalledTimes(1);
+ const calledDate = onChangeMock.mock.calls[0][0];
+ expect(calledDate.getHours()).toBe(18);
+ expect(calledDate.getMinutes()).toBe(30);
+ });
+
+ it("focuses input when clicked", () => {
+ const { container } = render();
+
+ const timeInput = container.querySelector(
+ 'input[type="time"]',
+ ) as HTMLInputElement;
+ const focusSpy = jest.spyOn(timeInput, "focus");
+
+ fireEvent.click(timeInput);
+
+ expect(focusSpy).toHaveBeenCalled();
+ focusSpy.mockRestore();
+ });
+
+ it("uses timeString as fallback when onChange value is empty", () => {
+ const onChangeMock = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const timeInput = container.querySelector(
+ 'input[type="time"]',
+ ) as HTMLInputElement;
+ fireEvent.change(timeInput, { target: { value: "" } });
+
+ expect(onChangeMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("renders container with correct class names", () => {
+ const { container } = render();
+
+ expect(
+ container.querySelector(".react-datepicker__input-time-container"),
+ ).toBeTruthy();
+ expect(
+ container.querySelector(".react-datepicker-time__input-container"),
+ ).toBeTruthy();
+ expect(
+ container.querySelector(".react-datepicker-time__input"),
+ ).toBeTruthy();
+ });
+});
diff --git a/src/test/popper_component.test.tsx b/src/test/popper_component.test.tsx
new file mode 100644
index 000000000..be3ddbb13
--- /dev/null
+++ b/src/test/popper_component.test.tsx
@@ -0,0 +1,294 @@
+import { render, fireEvent } from "@testing-library/react";
+import React from "react";
+
+import { PopperComponent } from "../popper_component";
+
+// Mock the withFloating HOC
+jest.mock("../with_floating", () => ({
+ __esModule: true,
+ default: (Component: React.ComponentType) => Component,
+}));
+
+// Mock FloatingArrow component
+jest.mock("@floating-ui/react", () => ({
+ FloatingArrow: ({ className }: { className: string }) => (
+
+ ),
+}));
+
+describe("PopperComponent", () => {
+ const mockPopperProps = {
+ refs: {
+ reference: { current: null },
+ floating: { current: null },
+ setFloating: jest.fn(),
+ setReference: jest.fn(),
+ setPositionReference: jest.fn(),
+ },
+ floatingStyles: { position: "absolute" as const, top: 0, left: 0 },
+ placement: "bottom" as const,
+ strategy: "absolute" as const,
+ x: 0,
+ y: 0,
+ middlewareData: {},
+ isPositioned: true,
+ update: jest.fn(),
+ elements: {
+ reference: null,
+ floating: null,
+ domReference: null,
+ },
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ context: {} as any,
+ arrowRef: { current: null },
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any;
+
+ const defaultProps = {
+ popperComponent: Popper Content
,
+ targetComponent: Target
,
+ popperOnKeyDown: jest.fn(),
+ popperProps: mockPopperProps,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("renders target component", () => {
+ const { container } = render();
+
+ expect(container.querySelector('[data-testid="target"]')).toBeTruthy();
+ });
+
+ it("renders target component with wrapper class", () => {
+ const { container } = render();
+
+ const wrapper = container.querySelector(".react-datepicker-wrapper");
+ expect(wrapper).toBeTruthy();
+ expect(wrapper?.querySelector('[data-testid="target"]')).toBeTruthy();
+ });
+
+ it("applies custom wrapperClassName", () => {
+ const { container } = render(
+ ,
+ );
+
+ const wrapper = container.querySelector(
+ ".react-datepicker-wrapper.custom-wrapper",
+ );
+ expect(wrapper).toBeTruthy();
+ });
+
+ it("hides popper when hidePopper is true", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.querySelector('[data-testid="popper-content"]')).toBe(
+ null,
+ );
+ });
+
+ it("shows popper when hidePopper is false", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(
+ container.querySelector('[data-testid="popper-content"]'),
+ ).toBeTruthy();
+ });
+
+ it("applies popper className", () => {
+ const { container } = render(
+ ,
+ );
+
+ const popper = container.querySelector(
+ ".react-datepicker-popper.custom-popper",
+ );
+ expect(popper).toBeTruthy();
+ });
+
+ it("applies data-placement attribute", () => {
+ const { container } = render(
+ ,
+ );
+
+ const popper = container.querySelector(".react-datepicker-popper");
+ expect(popper?.getAttribute("data-placement")).toBe("bottom");
+ });
+
+ it("calls popperOnKeyDown when key is pressed in popper", () => {
+ const onKeyDownMock = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const popper = container.querySelector(
+ ".react-datepicker-popper",
+ ) as HTMLElement;
+ fireEvent.keyDown(popper, { key: "Escape" });
+
+ expect(onKeyDownMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("renders arrow when showArrow is true", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(
+ container.querySelector('[data-testid="floating-arrow"]'),
+ ).toBeTruthy();
+ });
+
+ it("does not render arrow when showArrow is false", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.querySelector('[data-testid="floating-arrow"]')).toBe(
+ null,
+ );
+ });
+
+ it("wraps popper in TabLoop when enableTabLoop is true", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.querySelector(".react-datepicker__tab-loop")).toBeTruthy();
+ });
+
+ it("renders in portal when portalId is provided", () => {
+ const { container } = render(
+ ,
+ );
+
+ // Popper should not be in the main container
+ expect(container.querySelector('[data-testid="popper-content"]')).toBe(
+ null,
+ );
+
+ // Popper should be in the portal
+ const portalRoot = document.getElementById("test-portal");
+ expect(portalRoot).toBeTruthy();
+ expect(
+ portalRoot?.querySelector('[data-testid="popper-content"]'),
+ ).toBeTruthy();
+
+ // Cleanup
+ portalRoot?.remove();
+ });
+
+ it("does not render in portal when hidePopper is true even with portalId", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.querySelector('[data-testid="popper-content"]')).toBe(
+ null,
+ );
+ expect(document.getElementById("test-portal-2")).toBe(null);
+ });
+
+ it("wraps popper in custom container when popperContainer is provided", () => {
+ const CustomContainer: React.FC<{ children?: React.ReactNode }> = ({
+ children,
+ }) => {children}
;
+
+ const { container } = render(
+ ,
+ );
+
+ expect(
+ container.querySelector('[data-testid="custom-container"]'),
+ ).toBeTruthy();
+ expect(
+ container
+ .querySelector('[data-testid="custom-container"]')
+ ?.querySelector('[data-testid="popper-content"]'),
+ ).toBeTruthy();
+ });
+
+ it("applies floating styles to popper", () => {
+ const customStyles = {
+ position: "absolute" as const,
+ top: 100,
+ left: 200,
+ };
+
+ const customPopperProps = {
+ ...mockPopperProps,
+ floatingStyles: customStyles,
+ };
+
+ const { container } = render(
+ ,
+ );
+
+ const popper = container.querySelector(
+ ".react-datepicker-popper",
+ ) as HTMLElement;
+ expect(popper.style.position).toBe("absolute");
+ expect(popper.style.top).toBe("100px");
+ expect(popper.style.left).toBe("200px");
+ });
+
+ it("renders with shadow DOM when portalHost is provided", () => {
+ const shadowHost = document.createElement("div");
+ document.body.appendChild(shadowHost);
+ const shadowRoot = shadowHost.attachShadow({ mode: "open" });
+
+ render(
+ ,
+ );
+
+ const portalRoot = shadowRoot.getElementById("shadow-portal");
+ expect(portalRoot).toBeTruthy();
+ expect(
+ portalRoot?.querySelector('[data-testid="popper-content"]'),
+ ).toBeTruthy();
+
+ shadowHost.remove();
+ });
+});
diff --git a/src/test/portal.test.tsx b/src/test/portal.test.tsx
new file mode 100644
index 000000000..5793e3587
--- /dev/null
+++ b/src/test/portal.test.tsx
@@ -0,0 +1,157 @@
+import { render } from "@testing-library/react";
+import React from "react";
+
+import Portal from "../portal";
+
+describe("Portal", () => {
+ afterEach(() => {
+ // Clean up any portal elements created during tests
+ const portalElements = document.querySelectorAll('[id^="test-portal"]');
+ portalElements.forEach((el) => el.remove());
+ });
+
+ it("renders children into a portal", () => {
+ const { container } = render(
+
+ Portal Content
+ ,
+ );
+
+ // Content should not be in the original container
+ expect(container.querySelector('[data-testid="portal-content"]')).toBe(
+ null,
+ );
+
+ // Content should be in the portal
+ const portalRoot = document.getElementById("test-portal-1");
+ expect(portalRoot).toBeTruthy();
+ expect(
+ portalRoot?.querySelector('[data-testid="portal-content"]'),
+ ).toBeTruthy();
+ });
+
+ it("creates portal root if it doesn't exist", () => {
+ expect(document.getElementById("test-portal-2")).toBe(null);
+
+ render(
+
+ Content
+ ,
+ );
+
+ const portalRoot = document.getElementById("test-portal-2");
+ expect(portalRoot).toBeTruthy();
+ expect(portalRoot?.parentElement).toBe(document.body);
+ });
+
+ it("uses existing portal root if it exists", () => {
+ const existingPortal = document.createElement("div");
+ existingPortal.id = "test-portal-3";
+ document.body.appendChild(existingPortal);
+
+ render(
+
+ Content
+ ,
+ );
+
+ const portalRoot = document.getElementById("test-portal-3");
+ expect(portalRoot).toBe(existingPortal);
+ expect(portalRoot?.querySelector('[data-testid="content"]')).toBeTruthy();
+
+ existingPortal.remove();
+ });
+
+ it("removes portal content on unmount", () => {
+ const { unmount } = render(
+
+ Portal Content
+ ,
+ );
+
+ const portalRoot = document.getElementById("test-portal-4");
+ expect(
+ portalRoot?.querySelector('[data-testid="portal-content"]'),
+ ).toBeTruthy();
+
+ unmount();
+
+ expect(portalRoot?.querySelector('[data-testid="portal-content"]')).toBe(
+ null,
+ );
+ });
+
+ it("renders multiple children correctly", () => {
+ render(
+
+ Child 1
+ Child 2
+ Child 3
+ ,
+ );
+
+ const portalRoot = document.getElementById("test-portal-5");
+ expect(portalRoot?.querySelector('[data-testid="child-1"]')).toBeTruthy();
+ expect(portalRoot?.querySelector('[data-testid="child-2"]')).toBeTruthy();
+ expect(portalRoot?.querySelector('[data-testid="child-3"]')).toBeTruthy();
+ });
+
+ it("works with shadow DOM when portalHost is provided", () => {
+ const shadowHost = document.createElement("div");
+ document.body.appendChild(shadowHost);
+ const shadowRoot = shadowHost.attachShadow({ mode: "open" });
+
+ render(
+
+ Shadow Content
+ ,
+ );
+
+ const portalRoot = shadowRoot.getElementById("test-portal-shadow");
+ expect(portalRoot).toBeTruthy();
+ expect(
+ portalRoot?.querySelector('[data-testid="shadow-content"]'),
+ ).toBeTruthy();
+
+ shadowHost.remove();
+ });
+
+ it("appends to portalHost instead of document.body when provided", () => {
+ const customHost = document.createElement("div");
+ customHost.id = "custom-host";
+ document.body.appendChild(customHost);
+ const shadowRoot = customHost.attachShadow({ mode: "open" });
+
+ render(
+
+ Custom Host Content
+ ,
+ );
+
+ const portalRoot = shadowRoot.getElementById("test-portal-custom-host");
+ expect(portalRoot).toBeTruthy();
+ expect(portalRoot?.parentNode).toBe(shadowRoot);
+
+ customHost.remove();
+ });
+
+ it("handles re-renders correctly", () => {
+ const { rerender } = render(
+
+ Content 1
+ ,
+ );
+
+ const portalRoot = document.getElementById("test-portal-6");
+ expect(portalRoot?.querySelector('[data-testid="content-1"]')).toBeTruthy();
+
+ rerender(
+
+ Content 2
+ ,
+ );
+
+ expect(portalRoot?.querySelector('[data-testid="content-1"]')).toBe(null);
+ expect(portalRoot?.querySelector('[data-testid="content-2"]')).toBeTruthy();
+ });
+});
diff --git a/src/test/tab_loop.test.tsx b/src/test/tab_loop.test.tsx
new file mode 100644
index 000000000..bb818d67c
--- /dev/null
+++ b/src/test/tab_loop.test.tsx
@@ -0,0 +1,250 @@
+import { render, fireEvent } from "@testing-library/react";
+import React from "react";
+
+import TabLoop from "../tab_loop";
+
+describe("TabLoop", () => {
+ it("renders children when enableTabLoop is true", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ expect(container.querySelector('[data-testid="child"]')).toBeTruthy();
+ });
+
+ it("renders children when enableTabLoop is false", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ expect(container.querySelector('[data-testid="child"]')).toBeTruthy();
+ });
+
+ it("renders tab loop wrapper when enableTabLoop is true", () => {
+ const { container } = render(
+
+ Content
+ ,
+ );
+
+ expect(container.querySelector(".react-datepicker__tab-loop")).toBeTruthy();
+ expect(
+ container.querySelector(".react-datepicker__tab-loop__start"),
+ ).toBeTruthy();
+ expect(
+ container.querySelector(".react-datepicker__tab-loop__end"),
+ ).toBeTruthy();
+ });
+
+ it("does not render tab loop wrapper when enableTabLoop is false", () => {
+ const { container } = render(
+
+ Content
+ ,
+ );
+
+ expect(container.querySelector(".react-datepicker__tab-loop")).toBe(null);
+ expect(container.querySelector(".react-datepicker__tab-loop__start")).toBe(
+ null,
+ );
+ expect(container.querySelector(".react-datepicker__tab-loop__end")).toBe(
+ null,
+ );
+ });
+
+ it("uses default enableTabLoop value when not provided", () => {
+ const { container } = render(
+
+ Content
+ ,
+ );
+
+ // Default is true
+ expect(container.querySelector(".react-datepicker__tab-loop")).toBeTruthy();
+ });
+
+ it("focuses last tabbable element when start sentinel is focused", () => {
+ const { container } = render(
+
+
+
+
+ ,
+ );
+
+ const startSentinel = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ ) as HTMLElement;
+ const lastButton = container.querySelector(
+ '[data-testid="button-3"]',
+ ) as HTMLButtonElement;
+
+ const focusSpy = jest.spyOn(lastButton, "focus");
+ fireEvent.focus(startSentinel);
+
+ expect(focusSpy).toHaveBeenCalled();
+ focusSpy.mockRestore();
+ });
+
+ it("focuses first tabbable element when end sentinel is focused", () => {
+ const { container } = render(
+
+
+
+
+ ,
+ );
+
+ const endSentinel = container.querySelector(
+ ".react-datepicker__tab-loop__end",
+ ) as HTMLElement;
+ const firstButton = container.querySelector(
+ '[data-testid="button-1"]',
+ ) as HTMLButtonElement;
+
+ const focusSpy = jest.spyOn(firstButton, "focus");
+ fireEvent.focus(endSentinel);
+
+ expect(focusSpy).toHaveBeenCalled();
+ focusSpy.mockRestore();
+ });
+
+ it("handles multiple tabbable element types", () => {
+ const { container } = render(
+
+
+
+
+
+
+ Link
+
+ ,
+ );
+
+ const endSentinel = container.querySelector(
+ ".react-datepicker__tab-loop__end",
+ ) as HTMLElement;
+ const firstButton = container.querySelector(
+ '[data-testid="button"]',
+ ) as HTMLButtonElement;
+
+ const focusSpy = jest.spyOn(firstButton, "focus");
+ fireEvent.focus(endSentinel);
+
+ expect(focusSpy).toHaveBeenCalled();
+ focusSpy.mockRestore();
+ });
+
+ it("ignores disabled elements", () => {
+ const { container } = render(
+
+
+
+
+ ,
+ );
+
+ const endSentinel = container.querySelector(
+ ".react-datepicker__tab-loop__end",
+ ) as HTMLElement;
+ const enabledButton = container.querySelector(
+ '[data-testid="button-enabled"]',
+ ) as HTMLButtonElement;
+
+ const focusSpy = jest.spyOn(enabledButton, "focus");
+ fireEvent.focus(endSentinel);
+
+ expect(focusSpy).toHaveBeenCalled();
+ focusSpy.mockRestore();
+ });
+
+ it("ignores elements with tabIndex -1", () => {
+ const { container } = render(
+
+
+
+
+ ,
+ );
+
+ const endSentinel = container.querySelector(
+ ".react-datepicker__tab-loop__end",
+ ) as HTMLElement;
+ const normalButton = container.querySelector(
+ '[data-testid="button-normal"]',
+ ) as HTMLButtonElement;
+
+ const focusSpy = jest.spyOn(normalButton, "focus");
+ fireEvent.focus(endSentinel);
+
+ expect(focusSpy).toHaveBeenCalled();
+ focusSpy.mockRestore();
+ });
+
+ it("handles case with only one tabbable element", () => {
+ const { container } = render(
+
+
+ ,
+ );
+
+ const startSentinel = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ ) as HTMLElement;
+
+ // Should not throw error with single element
+ expect(() => fireEvent.focus(startSentinel)).not.toThrow();
+ });
+
+ it("handles case with no tabbable elements", () => {
+ const { container } = render(
+
+ No tabbable elements
+ ,
+ );
+
+ const startSentinel = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ ) as HTMLElement;
+
+ // Should not throw error with no tabbable elements
+ expect(() => fireEvent.focus(startSentinel)).not.toThrow();
+ });
+
+ it("renders with custom tabIndex elements", () => {
+ const { container } = render(
+
+
+ Div 1
+
+
+ Div 2
+
+ ,
+ );
+
+ const endSentinel = container.querySelector(
+ ".react-datepicker__tab-loop__end",
+ ) as HTMLElement;
+ const firstDiv = container.querySelector(
+ '[data-testid="div-1"]',
+ ) as HTMLDivElement;
+
+ const focusSpy = jest.spyOn(firstDiv, "focus");
+ fireEvent.focus(endSentinel);
+
+ expect(focusSpy).toHaveBeenCalled();
+ focusSpy.mockRestore();
+ });
+});
diff --git a/src/test/with_floating.test.tsx b/src/test/with_floating.test.tsx
new file mode 100644
index 000000000..2dd172f4d
--- /dev/null
+++ b/src/test/with_floating.test.tsx
@@ -0,0 +1,242 @@
+import { render, waitFor } from "@testing-library/react";
+import React from "react";
+
+import withFloating from "../with_floating";
+
+import type { FloatingProps } from "../with_floating";
+
+// Mock @floating-ui/react
+jest.mock("@floating-ui/react", () => ({
+ useFloating: jest.fn(() => ({
+ placement: "bottom",
+ strategy: "absolute",
+ middlewareData: {},
+ x: 0,
+ y: 0,
+ isPositioned: true,
+ update: jest.fn(),
+ floatingStyles: { position: "absolute" as const, top: 0, left: 0 },
+ refs: {
+ reference: { current: null },
+ floating: { current: null },
+ setFloating: jest.fn(),
+ setReference: jest.fn(),
+ },
+ elements: {
+ reference: null,
+ floating: null,
+ domReference: null,
+ },
+ context: {},
+ })),
+ arrow: jest.fn(() => ({ name: "arrow", fn: jest.fn() })),
+ offset: jest.fn(() => ({ name: "offset", fn: jest.fn() })),
+ flip: jest.fn(() => ({ name: "flip", fn: jest.fn() })),
+ autoUpdate: jest.fn(),
+}));
+
+// Get the mocked functions
+const {
+ useFloating: mockUseFloating,
+ arrow: mockArrow,
+ offset: mockOffset,
+ flip: mockFlip,
+ autoUpdate: mockAutoUpdate,
+} = jest.requireMock("@floating-ui/react") as {
+ useFloating: jest.Mock;
+ arrow: jest.Mock;
+ offset: jest.Mock;
+ flip: jest.Mock;
+ autoUpdate: jest.Mock;
+};
+
+interface TestComponentProps extends FloatingProps {
+ testProp?: string;
+}
+
+const TestComponent: React.FC = ({
+ testProp,
+ popperProps,
+}) => (
+
+
{popperProps.placement}
+
+ {popperProps.arrowRef.current ? "has-ref" : "no-ref"}
+
+
+);
+
+describe("withFloating", () => {
+ it("wraps component and provides popperProps", async () => {
+ const WrappedComponent = withFloating(TestComponent);
+ const { container } = render();
+ await waitFor(() => expect(container).toBeTruthy());
+
+ expect(
+ container.querySelector('[data-testid="test-component"]'),
+ ).toBeTruthy();
+ expect(
+ container.querySelector('[data-testid="placement"]')?.textContent,
+ ).toBe("bottom");
+ });
+
+ it("passes through original props", async () => {
+ const WrappedComponent = withFloating(TestComponent);
+ const { container } = render();
+ await waitFor(() => expect(container).toBeTruthy());
+
+ const testComponent = container.querySelector(
+ '[data-testid="test-component"]',
+ );
+ expect(testComponent?.getAttribute("data-test-prop")).toBe("custom-value");
+ });
+
+ it("provides arrowRef in popperProps", async () => {
+ const WrappedComponent = withFloating(TestComponent);
+ const { container } = render();
+ await waitFor(() => expect(container).toBeTruthy());
+
+ expect(
+ container.querySelector('[data-testid="arrow-ref"]')?.textContent,
+ ).toBe("no-ref");
+ });
+
+ it("sets hidePopper to true by default", async () => {
+ const WrappedComponent = withFloating(TestComponent);
+ render();
+ await waitFor(() => expect(mockUseFloating).toHaveBeenCalled());
+
+ expect(mockUseFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ open: false,
+ }),
+ );
+ });
+
+ it("respects hidePopper prop when set to false", async () => {
+ const WrappedComponent = withFloating(TestComponent);
+ render();
+ await waitFor(() => expect(mockUseFloating).toHaveBeenCalled());
+
+ expect(mockUseFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ open: true,
+ }),
+ );
+ });
+
+ it("passes popperPlacement to useFloating", async () => {
+ const WrappedComponent = withFloating(TestComponent);
+ render();
+ await waitFor(() => expect(mockUseFloating).toHaveBeenCalled());
+
+ expect(mockUseFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ placement: "top",
+ }),
+ );
+ });
+
+ it("includes default middleware", async () => {
+ const WrappedComponent = withFloating(TestComponent);
+ render();
+ await waitFor(() => expect(mockUseFloating).toHaveBeenCalled());
+
+ expect(mockFlip).toHaveBeenCalledWith({ padding: 15 });
+ expect(mockOffset).toHaveBeenCalledWith(10);
+ expect(mockArrow).toHaveBeenCalled();
+ expect(mockUseFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ middleware: expect.arrayContaining([
+ expect.objectContaining({ name: "flip" }),
+ expect.objectContaining({ name: "offset" }),
+ expect.objectContaining({ name: "arrow" }),
+ ]),
+ }),
+ );
+ });
+
+ it("includes custom popperModifiers", async () => {
+ const customModifier = { name: "custom", fn: jest.fn() };
+ const WrappedComponent = withFloating(TestComponent);
+ render();
+ await waitFor(() => expect(mockUseFloating).toHaveBeenCalled());
+
+ expect(mockUseFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ middleware: expect.arrayContaining([
+ expect.objectContaining({ name: "custom" }),
+ ]),
+ }),
+ );
+ });
+
+ it("passes popperProps to useFloating", async () => {
+ const customProps = { strategy: "fixed" as const };
+ const WrappedComponent = withFloating(TestComponent);
+ render();
+ await waitFor(() => expect(mockUseFloating).toHaveBeenCalled());
+
+ expect(mockUseFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ strategy: "fixed",
+ }),
+ );
+ });
+
+ it("sets whileElementsMounted to autoUpdate", async () => {
+ const WrappedComponent = withFloating(TestComponent);
+ render();
+ await waitFor(() => expect(mockUseFloating).toHaveBeenCalled());
+
+ expect(mockUseFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ whileElementsMounted: mockAutoUpdate,
+ }),
+ );
+ });
+
+ it("sets displayName correctly", () => {
+ const NamedComponent: React.FC = () => ;
+ NamedComponent.displayName = "MyComponent";
+
+ const WrappedComponent = withFloating(NamedComponent);
+ expect(WrappedComponent.displayName).toBe("withFloating(MyComponent)");
+ });
+
+ it("sets displayName from component name if displayName not set", () => {
+ function MyNamedFunction(_props: FloatingProps) {
+ return ;
+ }
+
+ const WrappedComponent = withFloating(MyNamedFunction);
+ expect(WrappedComponent.displayName).toBe("withFloating(MyNamedFunction)");
+ });
+
+ it("sets displayName to Component if no name available", () => {
+ const AnonymousComponent: React.FC = () => ;
+
+ const WrappedComponent = withFloating(AnonymousComponent);
+ expect(WrappedComponent.displayName).toBe(
+ "withFloating(AnonymousComponent)",
+ );
+ });
+
+ it("handles hidePopper boolean correctly", async () => {
+ const WrappedComponent = withFloating(TestComponent);
+
+ const { rerender } = render();
+ await waitFor(() =>
+ expect(mockUseFloating).toHaveBeenCalledWith(
+ expect.objectContaining({ open: false }),
+ ),
+ );
+
+ rerender();
+ await waitFor(() =>
+ expect(mockUseFloating).toHaveBeenCalledWith(
+ expect.objectContaining({ open: true }),
+ ),
+ );
+ });
+});