@@ -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