Repository navigation
Conversation
🤖 PR meta 🤖📦 Bundle sizeSize Change: 0 B Total Size: 8.63 MB
⚡ PerformanceShow the resultsClient side metrics exclude the server response time. front-end-block-theme
front-end-classic-theme
media-processing
media-upload
post-editor
site-editor
🏁 Flaky testsSome tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information. Page List converts to Navigation Links with entity bindings in
|
DashboardGrid and DashboardLanes start with a container width of 0 and measure it in a layout effect. Children run their layout effects first, so a tile that read its span or size at mount saw the one-column layout. Mount tiles only after the first measurement; the update renders synchronously before paint, so no empty frame shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015zLZuzKAxkbg7pY4W6xJB2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015zLZuzKAxkbg7pY4W6xJB2
Measure through a ref in a layout effect that runs in the surface's first commit, instead of waiting for the root element state set by the ref callback. Tiles now mount in the second commit, with the measured column count. Keep the exit animation's empty array when nothing is exiting, so a container change does not cost the surface an extra commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015zLZuzKAxkbg7pY4W6xJB2
louwie17
force-pushed
the
fix/grid-mount-after-measure
branch
from
November 9, 2026 14:20
f754b62 to
da3eaa2
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
DashboardGridandDashboardLanesnow mount their tiles after they first measure their container. A tile's first commit already has the measured column count.Why?
Both surfaces start with a container width of 0 and measure it in a layout effect. A responsive policy (
columnsas steps, orminColumnWidth) resolves to one column at width 0, so the first commit gives every tilegrid-column-end: span 1at full width. The measurement then sets the real count, before paint.React runs a component's layout effects after those of its children. A tile that reads its span or size when it mounts, in a layout effect or a ResizeObserver, sees the one-column layout. Measuring sooner in the surface does not help, because the children have already read it. If the tile's width does not change, for example a three-column tile that goes from
span 1tospan 3at the same width, its ResizeObserver never fires again and the tile keeps the layout it picked for one column.This shows up when a host remounts widgets whose modules are already loaded, such as when it switches back to a dashboard tab: the widget body renders in the same commit as its tile. Jetpack's Stats dashboard hit this and works around it by watching the tile's style with a MutationObserver (Automattic/jetpack#54676). Any other consumer that reads span or size at mount has the same problem.
How can a tile see a known width but the wrong columns?
The width and the column count come from two different places. The width is the browser's layout: once React inserts the grid into the DOM, the browser lays it out at its real width, and a child that calls
getBoundingClientRect()gets that width. The column count is React state: the surface computesrepeat(N, …)and each tile'sgrid-column-end: span …fromcontainerWidth, which starts atuseState( 0 )and only becomes the real width after the surface's own layout effect measures the DOM. Until then the DOM has the real width but a one-column track, and in one column a tile is full width.On trunk the measurement also ran one commit late. The effect depended on
gridRoot, which is state set by a ref callback, so in the first commit it sawnulland returned. The order was: commit 1 (width 0, one column) → tiles' layout effects readspan 1→ the surface's effect skips → commit 2 (gridRootset, width still 0) → tiles that re-render still seespan 1→ the surface measures → commit 3 with the real count. All of this is before the first paint, so no one sees a one-column grid, but a tile that chose its layout in a layout effect keeps the one-column choice.How?
Each surface measures through a ref in a layout effect that runs in its first commit, instead of waiting for the
gridRoot(orcontainer) state, and keeps ahasMeasuredflag that the effect sets. Until then the surface renders its root, the track template and the grid overlay, but not the tiles or the unplaced children. Measuring earlier alone is not enough, because tiles' layout effects run before the surface's; holding the tiles back is what guarantees none of them reads the unmeasured count. Setting the flag in a layout effect makes React render again synchronously before paint, in the same pass that applies the measured width, so the tiles mount once with the measured column count and no empty frame is painted.The surface itself now commits twice before paint: once empty to measure, once with the tiles. A React Profiler count of
DashboardGridat 1000px went from four commits on trunk to four with this change, but with the tiles mounted only once and with the measured columns. Two of the four commits are not about columns: dnd-kit registers each tile's droppable from an effect on mount, and the ResizeObserver reports the grid's height after paint for the overlay'srows. Both happen on trunk too and are left for follow-ups.useItemExitAnimationalso calledsetExitingItems( [] )with a new array whenever the container changed, which cost the surface an extra commit on mount. It now keeps the current array when nothing is exiting.The flag does not depend on the measured width. When the container measures 0 (it is hidden, or the test runs in jsdom), tiles mount as they did before and the ResizeObserver updates them later. Column-step resolution is unchanged.
Options considered:
useGridItemSpan(). Consumers would no longer have to parse computed CSS, but it adds public API and does not fix a tile that measures its own size at mount. It could still come later.@wordpress/widget-dashboard. That fixes only that consumer, and the surface owns the measurement, so the fix belongs in@wordpress/grid.On the server, layout effects do not run, so server-rendered markup contains the empty surface without tiles. Neither surface was producing a correct layout on the server before this change, since the width is unknown there, and
@wordpress/widget-dashboardrenders on the client.Testing Instructions
npm run test:unit -- packages/widget-dashboard/src/test/column-count.browser.test.tsx.columns: { 0: 1, 960: 3 }, unmount it, and mount it again so the widget body renders from the module cache in the same commit as its tile. A probe widget records its tile'sgrid-column-endand width in a layout effect when it mounts.expected { span: 'span 1', width: 1000 } to deeply equal { span: 'span 3', width: 1000 }expected 1000 to be 333.328125@wordpress/gridand@wordpress/widget-dashboardtests.The masonry test compares width only, because the lane polyfill places tiles in rows after it measures their heights.
Testing Instructions for Keyboard
No UI change. Keyboard reordering and resizing are covered by the existing
keyboard-activationtests, which pass.Use of AI Tools
Claude Code (Claude Opus 5.5) investigated the cause, wrote the fix and the tests, and drafted this description. I reviewed the change and the results.
🤖 Generated with Claude Code
https://claude.ai/code/session_015zLZuzKAxkbg7pY4W6xJB2