Repository: metabase/metabase
Pull request #54650: Replace findDOMNode in core components
Author: login: heypoom
Labels: name: .Team/Embedding
name: run-tests
name: double-backport
Created: 2025-03-04T14:20:38Z | Merged: 2025-03-07T06:54:27Z

Closes EMB-214

The following React components under `frontend/src/metabase/components` and `frontend/src/metabase/core/components` uses `React.findDOMNode` which is removed in React 19:
- OnClickOutsideWrapper.jsx
- TextWidget.tsx
- TokenField.tsx
- AccordionList.jsx

This PR removes use of `findDOMNode` in these four components to unblock React 19.


Diff:
diff --git a/frontend/src/metabase/components/OnClickOutsideWrapper/OnClickOutsideWrapper.jsx b/frontend/src/metabase/components/OnClickOutsideWrapper/OnClickOutsideWrapper.jsx
index 0266f8765cdc..43ba59bad07b 100644
--- a/frontend/src/metabase/components/OnClickOutsideWrapper/OnClickOutsideWrapper.jsx
+++ b/frontend/src/metabase/components/OnClickOutsideWrapper/OnClickOutsideWrapper.jsx
@@ -1,12 +1,12 @@
 import PropTypes from "prop-types";
-import { Children, Component } from "react";
-import ReactDOM from "react-dom";
+import { Children, Component, createRef } from "react";
 
 import {
   RENDERED_POPOVERS,
   removePopoverData,
   shouldClosePopover,
 } from "metabase/hooks/use-sequenced-content-close-handler";
