Skip to content

fix(ui-modal): avoid redundant Modal.Body re-renders from its observers - #2701

Open
balzss wants to merge 1 commit into
masterfrom
INSTUI-5166_modalbody_redundant_rerenders
Open

fix(ui-modal): avoid redundant Modal.Body re-renders from its observers#2701
balzss wants to merge 1 commit into
masterfrom
INSTUI-5166_modalbody_redundant_rerenders

Conversation

@balzss

@balzss balzss commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Modal.Body's resize/mutation observers called forceUpdate() on every observed change, so any DOM change inside the body re-rendered it even when the derived tabIndex was identical. In jsdom test suites those updates land outside act(), which is what consumers on 11.7.4 are seeing.
  • Derive needsTabIndex into state and compare against the last computed value before calling setState. The comparison has to precede setState — for class components it schedules (and warns) before the updater runs, so bailing out inside the updater is too late.
  • render() no longer reads live DOM geometry; it reads state, and the DOM reads moved into syncTabIndex.
  • Same change in v1 and v2. No prop, theme, or export changes.

Measured on the branch: 20 no-op mutations inside the body went from 20 re-renders to 0 (real Chromium), and 10 mutations from 10 act warnings to 0 (jsdom + RTL, both versions).

Test Plan

  • Keyboard-only: open a modal with a scrollable body and no focusable children, Tab to the body, confirm it takes focus and Up/Down scroll it.
  • With the modal still open, toggle a focusable child into the body — the body should stop being a tab stop and focus should go to the child instead. Toggle it back out and the body should be focusable again.
  • Toggle the body between scrollable and not while open; the tab stop should follow.
  • Worth a screen-reader pass (VO/NVDA/JAWS) on the scrollable-body aria-label, since this touches the same code path as INSTUI-5046.

Fixes INSTUI-5166

🤖 Generated with Claude Code

The resize and mutation observers called forceUpdate() on every observed
change, re-rendering even when the derived tabIndex was identical. Derive
needsTabIndex into state and compare before calling setState, which also
stops the act() warnings consumers see in jsdom test suites.

Applies to v1 and v2.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@balzss balzss self-assigned this Aug 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://instructure.design/pr-preview/pr-2701/

Built to branch gh-pages at 2026-08-28 09:00 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

github-actions Bot pushed a commit that referenced this pull request Aug 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Visual regression report

Cypress suite: ✅ Passing

Visual diff:No changes.

Status Count
Unchanged 96
Changed 0
New 0
Removed 0

Accessibility (axe): ✅ No violations.

📊 View full report — click a screenshot's ⚠ badge to see each violation boxed on the image, with the offending element named and contrast failures shown as color swatches.

Baselines come from the visual-baselines branch. They refresh on every merge to master. The Cypress suite line covers the a11y and console-error assertions — a ❌ there means the suite found real issues even if the visual diff is clean.

@balzss
balzss requested review from git-nandor and matyasf August 28, 2026 09:10
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