Skip to content

MM-69835 Remove all usage of findDOMNode - #11

Open
hmhealey wants to merge 6 commits into
MM-69835-2from
MM-69835-3
Open

MM-69835 Remove all usage of findDOMNode#11
hmhealey wants to merge 6 commits into
MM-69835-2from
MM-69835-3

Conversation

@hmhealey

@hmhealey hmhealey commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

The last big change to get this working with React 19 is to remove usage of findDOMNode. This is also the one that's the most likely to be a breaking change because it makes it so that we need to pass HTML elements around or use querySelector to get them which I did in a few places.

I'm mostly relying on the tests in mattermost/mattermost#37758 to ensure that I haven't broken anything here, so fingers crossed we don't run into any issues with how plugins or anything else use these components.

Ticket Link

https://mattermost.atlassian.net/browse/MM-69835

I removed the container prop from Modal, but we don't seem to use that ourselves
so it's probably fine to do. It was marked as private anyway.
This used to come from react-overlays, but it's not part of the newer version.
@hmhealey hmhealey changed the title Mm 69835 3 Remove all usage of findDOMNode Aug 4, 2026
@hmhealey hmhealey changed the title Remove all usage of findDOMNode MM-69835 Remove all usage of findDOMNode Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current versions of react-overlays and react-transition-group both use findDOMNode, so I've had to update them. In some cases, they still might try to call it if we pass a React element as a prop instead of an HTMLElement, but my hope is that we only ever use them through react-bootstrap so that won't happen.

The changes here are just porting the previous patch over to the new version of react-overlays. Note that this patch doesn't actually affect the code used at runtime, and the web app is going to actually have its own patch for this library

