Skip to content

feat(console): adopt react 19.3 transitions and fragment refs - #2646

Draft
malinskibeniamin wants to merge 1 commit into
masterfrom
ben-malinski/upgrade-react-19-3
Draft

feat(console): adopt react 19.3 transitions and fragment refs#2646
malinskibeniamin wants to merge 1 commit into
masterfrom
ben-malinski/upgrade-react-19-3

Conversation

@malinskibeniamin

Copy link
Copy Markdown
Contributor

Summary

  • Upgrade React and React DOM from locked 19.2.7 to 19.3.0, their types to 19.3.0, and React DOM's scheduler to 0.28.0. Bun and generated Yarn audit mirror updated together.
  • Animate lazy editor/diff/YAML reveals through a shared, wrapper-free ViewTransition + Suspense boundary. Initial fallbacks and cached content remain immediate. Reduced-motion CSS disables both named and browser-root snapshot animations.
  • Focus expanded message controls through a Fragment ref, without an extra DOM wrapper or stealing focus when selecting another message. Add accessible names and regression tests.

Impact

Keyboard users land on the expanded message's controls instead of losing focus to the document. Browser checks confirm animated loading reveals normally, zero CSS transition animations under reduced motion, and working close/reopen behavior. No performance claim.

React 19.3 feature coverage

Feature Decision
ViewTransition / Suspense reveals Adopted in KowlEditor, KowlDiffEditor, PipelinesYamlEditor.
Fragment refs Adopted for expanded message action focus.
addTransitionType No directional/carousel interaction in this slice; do not introduce artificial transitions.
Image/font suspension Available through the boundary; no new image/font loading infrastructure needed for these editors.
browser() / direct RSC Context Not applicable to this client-rendered Console; no SSR/RSC introduced.
Trusted Types Runtime support upgraded; HTML sinks are CLI-managed registry files and cannot be edited locally. No global CSP policy enabled.
Other fixes/events Runtime fixes inherited. No actual Fullscreen API or Server Actions call sites to migrate; expanded panels use CSS, not browser fullscreen.

Dependency upgrade path

Stable minor: React/DOM 19.2.7 → 19.3.0; @types/react 19.2.17 → 19.3.0; @types/react-dom 19.2.3 → 19.3.0; scheduler 0.27.0 → 0.28.0.

Requested exact release is less than the repository's 72-hour cooling window: used a one-shot --minimum-release-age=0 for the four requested React packages. Repository bunfig policy unchanged; lifecycle scripts disabled. Clean frozen install passed. No arbitrary coupled package churn; existing wrapper peers accept React 19. @monaco-editor/react 4.7.0 remains the latest stable release.

React 18 Cloud host isolation is unchanged (shared: {} and separate React roots).

Sources: https://react.dev/blog/2026/09/09/react-19-3, https://react.dev/reference/react/ViewTransition, https://react.dev/reference/react/Fragment

Verification

  • Passed: bun run type:check, bun run build, bun run test:ci (993 unit + 1,429 integration + 1 federation tests).
  • RED→GREEN: expanded message focus; loading-boundary tests. Browser reduced-motion failure caught browser-root animations and fixed them.
  • Passed: fresh bun install --frozen-lockfile --ignore-scripts; lockfile diff limited to React ecosystem.
  • Passed: focused lint on changed files covered by the project's lint configuration.
  • Global lint remains failing: 2,180 diagnostics versus 2,179 baseline; additional reported group is an unchanged deprecated Router useBlocker import, not a changed call site. lint:fix script does not exist, so ran the project's bun run lint equivalent.
  • Audit unchanged: 62 advisories across 21 packages; BSR registry does not support npm audit.
  • Real Chrome component consumer: loading/cached/reduced-motion/focus/close/reopen passed. Editor surfaces mount, accept input and remount, but diff disposal emits the same upstream error on both React 19.2.7 baseline and 19.3.0. Uncaught Error: TextModel got disposed before DiffEditorWidget model got reset suren-atoyan/monaco-react#647
  • Full-stack E2E blocked: backend image built, but Docker Desktop became unavailable during Redpanda startup. Playwright-managed Chromium download also failed (timeout/DNS); component checks used installed Chrome.

Draft publication limitations

User approved draft publication after disclosure of incomplete full-stack dogfood, the known upstream diff error, and missing uploaded before/after screenshot evidence. These remain verification limitations, not passing checks. Local evidence and scripts are under .context/react-evidence/ and .context/react-*.log; these are not reviewer-accessible URLs.

Cleanup caveat: test network 1b3544115c222b3a307a8ff082a3bfa22b993a6ac6af66750845d2c76d112c54 could not be checked/removed while Docker Desktop was unavailable. The interrupted test process is stopped.

@github-actions

Copy link
Copy Markdown
Contributor

Clean — no registry drift, off-token colours, or ad-hoc classes

App: frontend · Scope: diff vs origin/master · Files: 7

Count
⚠️ Outdated registry components 0
🛠 Locally-modified components 0
❓ Unknown to registry 0
🎨 Off-token palette colours 0
🔢 Ad-hoc utility classes 0

Generated by lookout audit-changes.

@malinskibeniamin malinskibeniamin left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated /review: 4 finding(s).

::view-transition-group(root),
::view-transition-old(root),
::view-transition-new(root),
::view-transition-group(.console-content-reveal),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Priority: P1

Reduced-motion opt-out never applies — invalid view-transition selector

::view-transition-group(.console-content-reveal) is not valid CSS. The argument grammar for these pseudo-elements is <pt-name-selector> (* or a <custom-ident>) optionally followed by a class selector, so a view-transition class must be written as *.console-content-reveal. A bare .console-content-reveal fails to parse, and because an invalid selector invalidates the entire selector list, the root opt-outs on lines 3-5 are dropped too.

Consequence: under prefers-reduced-motion: reduce the whole rule is discarded, so users who asked for reduced motion still get the full-page cross-fade every time a lazy editor resolves — the exact case this file exists to suppress.

Correction:

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*.console-content-reveal),
  ::view-transition-old(*.console-content-reveal),
  ::view-transition-new(*.console-content-reveal) {
    animation: none;
  }
  ::view-transition-group(root),
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
}

Splitting the class rule from the root rule also keeps one bad selector from taking the other down in future edits.

Verify: load a page with a Monaco editor in Chrome with reduced motion forced (DevTools → Rendering → Emulate CSS prefers-reduced-motion: reduce) and confirm no cross-fade; DevTools Styles should show the rule matching rather than greyed out as invalid.

Automated /review.

function ExpandedMessageActions({ onCollapse, onClose }: { onCollapse: () => void; onClose: () => void }) {
const actionsRef = useRef<FragmentInstance>(null);
useEffect(function focusExpandedActions() {
actionsRef.current?.focus({ preventScroll: true });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Priority: P2

Focus is moved into the sheet but never restored when it unmounts

focusExpandedActions pulls focus onto the collapse button on mount, but every exit path unmounts this subtree: Escape and the collapse button flip expanded to false (topic-messages-view.tsx:529/551 swap presentations), and Close unmounts the panel entirely. Focus then falls back to document.body, so keyboard users lose their row position and restart the tab order from the top of the page. Because detailExpanded is restored from persisted state (topic-messages-view.tsx:79), simply clicking a row can also yank focus out of the table with no way back.

Correction: capture document.activeElement before focusing (or have the parent own the trigger element) and restore it in the effect cleanup, guarding that the saved element is still connected:

useEffect(function focusExpandedActions() {
  const previous = document.activeElement as HTMLElement | null;
  actionsRef.current?.focus({ preventScroll: true });
  return () => {
    if (previous?.isConnected) previous.focus({ preventScroll: true });
  };
}, []);

Verify: extend the new test to assert focus destination after Escape, Collapse, and Close (render the parent so the docked panel/table remains mounted), then tab-navigate the messages page manually to confirm focus returns to the originating control.

Automated /review.

/** Reveal lazy content without animating its initial fallback or cached content. */
export function LoadingBoundary({ children, fallback }: { children: ReactNode; fallback: ReactNode }) {
return (
<ViewTransition default="none" update="console-content-reveal">

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Priority: P2

A page-wide view transition fires whenever any lazy editor resolves

<ViewTransition> drives a document-level transition, so the browser snapshots and cross-fades the root group as well — the CSS comment on loading-boundary.css:2 acknowledges this. Every Monaco/YAML editor reveal (kowl-editor.tsx:65, kowl-editor.tsx:82, pipelines-yaml-editor.tsx:109) therefore animates the entire viewport, not just the editor slot, and the page is non-interactive for the duration. Outside prefers-reduced-motion there is no opt-out at all, and with the selector defect above there is none inside it either.

Correction: scope the reveal so the root snapshot does not animate for all users — add an unconditional ::view-transition-group(root) { animation: none; } (or animation-duration: 0s) alongside the named class, keeping the reduced-motion block only for the named group. Alternatively gate the wrapper behind a matchMedia('(prefers-reduced-motion: reduce)') check and render a plain <Suspense> when motion is unwanted.

Verify: open the topic message detail value section in Chrome, record a performance trace during editor load, and confirm no full-viewport cross-fade and no input delay while the transition runs.

Automated /review.

expect(screen.getByRole('button', { name: 'Collapse back to panel' })).toHaveFocus();

await user.tab();
const close = screen.getByRole('button', { name: 'Close' });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Priority: P3

The "does not steal focus when the message changes" assertion cannot fail

focusExpandedActions has an empty dependency array and ExpandedMessageActions is not keyed by message, so a rerender with a new offset can never re-run the effect — this assertion is green regardless of the behaviour it claims to protect. The regression it is meant to guard (re-focusing when the selected record changes) would only appear if the subtree remounted, for example if the header were keyed on msg.offset or the panel were remounted by the parent.

Correction: drive the change through the real entrypoint instead — render the parent view (or toggle expanded off and on) and assert focus placement across that cycle, so a keyed remount or a dependency added to the effect is caught.

Verify: temporarily add key={msg.offset} to ExpandedMessageActions locally; the test should go RED. It currently stays green.

Automated /review.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant