Repository navigation
Conversation
…dex='0']`
Chromium collects attribute invalidation sets by attribute name, so the
`[tabindex='0']` selector made every `tabindex` write invalidate the style of
the element written to. On the focused element, the write also makes Chromium
update style to check that it's still focusable, so moving the active cell
forced extra style recalcs. Rows get `rdg-row-active` exactly when they get
`tabIndex={0}`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A child that inherits a non-inherited property makes Chromium restyle every child of its parent whenever one of the parent's non-inherited styles changes. The root's scroll padding changes when the active cell moves between frozen and unfrozen columns, or between the header or summary rows and the body, so each of those moves restyled every row. The root's children are now `border-box` themselves, which is the root's own `box-sizing`, and everything below them still inherits it, so computed styles don't change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rdg-row-active instead of [tabindex='0']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.
Moving the active cell restyled more than the cells it moves between, for two reasons:
TreeDataGridis drawn by.rdg-row[tabindex='0']. Chromium collects attribute invalidation sets by attribute name, not by element, so this selector makes everytabindexwrite on the page invalidate the style of the element it's written to, row or not. When that element is focused, Chromium also brings style up to date on the spot to check that the element is still focusable (Element::AttributeChangedcallsUpdateStyleAndLayoutTreeForElement), so the invalidation turns into a forced style recalc. Moving the active cell writestabindexon the focused element: the old cell, the old row in aTreeDataGrid, or focusable content in a cell, like the checkboxes ofSelectColumn. The library's rows get therdg-row-activeclass exactly when they gettabIndex={0}, so the rule can match the class instead.box-sizing: inheriton the root's children. When a child usesinheritfor a non-inherited property, Chromium restyles all children of its parent whenever any non-inherited style of the parent changes (ComputedStyle::ComputeDifference). The root's scroll padding changes when the active cell moves between frozen and unfrozen columns, or between the header or summary rows and the body, so each of those moves restyled every rendered row: about 60 elements in a 600px tall grid. The root's children now getbox-sizing: border-box, the root's own value, and everything below them still inherits it, so no computed style changes.Changes
.rdg-row.rdg-row-activeinstead of.rdg-row[tabindex='0'], with the same specificity.box-sizing: border-boxinstead of inheriting it.tabindexon a row to trigger the rule now sets the class. It fails with the old selector.This isn't a breaking change: the computed
box-sizingand the size and position of every element of the library, and of custom content in cells, headers, and editors, are the same as before. Two edge cases change:box-sizingon.rdgitself no longer passes it on to the content of the grid, which staysborder-box.renderRowthat passestabIndex={0}to a row of aDataGridno longer draws the border, as only active rows of aTreeDataGridget the class.#4267 and #4289 merge cleanly with this branch. #3976 moves the
[tabindex='0']block ofsrc/style/row.tsinto a newrowFocusableclass, so whichever merges second has to carry the selector over; #3976 already conflicts withmainin that file.Alternatives
Removing the
box-sizingrule altogether is the fastest option, but it's a breaking change. The library's own elements would only needbox-sizing: border-boxon the drag handle, which otherwise grows from 16 to 20px on hover, and their layout stays the same. But custom content falls back to the browser's defaults, which iscontent-boxfor most elements, in apps without a global reset:border-boxon every element<input>withinline-size: 100%andpadding: 4px<input>withinline-size: 100%,padding-inline: 6px, and a 2px border<div>withinline-size: 50px,padding-inline: 10px, and a 1px borderbox-sizing: content-boxwrapper,inline-size: 50px,padding-inline: 10pxThe same in Chromium, Firefox, and WebKit. The demo site's header filters and the progress editor's dialog in
CommonFeatureswould change too.border-boxon every element instead ofinheritbreaks the last case, and made scrolling 7% slower in style time.Style recalc time per move in µs, mean of 5 rounds with rotated build order, measured on top of the first change:
border-boxon every elementSelectColumnTreeDataGridrows and cellsWithout the rule, moves get 3.5–13 µs faster than with this PR, about 1–3% of a move. Most likely, that's because Chromium creates a
::beforeand an::afterfor each element that a::beforeor::afterrule matches, resolves their styles, and discards them when they have nocontent.Verification
Chromium with production React and a 30-column grid, from CDP traces of each arrow-key move. The counts were the same in all 5 cycles per key. Style recalc passes / elements restyled per move:
mainSelectColumnTreeDataGridrows and cellsMoving between a frozen and an unfrozen column now restyles 6–9 elements instead of 63–68. ← takes a pass more than → in the last column because React writes the new cell before the old one. Between focused rows in a
TreeDataGrid, ↓ takes a pass fewer and ↑ a pass more, for the same reason.Style recalc time per move in µs, mean of 5 rounds with rotated build order. An identical copy of this PR measured 38.2, 38.1, 97.0, and 88.8:
mainSelectColumnTreeDataGridrows and cellsThe whole key press (
flushSync) went from 556 to 499 µs in and around aSelectColumn, and from 699 to 673 µs in theTreeDataGrid. The difference for plain cells is within its ±10% between rounds.The sizes, positions, and computed
box-sizingof every element in aDataGridand aTreeDataGridwith frozen columns, summary rows, sorted, resizable, and draggable columns, an open editor, the drag handle at rest and hovered, a column's drag image, and an active row, including the::beforeand::afterborders, matchedmain's in Chromium, Firefox, and WebKit. So did focus, scroll positions, scroll padding, and the active row's and active cell's borders after each of 45 moves. CI's visual tests pass with the existing screenshots.vp run ci --coveragepasses.🤖 Generated with Claude Code