Comment thread src/CarouselItem.js
return <div {...props} className={classNames(className, classes)} />;
return (
<div
ref={this.containerRef}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these are straightforward because there's already a DOM node that we can attach a ref to instead of using findDOMNode

Comment thread src/Collapse.js
const handleExit = createChainedFunction(this.handleExit, onExit);
const handleExiting = createChainedFunction(this.handleExiting, onExiting);

const ref = makeMergedRef([this.childRef, getElementRef(children)]);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a case where we need to inject a ref and hope that it returns a DOM node. I think this may cause an error if Transition doesn't forward its ref to an HTML element, but I don't think we pas anything as Transition other than Fade and Collapse, both of which now forward refs

Comment thread src/Dropdown.js

focus() {
const toggle = ReactDOM.findDOMNode(this.toggle);
const toggle = this.containerRef.current.querySelector(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, I didn't think we can pass a ref around for this, so I went with searching the DOM and hoping that the a11y for the toggle button is set up correctly. We use this component in 1 place in the web app (which has tests thankfully) and one or two plugins, so I'm hoping this doesn't break

Comment thread src/DropdownMenu.js
}
}

function RootCloseWrapper({

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new version of react-overlays doesn't have its own RootCloseWrapper, and I think it's too much work to migrate this to a functional component

Comment thread src/Modal.js
function DialogTransition(props) {
return <Fade {...props} timeout={Modal.TRANSITION_DURATION} />;
}
const DialogTransition = React.forwardRef((props, ref) => (

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These forwardRefs aren't actually needed once we're using React 19, but I wanted to make sure this all works with React 18 to ease in the upgrade

Comment thread src/Modal.js
prefix(props, 'backdrop'),
backdropClassName,
inClassName
renderBackdrop={backdropProps => (

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a change in the API for the react-overlays Modal because:

  1. The backdropClassName prop was replaced by renderBackdrop
  2. Instead of passing a child that gets cloned to inject props, there's now a renderDialog prop.

I think I've got them both working the same now since I didn't want to change the API of our React Bootstrap components at all

Comment thread src/ModalDialog.js
Comment on lines +21 to +22
bsClass: PropTypes.string,
bsSize: PropTypes.oneOf(SIZES)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should've been in #12, so just ignore them for now

Comment thread src/Overlay.js
{child}
<BaseOverlay {...props} target={null} transition={transition}>
{({ props: overlayProps }) => (
<div ref={overlayProps.ref} style={{ display: 'content' }}>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, I didn't have an option for attaching a ref to the arbitrary child or using a11y attributes, so I instead opted to add an extra div and just hope that it can properly match the size of its child by using display: content

@hmhealey

hmhealey commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai Review this

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

@hmhealey I will review pull request #11. I will focus on the findDOMNode replacements and compatibility risks.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change updates overlay dependencies and Escape handling. It replaces findDOMNode usage with explicit refs, adopts newer overlay render APIs, converts transition components to forwarded refs, and updates related tests.

Changes

React ref and overlay modernization

Layer / File(s) Summary
Shared overlay contracts
package.json, patches/react-overlays+5.2.1.patch, src/utils/mergeRefs.js
Dependencies and patched overlay builds support updated transition APIs, modern Escape events, and merged React refs.
Component DOM refs
src/CarouselItem.js, src/Collapse.js, src/Nav.js
Components use explicit refs for transition cleanup, collapse handlers, navigation lookup, and child ref forwarding.
Dropdown and overlay flow
src/Dropdown.js, src/DropdownMenu.js, src/Overlay.js, src/OverlayTrigger.js, test/OverlayTriggerSpec.js
Dropdown and overlay components use container refs, merged refs, render props, useRootClose, and modern context tests.
Modal and transition components
src/Fade.js, src/Modal.js, src/ModalDialog.js, test/FadeSpec.js, test/ModalSpec.js, test/index.js
Fade and modal components use forwarded refs and updated render contracts. Tests inspect rendered elements and no longer suppress findDOMNode warnings.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: removing all usage of findDOMNode.
Description check ✅ Passed The description explains the findDOMNode removal, React 19 compatibility goal, potential breaking changes, testing, and ticket reference.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch MM-69835-3

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

package.json

Parsing error: Unexpected token, expected ";"

1 | {

2 | "name": "react-bootstrap",
| ^
3 | "version": "0.32.4",
4 | "description": "Bootstrap 3 components built with React",
5 | "repository": {


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Collapse.js (1)

201-216: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore the DOM node in Collapse transition callbacks.

With react-transition-group@4.4.5, nodeRef omits the DOM node. Enter callbacks receive isAppearing, exit callbacks receive undefined, and addEndListener receives only done. Wrap all exposed callbacks to pass this.childRef.current, and add regression tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Collapse.js` around lines 201 - 216, Update the callback wrappers in
Collapse’s transition setup to prepend this.childRef.current to onEnter,
onEntering, onEntered, onExit, onExiting, and addEndListener arguments,
preserving isAppearing for enter callbacks and undefined for exit callbacks.
Ensure consumers receive the DOM node despite nodeRef, and add regression
coverage for these callback signatures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 129: Update TabPane’s caller-supplied transition rendering to create and
pass a DOM nodeRef to the transition, and attach that ref to the pane element so
custom transitions avoid findDOMNode. Preserve the existing Fade, Collapse, and
modal transition behavior, while requiring custom transition components to
consume the provided nodeRef.

In `@src/Fade.js`:
- Around line 72-101: Update the Fade transition callbacks around the Transition
usage to restore the DOM-node first argument omitted when nodeRef is provided.
Wrap onEnter, onEntering, onEntered, onExit, onExiting, onExited, and
addEndListener so each invokes the supplied callback with childRef.current first
while preserving remaining arguments and existing behavior, including
Modal/DialogTransition forwarding. Add regression coverage for enter, exit, and
addEndListener callbacks.

In `@src/Modal.js`:
- Around line 279-294: Restore compatibility for dialogComponentClass values
that do not forward refs by preserving the previous wrapper behavior around the
Dialog render path. Ensure this._modal.dialog consistently references the
expected DOM dialog element so updateStyle() works when the modal enters, while
retaining support for ref-forwarding components and existing styling props.

In `@src/Overlay.js`:
- Around line 91-96: Update the BaseOverlay invocation and rendered overlay
element in Overlay: remove target={null} so triggerRef is passed through, and
spread the generated overlayProps onto the div alongside its ref and existing
style. Preserve the child rendering and current display style.

In `@src/OverlayTrigger.js`:
- Around line 297-299: Update the trigger rendering in OverlayTrigger so
triggerRef attaches to the actual geometry-bearing trigger DOM node via ref
forwarding or the established render-prop contract, rather than relying on
invalid display:'content'. If arbitrary children require a wrapper, give it an
explicit measurable inline layout contract and preserve trigger positioning; add
a regression test covering inline-trigger geometry.

In `@src/utils/mergeRefs.js`:
- Around line 10-20: Update makeMergedRef to retain cleanup functions returned
by callback refs, and return a composed cleanup that clears object refs, invokes
each returned cleanup, and calls ref(null) only for callbacks without cleanup.
Ensure useMergedRef uses this updated behavior without discarding the returned
cleanup.

---

Outside diff comments:
In `@src/Collapse.js`:
- Around line 201-216: Update the callback wrappers in Collapse’s transition
setup to prepend this.childRef.current to onEnter, onEntering, onEntered,
onExit, onExiting, and addEndListener arguments, preserving isAppearing for
enter callbacks and undefined for exit callbacks. Ensure consumers receive the
DOM node despite nodeRef, and add regression coverage for these callback
signatures.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8bad325a-fb9c-481e-a107-91abbddd8205

📥 Commits

Reviewing files that changed from the base of the PR and between b5d10b9 and 2dd3aae.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (19)
  • package.json
  • patches/react-overlays+0.9.3.patch
  • patches/react-overlays+5.2.1.patch
  • src/CarouselItem.js
  • src/Collapse.js
  • src/Dropdown.js
  • src/DropdownMenu.js
  • src/Fade.js
  • src/Modal.js
  • src/ModalDialog.js
  • src/Nav.js
  • src/Overlay.js
  • src/OverlayTrigger.js
  • src/utils/mergeRefs.js
  • test/DropdownSpec.js
  • test/FadeSpec.js
  • test/ModalSpec.js
  • test/OverlayTriggerSpec.js
  • test/index.js
💤 Files with no reviewable changes (3)
  • test/DropdownSpec.js
  • test/index.js
  • patches/react-overlays+0.9.3.patch

Comment thread package.json
"react-overlays": "^5.2.0",
"react-prop-types": "^0.4.0",
"react-transition-group": "^2.0.0",
"react-transition-group": "^4.4.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 6 'findDOMNode|nodeRef|<Transition\b|<CSSTransition\b' src test

Repository: mattermost/react-bootstrap

Length of output: 4347


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- transition-related source files ---'
rg -n -C 12 'findDOMNode|nodeRef|<Transition\b|<CSSTransition\b|Transition\s*=' src test

printf '%s\n' '--- TabPane implementation ---'
sed -n '180,275p' src/TabPane.js

printf '%s\n' '--- transition component definitions and exports ---'
rg -n -C 8 'function .*Transition|class .*Transition|const .*Transition|export .*Transition|react-transition-group' src

printf '%s\n' '--- package dependency and repository metadata ---'
sed -n '115,140p' package.json
git ls-files 'src/**' 'test/**' | sort

Repository: mattermost/react-bootstrap

Length of output: 24171


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- all animation and transition call sites ---'
rg -n -C 8 'animation|transition|backdropTransition|Transition|CSSTransition' src \
  -g '*.js'

printf '%s\n' '--- transition-related imports and JSX ---'
rg -n -C 5 'react-transition-group|<Transition\b|<CSSTransition\b|<Fade\b|<Collapse\b' src \
  -g '*.js'

printf '%s\n' '--- dynamic Transition verifier ---'
python3 - <<'PY'
from pathlib import Path
import re

for path in sorted(Path("src").rglob("*.js")):
    text = path.read_text()
    for match in re.finditer(r"<Transition\b|<CSSTransition\b", text):
        line = text.count("\n", 0, match.start()) + 1
        end = text.find(">", match.start())
        opening = text[match.start():end + 1] if end != -1 else text[match.start():]
        print(f"{path}:{line}: {opening.replace(chr(10), ' ')}")
    for match in re.finditer(r"const\s+Transition\s*=\s*([^;]+);", text):
        line = text.count("\n", 0, match.start()) + 1
        print(f"{path}:{line}: dynamic Transition = {match.group(1).strip()}")
PY

Repository: mattermost/react-bootstrap

Length of output: 43757


Pass a DOM nodeRef through TabPane transitions.

Fade, Collapse, and modal transitions supply nodeRef, but src/TabPane.js:196,234 renders a caller-supplied transition without it. A react-transition-group Transition can therefore call findDOMNode under React 19. Pass a ref to the pane and require custom transition components to use it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 129, Update TabPane’s caller-supplied transition
rendering to create and pass a DOM nodeRef to the transition, and attach that
ref to the pane element so custom transitions avoid findDOMNode. Preserve the
existing Fade, Collapse, and modal transition behavior, while requiring custom
transition components to consume the provided nodeRef.

Source: MCP tools

Comment thread src/Fade.js
Comment on lines +72 to +101
{
className,
children,
in: inProp = false,
timeout = 300,
mountOnEnter = false,
unmountOnExit = false,
appear = false,
...props
},
ref
) => {
const childRef = React.useRef(null);

const setChildRef = useMergedRef([childRef, getElementRef(children), ref]);

return (
<Transition {...props}>
<Transition
{...props}
in={inProp}
timeout={timeout}
mountOnEnter={mountOnEnter}
unmountOnExit={unmountOnExit}
appear={appear}
nodeRef={childRef}
>
{(status, innerProps) =>
React.cloneElement(children, {
...innerProps,
ref: setChildRef,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate consumers that expect transition callback arguments.
rg -n -C3 -g '*.js' \
  '(<Fade\b|addEndListener|onEnter|onEntering|onEntered|onExit|onExiting|onExited)' \
  src test

Repository: mattermost/react-bootstrap

Length of output: 20160


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- dependency declarations ---'
rg -n -C2 'react-transition-group' package.json package-lock.json yarn.lock 2>/dev/null || true

printf '%s\n' '--- Fade implementation ---'
cat -n src/Fade.js | sed -n '1,150p'

printf '%s\n' '--- Collapse implementation ---'
cat -n src/Collapse.js | sed -n '180,245p'

printf '%s\n' '--- transition callback tests and usages ---'
rg -n -C4 -g '*.js' \
  'addEndListener|onEnter|onEntering|onEntered|onExit|onExiting|onExited' \
  test/FadeSpec.js test/ModalSpec.js src/Modal.js src/Collapse.js test 2>/dev/null | sed -n '1,260p'

Repository: mattermost/react-bootstrap

Length of output: 17594


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

curl -fsSL \
  https://raw.githubusercontent.com/reactjs/react-transition-group/v4.4.5/src/Transition.js \
  -o "$tmpdir/Transition.js"

printf '%s\n' '--- react-transition-group 4.4.5 callback implementation ---'
rg -n -C12 \
  'nodeRef|onEnter|onEntering|onEntered|onExit|onExiting|onExited|addEndListener' \
  "$tmpdir/Transition.js" | sed -n '1,300p'

printf '%s\n' '--- repository callback documentation ---'
rg -n -C5 \
  'Callback fired|onEnter|onEntering|onEntered|onExit|onExiting|onExited|addEndListener' \
  README.md docs src test package.json 2>/dev/null | sed -n '1,320p'

Repository: mattermost/react-bootstrap

Length of output: 23094


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
def react_transition_group_args(node_ref, node, appearing):
    if node_ref:
        return {
            "onEnter": (appearing,),
            "onEntering": (appearing,),
            "onEntered": (appearing,),
            "onExit": (),
            "onExiting": (),
            "onExited": (),
            "addEndListener": ("done",),
        }
    return {
        "onEnter": (node, appearing),
        "onEntering": (node, appearing),
        "onEntered": (node, appearing),
        "onExit": (node,),
        "onExiting": (node,),
        "onExited": (node,),
        "addEndListener": (node, "done"),
    }

def fade_compatibility_args(callback, callback_args, node):
    if callback == "addEndListener":
        return (node, callback_args[0])
    if callback in {"onEnter", "onEntering", "onEntered"}:
        return (node, callback_args[0])
    return (node,)

node = "<childRef.current>"
actual = react_transition_group_args(True, node, True)
expected = react_transition_group_args(False, node, True)

for callback in expected:
    adapted = fade_compatibility_args(callback, actual[callback], node)
    assert adapted == expected[callback], (
        callback,
        adapted,
        expected[callback],
    )

print("All Fade callback wrappers restore the DOM node as the first argument.")

Repository: mattermost/react-bootstrap

Length of output: 322


Restore the DOM-node callback argument.

nodeRef causes react-transition-group@4.4.5 to omit the DOM node from transition callbacks. Wrap onEnter, onEntering, onEntered, onExit, onExiting, onExited, and addEndListener before passing them to Transition. Pass childRef.current as the first argument. This preserves Fade callbacks, including callbacks forwarded by Modal through DialogTransition. Add enter, exit, and addEndListener regression tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Fade.js` around lines 72 - 101, Update the Fade transition callbacks
around the Transition usage to restore the DOM-node first argument omitted when
nodeRef is provided. Wrap onEnter, onEntering, onEntered, onExit, onExiting,
onExited, and addEndListener so each invokes the supplied callback with
childRef.current first while preserving remaining arguments and existing
behavior, including Modal/DialogTransition forwarding. Add regression coverage
for enter, exit, and addEndListener callbacks.

Comment thread src/Modal.js
Comment on lines +279 to 294
renderDialog={renderProps => (
<Dialog
{...dialogProps}
{...renderProps}
style={{ ...renderProps.style, ...this.state.style, ...style }}
className={classNames(
renderProps.className,
className,
inClassName
)}
onClick={backdrop === true ? this.handleDialogClick : null}
handleDialogMouseDown={this.handleDialogMouseDown}
>
{children}
</Dialog>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find custom dialog implementations and their Modal call sites.
rg -n -C4 -g '*.js' \
  '\bdialogComponentClass\b|<ModalDialog\b|React\.forwardRef' \
  src test

Repository: mattermost/react-bootstrap

Length of output: 3539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Modal.js ---'
sed -n '1,340p' src/Modal.js

printf '%s\n' '--- ModalDialog.js ---'
sed -n '1,180p' src/ModalDialog.js

printf '%s\n' '--- dependency declarations ---'
rg -n -C3 'react-overlays|react-bootstrap|dialogComponentClass' package.json package-lock.json yarn.lock 2>/dev/null || true

printf '%s\n' '--- BaseModal references ---'
rg -n -C5 'BaseModal|renderDialog|updateStyle|_modal' src test

Repository: mattermost/react-bootstrap

Length of output: 21999


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for version in 5.2.0 5.2.1; do
  printf '%s\n' "--- react-overlays ${version} Modal.js ---"
  curl -fsSL "https://unpkg.com/react-overlays@${version}/esm/Modal.js" |
    sed -n '1,280p'
done

printf '%s\n' '--- React peer versions ---'
sed -n '/"peerDependencies"/,/^[[:space:]]*},/p' package.json

printf '%s\n' '--- Modal history summary ---'
git log --oneline --all -- src/Modal.js test/ModalSpec.js | head -30

Repository: mattermost/react-bootstrap

Length of output: 21134


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- renderDialog change ---'
git show --format=fuller --stat 8a8353a -- src/Modal.js test/ModalSpec.js
git show --format= --unified=80 8a8353a -- src/Modal.js test/ModalSpec.js | sed -n '1,260p'

printf '%s\n' '--- preceding Modal implementation ---'
parent="$(git rev-parse 8a8353a^)"
git show "${parent}:src/Modal.js" | sed -n '210,330p'

printf '%s\n' '--- current commit and recent change ---'
git log -1 --format='%H%n%P%n%s' -- src/Modal.js
git show --format= --unified=40 2dd3aae -- src/Modal.js test/ModalSpec.js | sed -n '1,220p'

printf '%s\n' '--- relevant dependency helper source ---'
curl -fsSL https://unpkg.com/dom-helpers@5.2.1/esm/ownerDocument.js
curl -fsSL https://unpkg.com/dom-helpers@5.2.1/esm/contains.js

Repository: mattermost/react-bootstrap

Length of output: 14323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- dialogComponentClass documentation and examples ---'
rg -n -C6 'dialogComponentClass|custom.*dialog|custom dialog' . \
  -g '!node_modules' -g '!dist' -g '!build' -g '!coverage'

printf '%s\n' '--- current custom-dialog test ---'
sed -n '220,250p' test/ModalSpec.js

printf '%s\n' '--- BaseModal renderDialog contract ---'
curl -fsSL https://unpkg.com/react-overlays@5.2.1/esm/Modal.d.ts |
  rg -n -C8 'renderDialog|dialog'

Repository: mattermost/react-bootstrap

Length of output: 50383


Preserve dialogComponentClass compatibility.

Custom dialog components that do not forward refs now leave this._modal.dialog null or set it to a component instance. updateStyle() then fails when the modal enters. Restore the previous wrapper behaviour, or document this breaking change and add migration guidance. The updated test only covers ref-forwarding components.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Modal.js` around lines 279 - 294, Restore compatibility for
dialogComponentClass values that do not forward refs by preserving the previous
wrapper behavior around the Dialog render path. Ensure this._modal.dialog
consistently references the expected DOM dialog element so updateStyle() works
when the modal enters, while retaining support for ref-forwarding components and
existing styling props.

Comment thread src/Overlay.js
Comment on lines +91 to +96
<BaseOverlay {...props} target={null} transition={transition}>
{({ props: overlayProps }) => (
<div ref={overlayProps.ref} style={{ display: 'content' }}>
{child}
</div>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- src/Overlay.js ---'
cat -n src/Overlay.js | sed -n '1,150p'

printf '%s\n' '--- src/OverlayTrigger.js ---'
cat -n src/OverlayTrigger.js | sed -n '180,235p'

printf '%s\n' '--- react-overlays references ---'
rg -n --hidden -S 'BaseOverlay|react-overlays|overlayProps|target=\{null\}|display: .content.' . \
  -g '!node_modules' -g '!dist' -g '!build'

Repository: mattermost/react-bootstrap

Length of output: 9218


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- bundled local Overlay implementation ---'
cat -n lib/Overlay.js | sed -n '1,145p'

printf '%s\n' '--- AMD Overlay implementation ---'
sed -n '14360,14520p' amd/react-bootstrap.js

printf '%s\n' '--- local references to overlay render props ---'
rg -n -C 8 'function Overlay|render\\(\\).*target|target.*popper|overlayProps|style:.*position|attributes' \
  amd/react-bootstrap.js lib es src patches \
  -g '*.js' -g '*.patch' | sed -n '1,260p'

Repository: mattermost/react-bootstrap

Length of output: 23387


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- bundled react-overlays Overlay implementation ---'
cat -n amd/react-bootstrap.js | sed -n '6520,6725p'

printf '%s\n' '--- dependency metadata ---'
sed -n '118,134p' package.json
sed -n '5368,5384p' yarn.lock

Repository: mattermost/react-bootstrap

Length of output: 8881


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from urllib.request import urlopen
urls = [
    "https://unpkg.com/react-overlays@5.2.1/esm/Overlay.js",
    "https://unpkg.com/react-overlays@5.2.1/esm/Position.js",
]
for url in urls:
    print(f"--- {url} ---")
    with urlopen(url, timeout=15) as response:
        text = response.read().decode()
    for number, line in enumerate(text.splitlines(), 1):
        print(f"{number:4}: {line}")
PY

Repository: mattermost/react-bootstrap

Length of output: 2512


🏁 Script executed:

#!/bin/bash
set -eu

for path in esm/Overlay.js esm/Position.js; do
  url="https://unpkg.com/react-overlays@5.2.1/$path"
  printf '%s\n' "--- $url ---"
  curl -ksSfL "$url" | nl -ba
done

Repository: mattermost/react-bootstrap

Length of output: 366


🌐 Web query:

react-overlays 5.2.1 esm Overlay.js usePopper render prop props target

💡 Result:

In react-overlays 5.2.1, the Overlay component provides functionality for positioning elements relative to a target using Popper.js [1][2]. The Overlay component uses a render prop (as its children) to allow full control over rendering the overlay element [1][3]. This render prop function receives an object containing positioning information and props that must be spread onto the overlay element to ensure correct positioning and styling [1][3]. The render prop signature for the Overlay children is a function that returns a React element [1]: ({ show, placement, update, forceUpdate, props, arrowProps }) => React.Element Key elements of the render prop: - props: An object containing essential attributes (e.g., ref, style) that must be applied to the overlay element to connect it to the Popper.js positioning engine [1][3]. - arrowProps: Optional props for positioning an arrow element if used [1][3]. - show, placement: State information about the current visibility and positioning of the overlay [1]. The target prop is required to define the reference point for the overlay [1][4]. It accepts a DOM Element, a ref to an element, or a function that returns either [1]. Example usage: import { Overlay } from 'react-overlays'; import { useRef } from 'react'; function Example({ show }) { const target = useRef(null); return ( <>

Target Element
{({ props, arrowProps, placement }) => ( <div {...props} style={{...props.style, backgroundColor: 'white' }}> Overlay Content )} </>); } The component internally utilizes the usePopper hook to handle positioning logic [1][2]. The popperConfig prop can be used to pass additional configuration options directly to the underlying react-popper Popper component [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

overlay = Path("src/Overlay.js").read_text()
trigger = Path("src/OverlayTrigger.js").read_text()

base_call = re.search(
    r"<BaseOverlay\s+\{\.\.\.props\}([^>]*)>", overlay, re.S
)
div = re.search(
    r"<div([^>]*)>\s*\{child\}\s*</div>", overlay, re.S
)
trigger_call = re.search(
    r"<Overlay\s*\n\s*\{\.\.\.props\}.*?\n\s*target=\{this\.triggerRef\}",
    trigger,
    re.S,
)

print("OverlayTrigger passes triggerRef:", bool(trigger_call))
print("BaseOverlay props:", " ".join(base_call.group(1).split()) if base_call else "<not found>")
print("Overlay div props:", " ".join(div.group(1).split()) if div else "<not found>")

if not base_call or "target={null}" not in base_call.group(1):
    raise SystemExit("expected target={null} override was not found")
if not div or "ref={overlayProps.ref}" not in div.group(1):
    raise SystemExit("expected ref forwarding was not found")
if "overlayProps" in div.group(1):
    raise SystemExit("unexpected full overlayProps spread")
PY

Repository: mattermost/react-bootstrap

Length of output: 369


Restore the target and generated overlay props.

Remove target={null} so triggerRef reaches BaseOverlay. Spread overlayProps onto the overlay element so Popper-generated positioning styles and attributes are applied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Overlay.js` around lines 91 - 96, Update the BaseOverlay invocation and
rendered overlay element in Overlay: remove target={null} so triggerRef is
passed through, and spread the generated overlayProps onto the div alongside its
ref and existing style. Preserve the child rendering and current display style.

Comment thread src/OverlayTrigger.js
Comment on lines +297 to +299
<div ref={this.triggerRef} style={{ display: 'content' }}>
{cloneElement(child, triggerProps)}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

printf '%s\n' '--- files ---'
git ls-files 'src/OverlayTrigger.js' 'src/*' | sed -n '1,120p'
printf '%s\n' '--- OverlayTrigger outline ---'
ast-grep outline src/OverlayTrigger.js --view compact 2>/dev/null || true
printf '%s\n' '--- target and display usages ---'
rg -n -C 4 "triggerRef|display: ['\"]content|Overlay\.target|Overlay" src/OverlayTrigger.js src -g '*.js' | sed -n '1,240p'

Repository: mattermost/react-bootstrap

Length of output: 12908


🏁 Script executed:

printf '%s\n' '--- OverlayTrigger implementation ---'
sed -n '1,330p' src/OverlayTrigger.js
printf '%s\n' '--- Overlay implementation ---'
sed -n '1,130p' src/Overlay.js
printf '%s\n' '--- related tests and usages ---'
git ls-files | rg '(^|/)(test|tests|__tests__)/|OverlayTrigger|Dropdown\.js$' | sed -n '1,160p'
rg -n -C 5 "<OverlayTrigger|OverlayTrigger|display: ['\"]contents['\"]|display: ['\"]content['\"]" test tests src README.md docs 2>/dev/null | sed -n '1,280p'
printf '%s\n' '--- package versions ---'
sed -n '1,180p' package.json

Repository: mattermost/react-bootstrap

Length of output: 30850


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
import re

trigger = Path('src/OverlayTrigger.js').read_text()
overlay = Path('src/Overlay.js').read_text()

ref_target = bool(re.search(r'target=\{this\.triggerRef\}', trigger))
wrapper = re.search(
    r'<div ref=\{this\.triggerRef\} style=\{\{ display: [\'"]([^\'"]+)[\'"] \}\}>',
    trigger,
)
child_clone = bool(re.search(r'cloneElement\(child, triggerProps\)', trigger))

print({
    'overlay_target_uses_trigger_ref': ref_target,
    'trigger_wrapper_display_value': wrapper.group(1) if wrapper else None,
    'child_is_cloned_with_trigger_props': child_clone,
    'overlay_wrapper_display_value': re.search(
        r'<div ref=\{overlayProps\.ref\} style=\{\{ display: [\'"]([^\'"]+)[\'"] \}\}>',
        overlay,
    ).group(1),
})
PY

printf '%s\n' '--- complete OverlayTrigger tests ---'
cat -n test/OverlayTriggerSpec.js | sed -n '1,360p'
printf '%s\n' '--- exact dependency lock entries ---'
rg -n -C 3 '"(react|react-dom|react-overlays)"' yarn.lock package-lock.json 2>/dev/null | sed -n '1,180p'

Repository: mattermost/react-bootstrap

Length of output: 11739


🌐 Web query:

CSS Display Module Level 3 invalid display value content display contents principal box getBoundingClientRect

💡 Result:

Under the CSS Display Module Level 3 specification, elements with display: contents do not generate a principal box [1][2][3]. Instead, the element is effectively replaced by its contents (children and pseudo-elements) in the box tree for the purposes of layout and box generation [1][4][3]. Because the element itself does not generate a box, it has no dimensions, position, or associated bounding box [5][6][7]. Consequently, when you call getBoundingClientRect on an element with display: contents, browsers return a DOMRect with all properties (x, y, width, height, top, etc.) set to zero [5][6][7]. This behavior is a direct consequence of the element having no box in the rendering tree [5]. While there are ongoing discussions within the W3C CSS Working Group regarding potential changes to this behavior (e.g., whether getClientRects should return a flattened list of its children's rectangles) [8][9], current browser implementations consistently follow the existing specification where the element is "invisible" to the layout engine, resulting in zero-sized measurements [5][6][7]. Developers relying on bounding boxes for positioning (such as in tooltip or popover libraries) must typically work around this by measuring the element's children instead [5][6][7]. Note that for replaced elements (such as or ) or other elements whose rendering is not entirely controlled by CSS, display: contents may compute to display: none or otherwise be treated as invalid, causing the element to be removed from the box tree entirely [10][3]. Using display: none is recommended for these elements to ensure predictable behavior [3].

Citations:


🌐 Web query:

MDN CSS display property valid values contents content invalid declaration initial value block div

💡 Result:

The CSS display property determines how an element generates boxes and how its children are laid out [1][2]. Initial Value: The initial value of the display property is inline [3][4][5]. Valid Values: The display property accepts a wide range of keyword values [1][3]. Modern CSS specifications define a multi-keyword syntax that allows for explicit control over an element's outer and inner display types [1][6][4]. For backward compatibility, browsers also support legacy single-keyword values (e.g., block, flex, grid) [1][6]. Commonly used values include: - block: Sets the element as a block-level box [3][2]. - inline: Sets the element as an inline-level box [3][2]. - contents: The element itself generates no box; instead, its children are rendered as if they were direct children of the element's parent [7][2]. - flex: Lays out children using the flexbox model [8][3]. - grid: Lays out children using the grid model [8][3]. - none: The element and its subtree are removed from the box tree and are not rendered [3][2]. Relationship between "block", "contents", and "content": - block: A standard display value that makes an element a block-level container [7][2]. - contents: A unique value that causes an element to be ignored in the box tree, effectively allowing its children to be positioned directly within the parent context [7][2]. - content: "content" is not a valid value for the display property. If used (e.g., display: content;), it is treated as an invalid declaration and ignored by the browser, with the element falling back to its previous or default display value [1][3]. The full specification uses a pair of keywords (an outer type and an inner type) [3][4][9]. For example, display: block flex is equivalent to the shorthand display: flex, where the outer type defaults to block [1][6].

Citations:


Use a valid, geometry-bearing trigger target.

display: 'content' is invalid, so this div remains a block-level box and can change inline trigger layout. Do not replace it with display: 'contents', because the target would have no measurable box.

Attach triggerRef to the trigger DOM node through ref forwarding or a render-prop contract. If arbitrary children remain supported, use a wrapper with an explicit measurable layout contract. Add an inline-trigger regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/OverlayTrigger.js` around lines 297 - 299, Update the trigger rendering
in OverlayTrigger so triggerRef attaches to the actual geometry-bearing trigger
DOM node via ref forwarding or the established render-prop contract, rather than
relying on invalid display:'content'. If arbitrary children require a wrapper,
give it an explicit measurable inline layout contract and preserve trigger
positioning; add a regression test covering inline-trigger geometry.

Comment thread src/utils/mergeRefs.js
Comment on lines +10 to +20
export function makeMergedRef(refs) {
return el => {
refs.forEach(ref => {
if (typeof ref === 'function') {
ref(el);
} else if (ref != null) {
ref.current = el;
}
});
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- tracked candidate files ---'
git ls-files | rg '(^|/)(mergeRefs\.js|Collapse\.js|Dropdown\.js|Fade\.js|package\.json|.*test.*|.*spec.*)$' | head -200

printf '%s\n' '--- mergeRefs.js ---'
cat -n src/utils/mergeRefs.js

printf '%s\n' '--- consumer excerpts ---'
for f in src/Collapse.js src/Dropdown.js src/Fade.js; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f" | sed -n '70,105p;175,250p;235,275p'
  fi
done

printf '%s\n' '--- ref utility usages and package metadata ---'
rg -n --glob '!node_modules' 'makeMergedRef|useMergedRef|getElementRef|ReactDOM|react' src package.json .github 2>/dev/null | head -300

Repository: mattermost/react-bootstrap

Length of output: 22683


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- package and test configuration ---'
cat -n package.json | sed -n '40,125p'
printf '%s\n' '--- ref-related tests ---'
rg -n -C 5 'ref|callback|cleanup|Collapse|Fade|Dropdown' test/CollapseSpec.js test/FadeSpec.js test/DropdownSpec.js test/helpers.js test/index.js 2>/dev/null | head -300

printf '%s\n' '--- all source and test references to mergeRefs ---'
rg -n -C 3 'makeMergedRef|useMergedRef|getElementRef' src test es lib

printf '%s\n' '--- repository state ---'
git status --short
git diff --stat

Repository: mattermost/react-bootstrap

Length of output: 21534


🌐 Web query:

React 19 callback ref cleanup function official documentation callback refs cleanup

💡 Result:

In React 19, you can now return a cleanup function from a ref callback [1][2][3]. This functionality allows you to perform cleanup tasks—such as disconnecting an IntersectionObserver, removing event listeners, or destroying third-party chart instances—directly within the ref callback when the component unmounts [1][4][5][6]. Key details of this feature include: How it works: When the component unmounts, React will execute the cleanup function returned by your ref callback [1][2][4]. If a cleanup function is returned, React will skip the legacy behavior of calling the ref function with null upon unmounting [1][3][6]. Implementation example: <input ref={(ref) => { // Setup logic const observer = new ResizeObserver(...); if (ref) observer.observe(ref); // Return cleanup function return => { observer.disconnect; }; }} /> TypeScript and Implicit Returns: Because returning any value from a ref callback now indicates a cleanup function, TypeScript will reject implicit returns [7][1][3]. You must explicitly wrap your callback in a block and avoid returning non-cleanup values [7][8][3]. For example: // Incorrect (implicit return) <div ref={current => (instance = current)} /> // Correct (explicit return) <div ref={current => { instance = current; }} /> Migration and Compatibility: React plans to deprecate the behavior of calling ref functions with null during unmounting in future versions [1][3]. If your existing code relies on this behavior, you should transition to the new cleanup function pattern [7][1]. Tools such as the no-implicit-ref-callback-return codemod are available to help identify and fix implicit returns that may conflict with this new feature [7][1].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from dataclasses import dataclass

`@dataclass`
class RefObject:
    current: object = None

def current_merged(refs, el):
    for ref in refs:
        if callable(ref):
            ref(el)
        elif ref is not None:
            ref.current = el

def proposed_merged(refs, el):
    cleanups = []
    for ref in refs:
        if callable(ref):
            cleanup = ref(el)
            cleanups.append(cleanup if callable(cleanup) else (lambda ref=ref: ref(None)))
        elif ref is not None:
            ref.current = el
            cleanups.append(lambda ref=ref: setattr(ref, "current", None))
        else:
            cleanups.append(None)
    active = [cleanup for cleanup in cleanups if cleanup is not None]
    return (lambda: [cleanup() for cleanup in active]) if active else None

events = []
def callback(node):
    events.append(("attach", node))
    return lambda: events.append(("cleanup", node))

obj = RefObject()
cleanup = None
current_merged([callback, obj], "node")
print("current implementation:", events, obj.current, cleanup)

events.clear()
obj = RefObject()
cleanup = proposed_merged([callback, obj], "node")
print("proposed attach:", events, obj.current, callable(cleanup))
cleanup()
print("proposed detach:", events, obj.current)
PY

Repository: mattermost/react-bootstrap

Length of output: 331


🌐 Web query:

site:github.com/react-bootstrap/react-bootstrap mergeRefs callback ref cleanup makeMergedRef

💡 Result:

In the context of React libraries like react-bootstrap and @restart/hooks, mergeRefs is a utility function used to combine multiple refs into a single ref callback [1][2][3]. This is necessary when a component needs to maintain its own internal ref to a DOM node while also exposing a ref provided by a parent via forwardRef [1][2]. The mergeRefs utility typically handles cleanup by returning a function or managing a callback ref that updates all provided refs when the component mounts or unmounts [2][3]. When a ref is no longer needed—such as when a component unmounts—the merged ref callback is called with null, which in turn ensures that all original refs are also set to null, preventing memory leaks and stale references [4]. Key points regarding these patterns: 1. Ref Merging: Functions like useMergedRefs (often powered by a mergeRefs utility) are designed to "merge" a user-provided ref (from a parent) and an internal ref (needed for component logic) into a single callback ref that the component applies to the DOM element [2][3]. 2. Cleanup: Because these are callback refs, React automatically invokes them with null during cleanup (unmounting). The mergeRefs implementation ensures this null value is propagated to every individual ref passed into the merger [2][4]. 3. makeMergedRef: While mergeRefs is the common utility name, some older or specific internal implementations use helpers like makeMergedRef to construct the callback function that orchestrates these assignments. For modern React development, ensure you are using the official useMergedRefs hook from @restart/hooks or a similar community-standard library, as they handle these edge cases, including cleanup and proper ref forwarding, consistently across different React versions [1][3].

Citations:


Preserve callback-ref cleanup functions in makeMergedRef. React 19 callback refs can return cleanup functions, but the merged ref discards them. Return a composed cleanup that clears object refs, invokes returned cleanups, and calls ref(null) only when a callback returns no cleanup. This also affects useMergedRef.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/mergeRefs.js` around lines 10 - 20, Update makeMergedRef to retain
cleanup functions returned by callback refs, and return a composed cleanup that
clears object refs, invokes each returned cleanup, and calls ref(null) only for
callbacks without cleanup. Ensure useMergedRef uses this updated behavior
without discarding the returned cleanup.

Source: MCP tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant