Skip to content

Commit

Permalink
Merge pull request #5310 from meriouma/fix-shadow-dom
Browse files Browse the repository at this point in the history
fix: fix click outside within Shadow DOM
  • Loading branch information
martijnrusschen authored Jan 10, 2025
2 parents 5ebec51 + c011c76 commit 6c6e8d4
Show file tree
Hide file tree
Showing 5 changed files with 2,012 additions and 3,031 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@rollup/plugin-typescript": "^11.1.6",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "14.5.2",
"@types/eslint": "^8.56.10",
"@types/jest": "^29.5.12",
"@types/node": "22",
Expand Down
13 changes: 10 additions & 3 deletions src/click_outside_wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,19 @@ const useDetectClickOutside = (
onClickOutsideRef.current = onClickOutside;
const handleClickOutside = useCallback(
(event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
const target =
(event.composed &&
event.composedPath &&
event
.composedPath()
.find((eventTarget) => eventTarget instanceof Node)) ||
event.target;
if (ref.current && !ref.current.contains(target as Node)) {
if (
!(
ignoreClass &&
event.target instanceof HTMLElement &&
event.target.classList.contains(ignoreClass)
target instanceof HTMLElement &&
target.classList.contains(ignoreClass)
)
) {
onClickOutsideRef.current?.(event);
Expand Down
29 changes: 29 additions & 0 deletions src/test/datepicker_test.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { render, act, waitFor, fireEvent } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { enUS, enGB } from "date-fns/locale";
import React from "react";

Expand Down Expand Up @@ -26,6 +27,7 @@ import {
import DatePicker, { registerLocale } from "../index";

import CustomInput from "./helper_components/custom_input";
import ShadowRoot from "./helper_components/shadow_root";
import TestWrapper from "./helper_components/test_wrapper";
import { getKey, safeQuerySelector } from "./test_utils";

Expand Down Expand Up @@ -196,6 +198,33 @@ describe("DatePicker", () => {
expect(shadow.getElementById("test-portal")).toBeDefined();
});

it("calendar should stay open when clicked within shadow dom and closed when clicked outside", async () => {
let instance: DatePicker | null = null;
render(
<ShadowRoot>
<DatePicker
ref={(node) => {
instance = node;
}}
/>
</ShadowRoot>,
);

expect(instance).toBeTruthy();
expect(instance!.input).toBeTruthy();

await userEvent.click(instance!.input!);
expect(instance!.isCalendarOpen()).toBe(true);
expect(instance!.calendar).toBeTruthy();
expect(instance!.calendar!.containerRef.current).toBeTruthy();

await userEvent.click(instance!.calendar!.containerRef.current!);
expect(instance!.isCalendarOpen()).toBe(true);

await userEvent.click(document.body);
expect(instance!.isCalendarOpen()).toBe(false);
});

it("should not set open state when it is disabled and gets clicked", () => {
const { container } = render(<DatePicker disabled />);

Expand Down
35 changes: 35 additions & 0 deletions src/test/helper_components/shadow_root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, {
type FC,
type PropsWithChildren,
useLayoutEffect,
useRef,
useState,
} from "react";
import { createPortal } from "react-dom";

const ShadowRoot: FC<PropsWithChildren> = ({ children }) => {
const containerRef = useRef<HTMLDivElement>(null);
const shadowRootRef = useRef<ShadowRoot>(null);
const [isInitialized, setIsInitialized] = useState(false);

useLayoutEffect(() => {
const container = containerRef.current;
if (isInitialized || !container) {
return;
}

shadowRootRef.current =
container.shadowRoot ?? container.attachShadow({ mode: "open" });
setIsInitialized(true);
}, [isInitialized]);

return (
<div ref={containerRef}>
{isInitialized &&
shadowRootRef.current &&
createPortal(children, shadowRootRef.current)}
</div>
);
};

export default ShadowRoot;
Loading

0 comments on commit 6c6e8d4

Please sign in to comment.