Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/react/src/ActionList/ActionList.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
}

/* if a list has a mix of items with and without descriptions, reset the label font-weight to normal */
&:has([data-has-description='true']):has([data-has-description='false']) {
&[data-mixed-descriptions='true'] {
& .ItemLabel {
font-weight: var(--base-text-weight-normal);
}
Expand Down
40 changes: 40 additions & 0 deletions packages/react/src/ActionList/ActionList.stress.dev.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const projects = Array.from({length: totalIterations}, (_, i) => ({
scope: `Scope ${i + 1}`,
}))

const mixedProjects = Array.from({length: totalIterations}, (_, i) => ({
name: `Project ${i + 1}`,
scope: i % 2 === 0 ? `Scope ${i + 1}` : undefined,
}))

export const SingleSelect = () => {
return (
<StressTest
Expand Down Expand Up @@ -48,3 +53,38 @@ export const SingleSelect = () => {
/>
)
}

export const MixedDescriptions = () => {
return (
<StressTest
componentName="ActionList"
title="Mixed Descriptions"
description="Stress test with a mix of items with and without descriptions to test :has() selector perf."
totalIterations={totalIterations}
renderIteration={count => {
return (
<>
<ActionList selectionVariant="single" showDividers role="menu" aria-label="Project">
{mixedProjects.map((project, index) => (
<ActionList.Item
key={index}
role="menuitemradio"
selected={index === count}
aria-checked={index === count}
>
<ActionList.LeadingVisual>
<TableIcon />
</ActionList.LeadingVisual>
{project.name}
{project.scope ? (
<ActionList.Description variant="block">{project.scope}</ActionList.Description>
) : null}
</ActionList.Item>
))}
</ActionList>
</>
)
}}
/>
)
}
29 changes: 29 additions & 0 deletions packages/react/src/ActionList/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {useProvidedRefOrCreate} from '../hooks'
import {FocusKeys, useFocusZone} from '../hooks/useFocusZone'
import {clsx} from 'clsx'
import classes from './ActionList.module.css'
import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect'

const UnwrappedList = <As extends React.ElementType = 'ul'>(
props: ActionListProps<As>,
Expand Down Expand Up @@ -66,6 +67,34 @@ const UnwrappedList = <As extends React.ElementType = 'ul'>(
[variant, selectionVariant, containerSelectionVariant, showDividers, listRole, headingId],
)

// Replaces a CSS `:has([data-has-description])` selector that caused full-subtree
// style recalculation on every DOM mutation (~674ms on 100 items, 10-20s freezes on Safari).
//
// Ideally we'd derive this from children during render, but each Item's description is
// detected via `useSlots` at render time, so the List can't know which Items have
// descriptions without duplicating slot detection or deeply inspecting children trees
// (fragile with Groups, conditional rendering, wrapper components, etc.).
//
// A context-based approach (Items registering their description state with the List) would
// work but adds registration/unregistration callbacks, a new provider, and re-renders when
// the count changes. Not worth the complexity for a derived boolean.
//
// Two querySelector calls after render is trivially cheap compared to what the browser
// was doing on every DOM mutation with `:has()`.
useIsomorphicLayoutEffect(() => {
const list = listRef.current
if (!list) return
const hasMixed =
list.querySelector('[data-has-description="true"]') !== null &&
list.querySelector('[data-has-description="false"]') !== null
const current = list.getAttribute('data-mixed-descriptions')
if (hasMixed && current !== 'true') {
list.setAttribute('data-mixed-descriptions', 'true')
} else if (!hasMixed && current !== null) {
list.removeAttribute('data-mixed-descriptions')
}
})

return (
<ListContext.Provider value={listContextValue}>
{slots.heading}
Expand Down
Loading