Skip to content

Commit 5dd5879

Browse files
authored
Merge pull request #12 from WTW-IM/ensure-mouse-events-retarget
Update: ensure mouse events are on shadow root
2 parents 31cd9c4 + 163b178 commit 5dd5879

File tree

4 files changed

+144
-4
lines changed

4 files changed

+144
-4
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text eol=lf

src/ReactHTMLElement.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import ReactDOM from 'react-dom';
2+
import forceRetarget from './forcedRetargeting';
23

34
interface LooseShadowRoot extends ShadowRoot {
45
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
56
}
67

78
// See https://github.com/facebook/react/issues/9242#issuecomment-543117675
8-
function retargetReactEvents(container: Node, shadow: LooseShadowRoot): void {
9+
function retargetReactEvents(
10+
container: Node,
11+
shadow: LooseShadowRoot,
12+
): () => void {
913
Object.defineProperty(container, 'ownerDocument', { value: shadow });
1014
/* eslint-disable no-param-reassign */
1115
shadow.defaultView = window;
@@ -19,6 +23,7 @@ function retargetReactEvents(container: Node, shadow: LooseShadowRoot): void {
1923
options: ElementCreationOptions,
2024
): Element => document.createElementNS(ns, tagName, options);
2125
shadow.createTextNode = (text: string): Text => document.createTextNode(text);
26+
return forceRetarget(shadow);
2227
/* eslint-enable no-param-reassign */
2328
}
2429

@@ -29,34 +34,48 @@ class ReactHTMLElement extends HTMLElement {
2934

3035
private mountSelector: string;
3136

37+
private retargetCleanupFunction: () => void;
38+
3239
get mountPoint(): Element {
3340
if (this._mountPoint) return this._mountPoint;
3441

3542
const shadow = this.attachShadow({ mode: 'open' });
3643
shadow.innerHTML = this.template;
3744
this._mountPoint = shadow.querySelector(this.mountSelector) as Element;
3845

39-
retargetReactEvents(this._mountPoint, shadow);
46+
this.retargetCleanup = retargetReactEvents(this._mountPoint, shadow);
4047

4148
return this._mountPoint;
4249
}
4350

4451
set mountPoint(mount: Element) {
4552
this._mountPoint = mount;
4653
if (this.shadowRoot) {
47-
retargetReactEvents(mount, this.shadowRoot);
54+
this.retargetCleanup = retargetReactEvents(mount, this.shadowRoot);
4855
}
4956
}
5057

58+
get retargetCleanup(): () => void {
59+
return this.retargetCleanupFunction;
60+
}
61+
62+
set retargetCleanup(cleanupFunction: () => void) {
63+
// Ensure that we cleanup an old listeners before we forget the cleanup function.
64+
this.retargetCleanup();
65+
this.retargetCleanupFunction = cleanupFunction;
66+
}
67+
5168
disconnectedCallback(): void {
5269
if (!this._mountPoint) return;
70+
this.retargetCleanup();
5371
ReactDOM.unmountComponentAtNode(this._mountPoint);
5472
}
5573

5674
constructor(template = '<div></div>', mountSelector = 'div') {
5775
super();
5876
this.template = template;
5977
this.mountSelector = mountSelector;
78+
this.retargetCleanupFunction = () => {};
6079
}
6180
}
6281

src/forcedRetargeting.ts

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Code came from https://github.com/spring-media/react-shadow-dom-retarget-events/blob/516dafb756d8e3daaadaf9f8b48f85c6811e93e7/index.js
2+
/* eslint-disable no-param-reassign */
3+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
4+
const reactEvents = [
5+
'onMouseDown',
6+
'onMouseMove',
7+
'onMouseOut',
8+
'onMouseOver',
9+
'onMouseUp',
10+
];
11+
12+
function findReactProperty(item: any, propertyPrefix: string): any {
13+
// eslint-disable-next-line no-restricted-syntax
14+
for (const key in item) {
15+
// eslint-disable-next-line no-prototype-builtins
16+
if (item.hasOwnProperty(key) && key.includes(propertyPrefix)) {
17+
return item[key];
18+
}
19+
}
20+
return null;
21+
}
22+
23+
function findReactEventHandlers(item: any): any {
24+
return findReactProperty(item, '__reactEventHandlers');
25+
}
26+
27+
function findReactComponent(item: any): any {
28+
return findReactProperty(item, '_reactInternal');
29+
}
30+
31+
function findReactProps(component: any): any {
32+
if (!component) return undefined;
33+
if (component.memoizedProps) return component.memoizedProps; // React 16 Fiber
34+
if (component._currentElement && component._currentElement.props) return component._currentElement.props; // React <=15
35+
return null;
36+
}
37+
38+
function dispatchEvent(
39+
event: any,
40+
eventType: string,
41+
componentProps: any,
42+
): any {
43+
event.persist = function() {
44+
event.isPersistent = function() {
45+
return true;
46+
};
47+
};
48+
49+
if (componentProps[eventType]) {
50+
componentProps[eventType](event);
51+
}
52+
}
53+
54+
function composedPath(
55+
el: HTMLElement | null,
56+
): (Node | (Window & typeof globalThis))[] {
57+
const path = [];
58+
while (el) {
59+
path.push(el);
60+
if (el.tagName === 'HTML') {
61+
path.push(document);
62+
path.push(window);
63+
return path;
64+
}
65+
el = el.parentElement;
66+
}
67+
return [];
68+
}
69+
70+
export default function retargetEvents(shadowRoot: Node): () => void {
71+
const removeEventListeners: (() => void)[] = [];
72+
73+
reactEvents.forEach((reactEventName) => {
74+
const nativeEventName = reactEventName.replace(/^on/, '').toLowerCase();
75+
76+
function retargetEventListener(event: Event | any): void {
77+
const path = event.path
78+
|| (event.composedPath && event.composedPath())
79+
|| composedPath(event.target);
80+
81+
for (let i = 0; i < path.length; i += 1) {
82+
const el = path[i];
83+
let props = null;
84+
const reactComponent = findReactComponent(el);
85+
const eventHandlers = findReactEventHandlers(el);
86+
87+
if (!eventHandlers) {
88+
props = findReactProps(reactComponent);
89+
} else {
90+
props = eventHandlers;
91+
}
92+
93+
if (reactComponent && props) {
94+
dispatchEvent(event, reactEventName, props);
95+
}
96+
97+
if (event.cancelBubble) {
98+
break;
99+
}
100+
101+
if (el === shadowRoot) {
102+
break;
103+
}
104+
}
105+
}
106+
107+
shadowRoot.addEventListener(nativeEventName, retargetEventListener, false);
108+
removeEventListeners.push(() => shadowRoot.removeEventListener(
109+
nativeEventName,
110+
retargetEventListener,
111+
false,
112+
));
113+
});
114+
115+
return () => {
116+
removeEventListeners.forEach((removeEventListener) => {
117+
removeEventListener();
118+
});
119+
};
120+
}

tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
"target": "es5"
1818
},
1919
"include": ["src"],
20-
"exclude": ["src/__tests__"]
20+
"exclude": ["src/__tests__", "node_modules"]
2121
}

0 commit comments

Comments
 (0)