+import { Box } from "metabase/ui";
 
 export default class OnClickOutsideWrapper extends Component {
   static propTypes = {
@@ -16,10 +16,12 @@ export default class OnClickOutsideWrapper extends Component {
     ignoreElement: PropTypes.object,
   };
 
+  contentRef = createRef();
+
   componentDidMount() {
     // necessary to ignore click events that fire immediately, causing modals/popovers to close prematurely
     this._timeout = setTimeout(() => {
-      const contentEl = ReactDOM.findDOMNode(this);
+      const contentEl = this.contentRef.current;
 
       this.popoverData = {
         contentEl,
@@ -58,6 +60,10 @@ export default class OnClickOutsideWrapper extends Component {
   };
 
   render() {
-    return Children.only(this.props.children);
+    return (
+      <Box ref={this.contentRef} display="contents">
+        {Children.only(this.props.children)}
+      </Box>
+    );
   }
 }
diff --git a/frontend/src/metabase/components/TextWidget/TextWidget.tsx b/frontend/src/metabase/components/TextWidget/TextWidget.tsx
index 17f71f3e6224..7005d9c064ef 100644
--- a/frontend/src/metabase/components/TextWidget/TextWidget.tsx
+++ b/frontend/src/metabase/components/TextWidget/TextWidget.tsx
@@ -1,5 +1,4 @@
-import { Component } from "react";
-import ReactDOM from "react-dom";
+import { Component, createRef } from "react";
 import { t } from "ttag";
 
 import { forceRedraw } from "metabase/lib/dom";
@@ -27,6 +26,8 @@ export class TextWidget extends Component<TextWidgetProps, State> {
     disabled: false,
   };
 
+  inputRef = createRef<HTMLInputElement>();
+
   constructor(props: TextWidgetProps) {
     super(props);
 
@@ -44,7 +45,7 @@ export class TextWidget extends Component<TextWidgetProps, State> {
     if (nextProps.value !== this.props.value) {
       this.setState({ value: nextProps.value }, () => {
         // HACK: Address Safari rendering bug which causes https://github.com/metabase/metabase/issues/5335
-        forceRedraw(ReactDOM.findDOMNode(this));
+        forceRedraw(this.inputRef.current);
       });
     }
   }
@@ -98,6 +99,7 @@ export class TextWidget extends Component<TextWidgetProps, State> {
         }}
         placeholder={isEditing ? t`Enter a default value…` : defaultPlaceholder}
         disabled={disabled}
+        ref={this.inputRef}
       />
     );
   }
diff --git a/frontend/src/metabase/components/TokenField/TokenField.tsx b/frontend/src/metabase/components/TokenField/TokenField.tsx
index 383ab6d51eaa..f5c571c812c2 100644
--- a/frontend/src/metabase/components/TokenField/TokenField.tsx
+++ b/frontend/src/metabase/components/TokenField/TokenField.tsx
@@ -1,7 +1,6 @@
 import cx from "classnames";
-import { Component } from "react";
-import * as React from "react";
-import { findDOMNode } from "react-dom";
+import type * as React from "react";
+import { Component, createRef } from "react";
 import _ from "underscore";
 
 import TippyPopover from "metabase/components/Popover/TippyPopover";
@@ -89,8 +88,7 @@ const defaultStyleValue = {
 };
 
 class _TokenField extends Component<TokenFieldProps, TokenFieldState> {
-  inputRef: React.RefObject<HTMLInputElement>;
-  scrollElement = null;
+  inputRef = createRef<HTMLInputElement>();
 
   constructor(props: TokenFieldProps) {
     super(props);
@@ -104,8 +102,6 @@ class _TokenField extends Component<TokenFieldProps, TokenFieldState> {
       isAllSelected: false,
       listIsHovered: false,
     };
-
-    this.inputRef = React.createRef();
   }
 
   UNSAFE_componentWillMount() {
@@ -444,19 +440,9 @@ class _TokenField extends Component<TokenFieldProps, TokenFieldState> {
     return JSON.stringify(v1) === JSON.stringify(v2);
   }
 
-  componentDidUpdate(prevProps: TokenFieldProps, prevState: TokenFieldState) {
+  componentDidUpdate(prevProps: TokenFieldProps) {
     const input = this.inputRef.current;
 
-    if (
-      prevState.selectedOptionValue !== this.state.selectedOptionValue &&
-      this.scrollElement != null
-    ) {
-      const element = findDOMNode(this.scrollElement);
-      if (element && isObscured(element) && element instanceof Element) {
-        element.scrollIntoView({ block: "nearest" });
-      }
-    }
-
     // if we added a value then scroll to the last item (the input)
     if (this.props.value.length > prevProps.value.length) {
       if (input && isObscured(input)) {
diff --git a/frontend/src/metabase/core/components/AccordionList/AccordionList.jsx b/frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
index 9159ed702505..766046d07b87 100644
--- a/frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
+++ b/frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
@@ -1,7 +1,6 @@
 import { getIn } from "icepick";
 import PropTypes from "prop-types";
-import { Component } from "react";
-import ReactDOM from "react-dom";
+import { Component, createRef } from "react";
 import { CellMeasurer, CellMeasurerCache, List } from "react-virtualized";
 import _ from "underscore";
 
@@ -45,6 +44,9 @@ export default class AccordionList extends Component {
       fixedWidth: true,
       minHeight: 10,
     });
+
+    /** @type {React.RefObject<HTMLDivElement>} */
+    this.listRootRef = createRef();
   }
 
   static propTypes = {
@@ -135,8 +137,6 @@ export default class AccordionList extends Component {
   };
 
   componentDidMount() {
-    this.container = ReactDOM.findDOMNode(this);
-
     // NOTE: for some reason the row heights aren't computed correctly when
     // first rendering, so force the list to update
     this._forceUpdateList();
@@ -144,11 +144,11 @@ export default class AccordionList extends Component {
     // Use list.scrollToRow instead of the scrollToIndex prop since the
     // causes the list's scrolling to be pinned to the selected row
     setTimeout(() => {
-      const hasFocusedChildren = this.container.contains(
-        document.activeElement,
-      );
+      const container = this._getListContainerElement();
+
+      const hasFocusedChildren = container?.contains(document.activeElement);
       if (!hasFocusedChildren && this.props.hasInitialFocus) {
-        this.container.focus();
+        container?.focus();
       }
 
       const index = this._initialSelectedRowIndex;
@@ -181,6 +181,15 @@ export default class AccordionList extends Component {
     }
   }
 
+  /** @returns {HTMLDivElement | null} */
+  _getListContainerElement() {
+    const element = this.isVirtualized()
+      ? this._list?.Grid?._scrollingContainer
+      : this.listRootRef.current;
+
+    return element ?? null;
+  }
+
   // resets the row height cache when the displayed rows change
   _clearRowHeightCache() {
     this._cache.clearAll();
@@ -603,7 +612,7 @@ export default class AccordionList extends Component {
   // Because of virtualization, focused search input can be removed which does not trigger blur event.
   // We need to restore focus on the component root container to make keyboard navigation working
   handleSearchRemoval = () => {
-    this.container?.focus();
+    this._getListContainerElement()?.focus();
   };
 
   render() {
@@ -637,6 +646,9 @@ export default class AccordionList extends Component {
             ...style,
           }}
           data-testid={testId}
+          ref={element => {
+            this.listRootRef.current = element;
+          }}
         >
           {rows.map((row, index) => (
             <AccordionListCell
@@ -686,7 +698,9 @@ export default class AccordionList extends Component {
     return (
       <List
         id={id}
-        ref={list => (this._list = list)}
+        ref={list => {
+          this._list = list;
+        }}
         className={className}
         style={{ ...defaultListStyle, ...style }}
         containerStyle={{ pointerEvents: "auto" }}


Reviews:
user: login: heypoom
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T15:11:26Z
user: login: rafpaf
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T15:19:17Z
user: login: npretto
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-04T16:21:29Z
user: login: npretto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T16:22:44Z
user: login: heypoom
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T16:39:57Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T05:36:03Z
user: login: heypoom
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T06:33:31Z
user: login: heypoom
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T06:40:03Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T06:50:11Z
user: login: heypoom
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T06:52:47Z
user: login: heypoom
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T11:07:08Z
user: login: heypoom
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T18:05:46Z
user: login: heypoom
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T18:12:19Z
user: login: WiNloSt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T08:10:59Z

Review comments:
diff_hunk: @@ -58,6 +59,8 @@ export default class OnClickOutsideWrapper extends Component {
   };
 
   render() {
-    return Children.only(this.props.children);
+    return (
+      <div ref={this.contentRef}>{Children.only(this.props.children)}</div>
path: frontend/src/metabase/components/OnClickOutsideWrapper/OnClickOutsideWrapper.jsx
user: login: heypoom
body: Adds a container so we can attach a ref.
created_at: 2025-03-04T14:30:54Z
author_association: MEMBER
diff_hunk: @@ -89,8 +88,7 @@ const defaultStyleValue = {
 };
 
 class _TokenField extends Component<TokenFieldProps, TokenFieldState> {
-  inputRef: React.RefObject<HTMLInputElement>;
-  scrollElement = null;
path: frontend/src/metabase/components/TokenField/TokenField.tsx
user: login: heypoom
body: We never assign to `scrollElement` here.
created_at: 2025-03-04T14:35:14Z
author_association: MEMBER
diff_hunk: @@ -444,19 +440,9 @@ class _TokenField extends Component<TokenFieldProps, TokenFieldState> {
     return JSON.stringify(v1) === JSON.stringify(v2);
   }
 
-  componentDidUpdate(prevProps: TokenFieldProps, prevState: TokenFieldState) {
+  componentDidUpdate(prevProps: TokenFieldProps) {
     const input = this.inputRef.current;
 
-    if (
-      prevState.selectedOptionValue !== this.state.selectedOptionValue &&
-      this.scrollElement != null
-    ) {
-      const element = findDOMNode(this.scrollElement);
-      if (element && isObscured(element) && element instanceof Element) {
-        element.scrollIntoView({ block: "nearest" });
-      }
-    }
path: frontend/src/metabase/components/TokenField/TokenField.tsx
user: login: heypoom
body: We never assign to `this.scrollElement` and it is always `null` so this logic is unused
created_at: 2025-03-04T14:35:32Z
author_association: MEMBER
diff_hunk: @@ -637,6 +636,7 @@ export default class AccordionList extends Component {
             ...style,
           }}
           data-testid={testId}
+          ref={this.containerRef}
path: frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
user: login: heypoom
body: Attach the ref to `AccordionListRoot`
created_at: 2025-03-04T14:36:29Z
author_association: MEMBER
diff_hunk: @@ -135,7 +136,9 @@ export default class AccordionList extends Component {
   };
 
   componentDidMount() {
-    this.container = ReactDOM.findDOMNode(this);
+    const container = this.isVirtualized()
+      ? this._list?.Grid?._scrollingContainer
+      : this.listRootRef.current;
path: frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
user: login: heypoom
body: `findDOMNode` resolves to the grid's scrolling container if it is virtualized, otherwise it resolves to the accordion root
created_at: 2025-03-04T15:07:45Z
author_association: MEMBER
diff_hunk: @@ -181,6 +180,12 @@ export default class AccordionList extends Component {
     }
   }
 
+  _getRootContainer() {
+    return this.isVirtualized()
+      ? this._list?.Grid?._scrollingContainer
+      : this.listRootRef.current;
+  }
path: frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
user: login: heypoom
body: `findDOMNode` was resolving to the scrolling container when virtualized, otherwise it resolves to the accordion root.
created_at: 2025-03-04T15:11:11Z
author_association: MEMBER
diff_hunk: @@ -58,6 +59,8 @@ export default class OnClickOutsideWrapper extends Component {
   };
 
   render() {
-    return Children.only(this.props.children);
+    return (
+      <div ref={this.contentRef}>{Children.only(this.props.children)}</div>
path: frontend/src/metabase/components/OnClickOutsideWrapper/OnClickOutsideWrapper.jsx
user: login: npretto
body: I hope this extra div won't mess up some layouts 👀 
created_at: 2025-03-04T16:20:17Z
author_association: MEMBER
diff_hunk: @@ -58,6 +59,8 @@ export default class OnClickOutsideWrapper extends Component {
   };
 
   render() {
-    return Children.only(this.props.children);
+    return (
+      <div ref={this.contentRef}>{Children.only(this.props.children)}</div>
path: frontend/src/metabase/components/OnClickOutsideWrapper/OnClickOutsideWrapper.jsx
user: login: npretto
body: (I rerun the tests to see if they were flaky or connected to this)
created_at: 2025-03-04T16:22:44Z
author_association: MEMBER
diff_hunk: @@ -58,6 +59,8 @@ export default class OnClickOutsideWrapper extends Component {
   };
 
   render() {
-    return Children.only(this.props.children);
+    return (
+      <div ref={this.contentRef}>{Children.only(this.props.children)}</div>
path: frontend/src/metabase/components/OnClickOutsideWrapper/OnClickOutsideWrapper.jsx
user: login: heypoom
body: I think it is likely related as most of the tests are failing, I'll take a look tomorrow 🙏🏻 
created_at: 2025-03-04T16:39:57Z
author_association: MEMBER
diff_hunk: @@ -181,6 +180,12 @@ export default class AccordionList extends Component {
     }
   }
 
+  _getRootContainer() {
+    return this.isVirtualized()
+      ? this._list?.Grid?._scrollingContainer
path: frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
user: login: WiNloSt
body: How did you come up with this value? How did you test it?
created_at: 2025-03-05T05:35:57Z
author_association: MEMBER
diff_hunk: @@ -181,6 +180,12 @@ export default class AccordionList extends Component {
     }
   }
 
+  _getRootContainer() {
+    return this.isVirtualized()
+      ? this._list?.Grid?._scrollingContainer
path: frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
user: login: heypoom
body: I checked the source code of `react-virtualized` and found that `findDOMNode` was accessing this scrolling container. I will actually update this as 9.22.5 (the version I just updated to yesterday in #54645) now exposes the public `elementRef` prop (which is a lot better than using `_scrollingContainer` as that is private)

https://github.com/bvaughn/react-virtualized/blob/9.22.5/source/Grid/Grid.js#L225-L226
created_at: 2025-03-05T06:33:31Z
author_association: MEMBER
diff_hunk: @@ -181,6 +180,12 @@ export default class AccordionList extends Component {
     }
   }
 
+  _getRootContainer() {
+    return this.isVirtualized()
+      ? this._list?.Grid?._scrollingContainer
path: frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
user: login: heypoom
body: Fixed in https://github.com/metabase/metabase/pull/54650/commits/73c8669b82fb41685a0c14e801d1d3105376fe61 and  https://github.com/metabase/metabase/pull/54650/commits/3b9d934c5fb4d860f4fe7089fb3f204af0efc989
created_at: 2025-03-05T06:40:03Z
author_association: MEMBER
diff_hunk: @@ -181,6 +185,12 @@ export default class AccordionList extends Component {
     }
   }
 
+  _getRootContainer() {
path: frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
user: login: WiNloSt
body: do we need this function at all? Can we, in theory, pass the same ref to both elements whether they're virtualized list or not.

@oisincoveney did this when refactoring ref not too long ago.

You can probably pass `listRootRef` to both root containers. By using a function you can set the element to multiple refs there.

https://github.com/metabase/metabase/blob/f4347d984c89aee9a12bc5445907bfef52683b42/frontend/src/metabase/components/ExplicitSize/ExplicitSize.tsx#L249-L259
created_at: 2025-03-05T06:50:08Z
author_association: MEMBER
diff_hunk: @@ -181,6 +185,12 @@ export default class AccordionList extends Component {
     }
   }
 
+  _getRootContainer() {
path: frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
user: login: heypoom
body: That might work, I'll give that a try once I get tests to pass with the current version 👍🏻 
created_at: 2025-03-05T06:52:47Z
author_association: MEMBER
diff_hunk: @@ -58,6 +59,8 @@ export default class OnClickOutsideWrapper extends Component {
   };
 
   render() {
-    return Children.only(this.props.children);
+    return (
+      <div ref={this.contentRef}>{Children.only(this.props.children)}</div>
path: frontend/src/metabase/components/OnClickOutsideWrapper/OnClickOutsideWrapper.jsx
user: login: heypoom
body: It did mess up layouts 😅, using `display="contents"` to preserve the layout does work though. Fixed in https://github.com/metabase/metabase/pull/54650/commits/f2f083621766845ece9c554cbb829900aa8603c3
created_at: 2025-03-05T11:07:07Z
author_association: MEMBER
diff_hunk: @@ -181,6 +185,12 @@ export default class AccordionList extends Component {
     }
   }
 
+  _getRootContainer() {
path: frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
user: login: heypoom
body: @WiNloSt I just noticed that `elementRef` does not exist in the NPM package of `react-virtualized` (even though it exists in the source code on the GitHub tag!), I confirmed that `package.json` version resolves to `9.22.5` and it doesn't contain that prop 😭 

I updated the PR to use the `_scrollingContainer` instead. I just realized that the focus() doesn't work (as the ref was `null`) thanks to a genuine Loki test failure.
created_at: 2025-03-06T18:05:46Z
author_association: MEMBER
diff_hunk: @@ -181,6 +180,12 @@ export default class AccordionList extends Component {
     }
   }
 
+  _getRootContainer() {
+    return this.isVirtualized()
+      ? this._list?.Grid?._scrollingContainer
path: frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
user: login: heypoom
body: update: I had to revert the `elementRef` prop as it isn't really available in the 9.22.5 version despite the above diffs in their GitHub tag 😢 

https://github.com/metabase/metabase/pull/54650#discussion_r1983833493
created_at: 2025-03-06T18:12:19Z
author_association: MEMBER
diff_hunk: @@ -181,6 +185,12 @@ export default class AccordionList extends Component {
     }
   }
 
+  _getRootContainer() {
path: frontend/src/metabase/core/components/AccordionList/AccordionList.jsx
user: login: WiNloSt
body: ~I'm not sure I follow. That `elementRef` is just a variable we defined~. It could be any other name.
https://github.com/metabase/metabase/blob/f4347d984c89aee9a12bc5445907bfef52683b42/frontend/src/metabase/components/ExplicitSize/ExplicitSize.tsx#L79

The point I wanted to show you is how we could leverage function ref, rather than passing a ref which only works with one single ref.

Edit: Ok, the `elementRef` thing you mentioned was a pure coincidence. So, I was confused.
created_at: 2025-03-07T08:10:59Z
author_association: MEMBER

PR comments:
user: login: trunk-io[bot]
created_at: 2025-03-04T14:51:37Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://img.shields.io/badge/8819_tests-fb68bc06-purple)](https://app.trunk.io/metabase/flaky-tests/pr/54650?repo=metabase/metabase&commitHash=fb68bc0630d2748dce0df9607db263d71a6fccb9&utm_source=gh-comment) &ensp; [![Static Badge](https://img.shields.io/badge/0-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/54650?repo=metabase/metabase&commitHash=fb68bc0630d2748dce0df9607db263d71a6fccb9&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/1-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/54650?repo=metabase/metabase&commitHash=fb68bc0630d2748dce0df9607db263d71a6fccb9&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/54650?repo=metabase/metabase&commitHash=fb68bc0630d2748dce0df9607db263d71a6fccb9&utm_source=gh-comment&runConclusion=QUARANTINED)


| Flaky Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>scenarios > dashboard warn before leave should warn a user before leaving after adding, removed, moving, or duplicating a tab</code> | Unable to find an element with the text "You're editing this dashboard" within 4 seconds. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54650?repo=metabase/metabase&commitHash=fb68bc0630d2748dce0df9607db263d71a6fccb9&utm_source=gh-comment&runConclusion=FAILURE&runId=d51c53c8-eb6a-5fd8-9b6f-0e684a59051c) |
<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54650?repo=metabase/metabase&commitHash=fb68bc0630d2748dce0df9607db263d71a6fccb9&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE
user: login: WiNloSt
created_at: 2025-03-05T05:27:40Z
body: it [says here](https://www.notion.so/metabase/117-Make-Embedding-SDK-compatible-with-React-19-1ab69354c901805a93b2f5eea81302be?pvs=4#1ab69354c9018086a82ad60b1546742a) we have 10 components that uses `findDomNode`, why do we only replace 4 here?
author_association: MEMBER
user: login: heypoom
created_at: 2025-03-05T06:21:10Z
body: > it [says here](https://www.notion.so/metabase/117-Make-Embedding-SDK-compatible-with-React-19-1ab69354c901805a93b2f5eea81302be?pvs=4#1ab69354c9018086a82ad60b1546742a) we have 10 components that uses findDomNode, why do we only replace 4 here?

@WiNloSt we split this into two tasks: "Replace use of findDOMNode in core components (EMB-214)" and "Replace use of findDOMNode in visualization components (EMB-213)" - it's better to do them separately as findDOMNode replacements can easily cause tests to fail, and DashViz can review the viz components separately
author_association: MEMBER
user: login: heypoom
created_at: 2025-03-05T13:52:29Z
body: Java 21 EE appdb tests and loki tests are failing, hmm. Let me merge master into this branch
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-06T18:22:31Z
body: 
  ## e2e tests failed on `fb68bc0630d2748dce0df9607db263d71a6fccb9-1`

  [e2e test run](https://github.com/metabase/metabase/actions/runs/13705305069?pr=54650)

  File | Test Name
  ---- | ---------
  `dashboard.cy.spec.js` | (_flaky_) scenarios > dashboard > warn before leave > should warn a user before leaving after adding, removed, moving, or duplicating a tab

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-07T07:49:53Z
body: 🚀 This should also be released by [v0.53.6](https://github.com/metabase/metabase/milestone/315)
author_association: CONTRIBUTOR