Skip to content

Instantly share code, notes, and snippets.

@vasnakos
Created October 9, 2024 13:05
Show Gist options
  • Save vasnakos/91aaccc88765d2346765b928e7b82559 to your computer and use it in GitHub Desktop.
Save vasnakos/91aaccc88765d2346765b928e7b82559 to your computer and use it in GitHub Desktop.
This function is designed to address issues with event handling in React 16 when using a Shadow DOM. React 16's synthetic event system does not work well with Shadow DOM due to limitations in how events are retargeted. This function "retargets" events that occur within a shadow DOM, making them compatible with React's synthetic event system by e…
/**
* Retargets DOM events within a Shadow DOM to ensure they are correctly recognized
* by React 16's synthetic event system. This is necessary because React 16 does not
* natively handle events from within a Shadow DOM, leading to issues with event propagation
* and target recognition.
*
* @param {ShadowRoot} shadowRoot - The root of the Shadow DOM where event retargeting should be applied.
*/
function retargetEvents(shadowRoot) {
// List of all event types to listen for within the Shadow DOM.
// These are standard DOM events that React may need to handle.
const eventTypes = [
"abort",
"animationcancel",
"animationend",
"animationiteration",
"animationstart",
"auxclick",
"blur",
"cancel",
"canplay",
"canplaythrough",
"change",
"click",
"close",
"contextmenu",
"cuechange",
"dblclick",
"drag",
"dragend",
"dragenter",
"dragexit",
"dragleave",
"dragover",
"dragstart",
"drop",
"durationchange",
"emptied",
"ended",
"error",
"focus",
"focusin",
"focusout",
"gotpointercapture",
"input",
"invalid",
"keydown",
"keypress",
"keyup",
"load",
"loadeddata",
"loadedmetadata",
"loadstart",
"lostpointercapture",
"mousedown",
"mouseenter",
"mouseleave",
"mousemove",
"mouseout",
"mouseover",
"mouseup",
"pause",
"play",
"playing",
"pointercancel",
"pointerdown",
"pointerenter",
"pointerleave",
"pointermove",
"pointerout",
"pointerover",
"pointerup",
"progress",
"ratechange",
"reset",
"resize",
"scroll",
"securitypolicyviolation",
"seeked",
"seeking",
"select",
"selectionchange",
"selectstart",
"stalled",
"submit",
"suspend",
"timeupdate",
"toggle",
"touchcancel",
"touchend",
"touchmove",
"touchstart",
"transitioncancel",
"transitionend",
"transitionrun",
"transitionstart",
"volumechange",
"waiting",
"wheel",
];
/**
* Patches the event object to ensure proper event propagation and compatibility
* across Shadow DOM boundaries.
*
* @param {Event} event - The event object to patch.
*/
function patchEvent(event) {
event.stopPropagation = function () {
const e = this;
if (!e.cancelBubble) {
e.cancelBubble = true;
}
};
// `composed` property of the Event interface returns a boolean value which indicates whether or not the event
// will propagate across the shadow DOM boundary into the standard DOM so we set composed=true to allow the event
// to cross Shadow DOM boundaries
if (event.composed === undefined) {
event.composed = true;
}
}
/**
* Retargets the event's target to the original element that triggered the event.
* This is crucial for React to correctly identify which element initiated the event,
* especially when events originate from within a Shadow DOM.
*
* @param {Event} event - The event object to retarget.
* @returns {Event} - The modified event with the retargeted `target`.
*/
function retarget(event) {
// Get the path of elements the event traveled through
const path = event.composedPath();
// Override the target property to point to the original element
// that triggered the event (path[0] is the deepest element)
Object.defineProperty(event, "target", {
enumerable: true, //make the property visible when iterating over the event object
configurable: true, // Allows further modification or deletion of this property if needed
get: () => path[0], // ensure that event.target always points to the original triggering element.
});
// Apply additional patches to the event to ensure proper behavior.
patchEvent(event);
// Return the modified event with the updated `target`.
return event;
}
// Iterate over each event type and add an event listener to the Shadow Root.
// The listener retargets the event during the capturing phase to ensure it happens before React's synthetic event system.
eventTypes.forEach((eventType) => {
shadowRoot.addEventListener(
eventType,
(event) => {
retarget(event);
},
true // Use capture phase to intercept events before they reach React.
);
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment