@samitouri / QOS-React / commits / 947e7962ad

fix[devtools/useModalDismissSignal]: use getRootNode for shadow root case support (#28145)

In our custom implementation for handling modals dismiss signal, we use element's `ownerDocument` field, which expectedly doesn't work well with shadow root. Now using [`getRootNode`](https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode) instead of `ownerDocument` to support shadow root case. Without this, if RDT Frontend is hosted inside the shadow root, the modal gets closed after any click, including on the buttons hosted by modal: https://github.com/facebook/react/blob/00d42ac3542179c55f936f395ede7abaeb5900a3/packages/react-devtools-shared/src/devtools/views/hooks.js#L228-L238 Test plan: - Modals work as expected for Chrome DevTools integration - Modals work as expected at every other surfaces: browser extension, electron wrapper for RN, inline version for web

Ruslan Lesiutin committed Feb 12, 2024 at 16:56 UTC 947e7962ad43bd0fe8826c48d94a9df8d5206850
1 file changed +14 -11
packages/react-devtools-shared/src/devtools/views/hooks.js
+14 -11
@@ -219,15 +219,18 @@ export function useModalDismissSignal(
219 return () => {};
220 }
221
222 - const handleDocumentKeyDown = (event: any) => {
222 + const handleRootNodeKeyDown = (event: KeyboardEvent) => {
223 if (event.key === 'Escape') {
224 dismissCallback();
225 }
226 };
227
228 - const handleDocumentClick = (event: any) => {
228 + const handleRootNodeClick: MouseEventHandler = event => {
229 if (
230 modalRef.current !== null &&
231 + /* $FlowExpectedError[incompatible-call] Instead of dealing with possibly multiple realms
232 + and multiple Node references to comply with Flow (e.g. checking with `event.target instanceof Node`)
233 + just delegate it to contains call */
234 !modalRef.current.contains(event.target)
235 ) {
236 event.stopPropagation();
@@ -237,7 +240,7 @@ export function useModalDismissSignal(
240 }
241 };
242
240 - let ownerDocument = null;
243 + let modalRootNode = null;
244
245 // Delay until after the current call stack is empty,
246 // in case this effect is being run while an event is currently bubbling.
@@ -248,12 +251,12 @@ export function useModalDismissSignal(
251 // It's important to listen to the ownerDocument to support the browser extension.
252 // Here we use portals to render individual tabs (e.g. Profiler),
253 // and the root document might belong to a different window.
251 - const div = modalRef.current;
252 - if (div != null) {
253 - ownerDocument = div.ownerDocument;
254 - ownerDocument.addEventListener('keydown', handleDocumentKeyDown);
254 + const modalDOMElement = modalRef.current;
255 + if (modalDOMElement != null) {
256 + modalRootNode = modalDOMElement.getRootNode();
257 + modalRootNode.addEventListener('keydown', handleRootNodeKeyDown);
258 if (dismissOnClickOutside) {
256 - ownerDocument.addEventListener('click', handleDocumentClick, true);
259 + modalRootNode.addEventListener('click', handleRootNodeClick, true);
260 }
261 }
262 }, 0);
@@ -263,9 +266,9 @@ export function useModalDismissSignal(
266 clearTimeout(timeoutID);
267 }
268
266 - if (ownerDocument !== null) {
267 - ownerDocument.removeEventListener('keydown', handleDocumentKeyDown);
268 - ownerDocument.removeEventListener('click', handleDocumentClick, true);
269 + if (modalRootNode !== null) {
270 + modalRootNode.removeEventListener('keydown', handleRootNodeKeyDown);
271 + modalRootNode.removeEventListener('click', handleRootNodeClick, true);
272 }
273 };
274 }, [modalRef, dismissCallback, dismissOnClickOutside]);