Skip to content

Commit

Permalink
Merge pull request #12 from WTW-IM/ensure-mouse-events-retarget
Browse files Browse the repository at this point in the history
Update: ensure mouse events are on shadow root
  • Loading branch information
stevematney committed Nov 20, 2020
2 parents 31cd9c4 + 163b178 commit 5dd5879
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text eol=lf
25 changes: 22 additions & 3 deletions src/ReactHTMLElement.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import ReactDOM from 'react-dom';
import forceRetarget from './forcedRetargeting';

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

// See https://github.com/facebook/react/issues/9242#issuecomment-543117675
function retargetReactEvents(container: Node, shadow: LooseShadowRoot): void {
function retargetReactEvents(
container: Node,
shadow: LooseShadowRoot,
): () => void {
Object.defineProperty(container, 'ownerDocument', { value: shadow });
/* eslint-disable no-param-reassign */
shadow.defaultView = window;
Expand All @@ -19,6 +23,7 @@ function retargetReactEvents(container: Node, shadow: LooseShadowRoot): void {
options: ElementCreationOptions,
): Element => document.createElementNS(ns, tagName, options);
shadow.createTextNode = (text: string): Text => document.createTextNode(text);
return forceRetarget(shadow);
/* eslint-enable no-param-reassign */
}

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

private mountSelector: string;

private retargetCleanupFunction: () => void;

get mountPoint(): Element {
if (this._mountPoint) return this._mountPoint;

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

retargetReactEvents(this._mountPoint, shadow);
this.retargetCleanup = retargetReactEvents(this._mountPoint, shadow);

return this._mountPoint;
}

set mountPoint(mount: Element) {
this._mountPoint = mount;
if (this.shadowRoot) {
retargetReactEvents(mount, this.shadowRoot);
this.retargetCleanup = retargetReactEvents(mount, this.shadowRoot);
}
}

get retargetCleanup(): () => void {
return this.retargetCleanupFunction;
}

set retargetCleanup(cleanupFunction: () => void) {
// Ensure that we cleanup an old listeners before we forget the cleanup function.
this.retargetCleanup();
this.retargetCleanupFunction = cleanupFunction;
}

disconnectedCallback(): void {
if (!this._mountPoint) return;
this.retargetCleanup();
ReactDOM.unmountComponentAtNode(this._mountPoint);
}

constructor(template = '<div></div>', mountSelector = 'div') {
super();
this.template = template;
this.mountSelector = mountSelector;
this.retargetCleanupFunction = () => {};
}
}

Expand Down
120 changes: 120 additions & 0 deletions src/forcedRetargeting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Code came from https://github.com/spring-media/react-shadow-dom-retarget-events/blob/516dafb756d8e3daaadaf9f8b48f85c6811e93e7/index.js
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
const reactEvents = [
'onMouseDown',
'onMouseMove',
'onMouseOut',
'onMouseOver',
'onMouseUp',
];

function findReactProperty(item: any, propertyPrefix: string): any {
// eslint-disable-next-line no-restricted-syntax
for (const key in item) {
// eslint-disable-next-line no-prototype-builtins
if (item.hasOwnProperty(key) && key.includes(propertyPrefix)) {
return item[key];
}
}
return null;
}

function findReactEventHandlers(item: any): any {
return findReactProperty(item, '__reactEventHandlers');
}

function findReactComponent(item: any): any {
return findReactProperty(item, '_reactInternal');
}

function findReactProps(component: any): any {
if (!component) return undefined;
if (component.memoizedProps) return component.memoizedProps; // React 16 Fiber
if (component._currentElement && component._currentElement.props) return component._currentElement.props; // React <=15
return null;
}

function dispatchEvent(
event: any,
eventType: string,
componentProps: any,
): any {
event.persist = function() {
event.isPersistent = function() {
return true;
};
};

if (componentProps[eventType]) {
componentProps[eventType](event);
}
}

function composedPath(
el: HTMLElement | null,
): (Node | (Window & typeof globalThis))[] {
const path = [];
while (el) {
path.push(el);
if (el.tagName === 'HTML') {
path.push(document);
path.push(window);
return path;
}
el = el.parentElement;
}
return [];
}

export default function retargetEvents(shadowRoot: Node): () => void {
const removeEventListeners: (() => void)[] = [];

reactEvents.forEach((reactEventName) => {
const nativeEventName = reactEventName.replace(/^on/, '').toLowerCase();

function retargetEventListener(event: Event | any): void {
const path = event.path
|| (event.composedPath && event.composedPath())
|| composedPath(event.target);

for (let i = 0; i < path.length; i += 1) {
const el = path[i];
let props = null;
const reactComponent = findReactComponent(el);
const eventHandlers = findReactEventHandlers(el);

if (!eventHandlers) {
props = findReactProps(reactComponent);
} else {
props = eventHandlers;
}

if (reactComponent && props) {
dispatchEvent(event, reactEventName, props);
}

if (event.cancelBubble) {
break;
}

if (el === shadowRoot) {
break;
}
}
}

shadowRoot.addEventListener(nativeEventName, retargetEventListener, false);
removeEventListeners.push(() => shadowRoot.removeEventListener(
nativeEventName,
retargetEventListener,
false,
));
});

return () => {
removeEventListeners.forEach((removeEventListener) => {
removeEventListener();
});
};
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
"target": "es5"
},
"include": ["src"],
"exclude": ["src/__tests__"]
"exclude": ["src/__tests__", "node_modules"]
}

0 comments on commit 5dd5879

Please sign in to comment.