Skip to content

Commit 5fb8321

Browse files
committed
fix: fix click outside within Shadow DOM (#5149)
1 parent b640106 commit 5fb8321

File tree

7 files changed

+2949
-3037
lines changed

7 files changed

+2949
-3037
lines changed

.yarn/releases/yarn-4.6.0.cjs

Lines changed: 934 additions & 0 deletions
Large diffs are not rendered by default.

.yarnrc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
nodeLinker: node-modules
2+
3+
yarnPath: .yarn/releases/yarn-4.6.0.cjs

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: 24 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,28 @@ 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+
await userEvent.click(instance!.input!);
214+
expect(instance!.isCalendarOpen()).toBe(true);
215+
216+
await userEvent.click(instance!.calendar!.containerRef.current!);
217+
expect(instance!.isCalendarOpen()).toBe(true);
218+
219+
await userEvent.click(document.body);
220+
expect(instance!.isCalendarOpen()).toBe(false);
221+
});
222+
199223
it("should not set open state when it is disabled and gets clicked", () => {
200224
const { container } = render(<DatePicker disabled />);
201225

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)