Skip to content

Commit 6c6e8d4

Browse files
Merge pull request #5310 from meriouma/fix-shadow-dom
fix: fix click outside within Shadow DOM
2 parents 5ebec51 + c011c76 commit 6c6e8d4

File tree

5 files changed

+2012
-3031
lines changed

5 files changed

+2012
-3031
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@rollup/plugin-typescript": "^11.1.6",
3939
"@testing-library/dom": "^10.4.0",
4040
"@testing-library/react": "^16.0.0",
41+
"@testing-library/user-event": "14.5.2",
4142
"@types/eslint": "^8.56.10",
4243
"@types/jest": "^29.5.12",
4344
"@types/node": "22",

src/click_outside_wrapper.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,19 @@ const useDetectClickOutside = (
2020
onClickOutsideRef.current = onClickOutside;
2121
const handleClickOutside = useCallback(
2222
(event: MouseEvent) => {
23-
if (ref.current && !ref.current.contains(event.target as Node)) {
23+
const target =
24+
(event.composed &&
25+
event.composedPath &&
26+
event
27+
.composedPath()
28+
.find((eventTarget) => eventTarget instanceof Node)) ||
29+
event.target;
30+
if (ref.current && !ref.current.contains(target as Node)) {
2431
if (
2532
!(
2633
ignoreClass &&
27-
event.target instanceof HTMLElement &&
28-
event.target.classList.contains(ignoreClass)
34+
target instanceof HTMLElement &&
35+
target.classList.contains(ignoreClass)
2936
)
3037
) {
3138
onClickOutsideRef.current?.(event);

src/test/datepicker_test.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { render, act, waitFor, fireEvent } from "@testing-library/react";
2+
import { userEvent } from "@testing-library/user-event";
23
import { enUS, enGB } from "date-fns/locale";
34
import React from "react";
45

@@ -26,6 +27,7 @@ import {
2627
import DatePicker, { registerLocale } from "../index";
2728

2829
import CustomInput from "./helper_components/custom_input";
30+
import ShadowRoot from "./helper_components/shadow_root";
2931
import TestWrapper from "./helper_components/test_wrapper";
3032
import { getKey, safeQuerySelector } from "./test_utils";
3133

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

201+
it("calendar should stay open when clicked within shadow dom and closed when clicked outside", async () => {
202+
let instance: DatePicker | null = null;
203+
render(
204+
<ShadowRoot>
205+
<DatePicker
206+
ref={(node) => {
207+
instance = node;
208+
}}
209+
/>
210+
</ShadowRoot>,
211+
);
212+
213+
expect(instance).toBeTruthy();
214+
expect(instance!.input).toBeTruthy();
215+
216+
await userEvent.click(instance!.input!);
217+
expect(instance!.isCalendarOpen()).toBe(true);
218+
expect(instance!.calendar).toBeTruthy();
219+
expect(instance!.calendar!.containerRef.current).toBeTruthy();
220+
221+
await userEvent.click(instance!.calendar!.containerRef.current!);
222+
expect(instance!.isCalendarOpen()).toBe(true);
223+
224+
await userEvent.click(document.body);
225+
expect(instance!.isCalendarOpen()).toBe(false);
226+
});
227+
199228
it("should not set open state when it is disabled and gets clicked", () => {
200229
const { container } = render(<DatePicker disabled />);
201230

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React, {
2+
type FC,
3+
type PropsWithChildren,
4+
useLayoutEffect,
5+
useRef,
6+
useState,
7+
} from "react";
8+
import { createPortal } from "react-dom";
9+
10+
const ShadowRoot: FC<PropsWithChildren> = ({ children }) => {
11+
const containerRef = useRef<HTMLDivElement>(null);
12+
const shadowRootRef = useRef<ShadowRoot>(null);
13+
const [isInitialized, setIsInitialized] = useState(false);
14+
15+
useLayoutEffect(() => {
16+
const container = containerRef.current;
17+
if (isInitialized || !container) {
18+
return;
19+
}
20+
21+
shadowRootRef.current =
22+
container.shadowRoot ?? container.attachShadow({ mode: "open" });
23+
setIsInitialized(true);
24+
}, [isInitialized]);
25+
26+
return (
27+
<div ref={containerRef}>
28+
{isInitialized &&
29+
shadowRootRef.current &&
30+
createPortal(children, shadowRootRef.current)}
31+
</div>
32+
);
33+
};
34+
35+
export default ShadowRoot;

0 commit comments

Comments
 (0)