From 3fe82786fc82b14ef5e4f09df7261f05f363afe3 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Mon, 12 Feb 2024 16:56:28 +0000 Subject: [PATCH] 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 --- .../src/devtools/views/hooks.js | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/hooks.js b/packages/react-devtools-shared/src/devtools/views/hooks.js index 7189dd7acf94d..54d22b23dd02e 100644 --- a/packages/react-devtools-shared/src/devtools/views/hooks.js +++ b/packages/react-devtools-shared/src/devtools/views/hooks.js @@ -219,15 +219,18 @@ export function useModalDismissSignal( return () => {}; } - const handleDocumentKeyDown = (event: any) => { + const handleRootNodeKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { dismissCallback(); } }; - const handleDocumentClick = (event: any) => { + const handleRootNodeClick: MouseEventHandler = event => { if ( modalRef.current !== null && + /* $FlowExpectedError[incompatible-call] Instead of dealing with possibly multiple realms + and multiple Node references to comply with Flow (e.g. checking with `event.target instanceof Node`) + just delegate it to contains call */ !modalRef.current.contains(event.target) ) { event.stopPropagation(); @@ -237,7 +240,7 @@ export function useModalDismissSignal( } }; - let ownerDocument = null; + let modalRootNode = null; // Delay until after the current call stack is empty, // in case this effect is being run while an event is currently bubbling. @@ -248,12 +251,12 @@ export function useModalDismissSignal( // It's important to listen to the ownerDocument to support the browser extension. // Here we use portals to render individual tabs (e.g. Profiler), // and the root document might belong to a different window. - const div = modalRef.current; - if (div != null) { - ownerDocument = div.ownerDocument; - ownerDocument.addEventListener('keydown', handleDocumentKeyDown); + const modalDOMElement = modalRef.current; + if (modalDOMElement != null) { + modalRootNode = modalDOMElement.getRootNode(); + modalRootNode.addEventListener('keydown', handleRootNodeKeyDown); if (dismissOnClickOutside) { - ownerDocument.addEventListener('click', handleDocumentClick, true); + modalRootNode.addEventListener('click', handleRootNodeClick, true); } } }, 0); @@ -263,9 +266,9 @@ export function useModalDismissSignal( clearTimeout(timeoutID); } - if (ownerDocument !== null) { - ownerDocument.removeEventListener('keydown', handleDocumentKeyDown); - ownerDocument.removeEventListener('click', handleDocumentClick, true); + if (modalRootNode !== null) { + modalRootNode.removeEventListener('keydown', handleRootNodeKeyDown); + modalRootNode.removeEventListener('click', handleRootNodeClick, true); } }; }, [modalRef, dismissCallback, dismissOnClickOutside]);