diff --git a/.changeset/perf-selectpanel2-empty-input.md b/.changeset/perf-selectpanel2-empty-input.md new file mode 100644 index 00000000000..0c470aa1097 --- /dev/null +++ b/.changeset/perf-selectpanel2-empty-input.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +SelectPanel (experimental v2): replace the `:has(input:placeholder-shown)` selector that hid the clear-action button with a `data-empty` attribute derived from React state. The clear-action visibility no longer triggers a descendant `:has()` re-evaluation on every keystroke. diff --git a/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css b/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css index 16afc9dd10a..536df6ca50f 100644 --- a/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css +++ b/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css @@ -121,8 +121,14 @@ .TextInput { padding-left: var(--base-size-8) !important; - /* stylelint-disable-next-line selector-class-pattern, selector-no-qualifying-type, selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited (github/github-ui#17224) */ - &:has(input:placeholder-shown) :global(.TextInput-action) { + /* + * `data-empty` lives on the inner `` (TextInput spreads unknown + * props onto the input, not its wrapper). The clear-action button is a + * later sibling of the input within this wrapper, so a general-sibling + * combinator targets it directly without a `:has()` walk. + */ + /* stylelint-disable-next-line selector-class-pattern, selector-no-qualifying-type -- `.TextInput-action` is a third-party class on the wrapped TextInput */ + & input[data-empty] ~ :global(.TextInput-action) { display: none; } } diff --git a/packages/react/src/experimental/SelectPanel2/SelectPanel.tsx b/packages/react/src/experimental/SelectPanel2/SelectPanel.tsx index cc9efdea44a..ce6ac54d31d 100644 --- a/packages/react/src/experimental/SelectPanel2/SelectPanel.tsx +++ b/packages/react/src/experimental/SelectPanel2/SelectPanel.tsx @@ -430,11 +430,18 @@ const SelectPanelSearchInput: FCWithSlotMarker = ({ const {setSearchQuery, moveFocusToList} = React.useContext(SelectPanelContext) + // Track whether the input is empty so the clear-action can be hidden via a + // data attribute instead of a `:has(input:placeholder-shown)` selector that + // forced a descendant re-evaluation on every keystroke. + const [uncontrolledEmpty, setUncontrolledEmpty] = React.useState(() => !(props.value ?? props.defaultValue)) + const isEmpty = props.value !== undefined ? props.value === '' : uncontrolledEmpty + const internalOnChange = (event: React.ChangeEvent) => { // If props.onChange is given, the application controls search, // otherwise the component does if (typeof propsOnChange === 'function') propsOnChange(event) else setSearchQuery(event.target.value) + setUncontrolledEmpty(event.target.value === '') } const internalKeyDown = (event: React.KeyboardEvent) => { @@ -464,10 +471,12 @@ const SelectPanelSearchInput: FCWithSlotMarker = ({ // @ts-ignore TODO this is a hacky solution to clear propsOnChange({target: inputRef.current, currentTarget: inputRef.current}) } + setUncontrolledEmpty(true) }} /> } className={clsx(classes.TextInput, className)} + data-empty={isEmpty || undefined} onChange={internalOnChange} onKeyDown={internalKeyDown} {...props}