Skip to content

Commit d407207

Browse files
committed
perf(ActionList): replace :has() selector with JS-computed attribute
1 parent ddc4ba9 commit d407207

2 files changed

Lines changed: 23 additions & 1 deletion

File tree

packages/react/src/ActionList/ActionList.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
}
8888

8989
/* if a list has a mix of items with and without descriptions, reset the label font-weight to normal */
90-
&:has([data-has-description='true']):has([data-has-description='false']) {
90+
&[data-mixed-descriptions='true'] {
9191
& .ItemLabel {
9292
font-weight: var(--base-text-weight-normal);
9393
}

packages/react/src/ActionList/List.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,28 @@ const UnwrappedList = <As extends React.ElementType = 'ul'>(
6666
[variant, selectionVariant, containerSelectionVariant, showDividers, listRole, headingId],
6767
)
6868

69+
// Replaces a CSS `:has([data-has-description])` selector that caused full-subtree
70+
// style recalculation on every DOM mutation (~674ms on 100 items, 10-20s freezes on Safari).
71+
//
72+
// Ideally we'd derive this from children during render, but each Item's description is
73+
// detected via `useSlots` at render time, so the List can't know which Items have
74+
// descriptions without duplicating slot detection or deeply inspecting children trees
75+
// (fragile with Groups, conditional rendering, wrapper components, etc.).
76+
//
77+
// A context-based approach (Items registering their description state with the List) would
78+
// work but adds registration/unregistration callbacks, a new provider, and re-renders when
79+
// the count changes. Not worth the complexity for a derived boolean.
80+
//
81+
// Two querySelector calls after render is trivially cheap compared to what the browser
82+
// was doing on every DOM mutation with `:has()`.
83+
React.useLayoutEffect(() => {
84+
const list = listRef.current
85+
if (!list) return
86+
const hasWithDescription = list.querySelector('[data-has-description="true"]') !== null
87+
const hasWithoutDescription = list.querySelector('[data-has-description="false"]') !== null
88+
list.setAttribute('data-mixed-descriptions', String(hasWithDescription && hasWithoutDescription))
89+
})
90+
6991
return (
7092
<ListContext.Provider value={listContextValue}>
7193
{slots.heading}

0 commit comments

Comments
 (0)