From 6db70d18315605fc962bd941db2590aa3fac0457 Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Wed, 26 Aug 2026 11:14:36 +1000 Subject: [PATCH] Remove the unused resting focus ring (#506) --- .../src/primitives/checkbox/recipe.css.ts | 3 +-- .../src/primitives/combobox/styles.css.ts | 24 ++++++++++++------- .../src/primitives/input-group/recipe.css.ts | 3 +-- .../@luke-ui/react/src/styles/focus-ring.ts | 10 -------- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/packages/@luke-ui/react/src/primitives/checkbox/recipe.css.ts b/packages/@luke-ui/react/src/primitives/checkbox/recipe.css.ts index 5b169e9a..7943953a 100644 --- a/packages/@luke-ui/react/src/primitives/checkbox/recipe.css.ts +++ b/packages/@luke-ui/react/src/primitives/checkbox/recipe.css.ts @@ -1,5 +1,5 @@ import { createVar, fallbackVar } from '@vanilla-extract/css'; -import { focusRing, restingFocusRing } from '../../styles/focus-ring.js'; +import { focusRing } from '../../styles/focus-ring.js'; import type { RecipeSelection, SlottedConfigInput } from '../../styles/recipe.js'; import { recipe } from '../../styles/recipe.js'; import { textLineHeight } from '../../text/recipe.css.js'; @@ -102,7 +102,6 @@ const checkboxConfig = { inlineSize: checkboxIndicatorSize, justifyContent: 'center', lineHeight: 1, - ...restingFocusRing(), transitionDuration: vars.motion.duration.feedback, transitionProperty: 'background-color, background-image, border-color, color, opacity', transitionTimingFunction: vars.motion.easing.standard, diff --git a/packages/@luke-ui/react/src/primitives/combobox/styles.css.ts b/packages/@luke-ui/react/src/primitives/combobox/styles.css.ts index f7feb4c8..3e085a47 100644 --- a/packages/@luke-ui/react/src/primitives/combobox/styles.css.ts +++ b/packages/@luke-ui/react/src/primitives/combobox/styles.css.ts @@ -1,7 +1,7 @@ import type { StyleRule } from '@vanilla-extract/css'; import { createVar } from '@vanilla-extract/css'; import { FIELD_CONTROL_ICON_SIZE } from '../../sizing/control-size.js'; -import { focusRing, restingFocusRing } from '../../styles/focus-ring.js'; +import { focusRing } from '../../styles/focus-ring.js'; import { composeInputStateSelectors, descendantDisabledSelector, @@ -21,11 +21,16 @@ import { FONT_METRIC_SCALE } from '../../theme/font-metric-scale.js'; // `::after` icon matches the trigger/clear chevrons at each size instead of a constant. const comboboxErrorIconSize = createVar(); -// React Aria's `ComboBox` publishes `isDisabled`/`isInvalid` through `GroupContext`, which -// `Group` writes onto the group element, so no `:has()` probing of descendants is needed. +// React Aria publishes disabled and invalid state on the group, so those states +// do not need to probe descendants. const { disabled, focusWithin, hover, invalid, invalidFocusWithin, readOnly, readOnlyFocusWithin } = composeInputStateSelectors(); +// The well ring tracks the text input so inner actions do not paint a second ring. +const inputFocus = `${focusWithin}:has(input:focus)`; +const invalidInputFocus = `${invalidFocusWithin}:has(input:focus)`; +const readOnlyInputFocus = `${readOnlyFocusWithin}:has(input:focus)`; + const comboboxActionStyles = { '@media': { '(forced-colors: active)': { @@ -144,8 +149,8 @@ const comboboxConfig = { forcedColorAdjust: 'auto', selectors: { [disabled]: { borderColor: 'GrayText', color: 'GrayText', opacity: 1 }, - [focusWithin]: { outlineColor: 'Highlight' }, - [invalidFocusWithin]: { outlineColor: 'Highlight' }, + [inputFocus]: { outlineColor: 'Highlight' }, + [invalidInputFocus]: { outlineColor: 'Highlight' }, // `invalidFocusWithin` is a strict subset of `invalid` and nothing else // here touches `::after`, so this already covers the focused case. [`${invalid}::after`]: invalidIndicatorIconForcedColors, @@ -169,7 +174,8 @@ const comboboxConfig = { letterSpacing: FONT_METRIC_SCALE[16].letterSpacing, lineHeight: FONT_METRIC_SCALE[16].lineHeight, minInlineSize: 0, - ...restingFocusRing('0px'), + // Suppress the reset ring React Aria exposes on the group when an inner action is focus-visible. + outline: 'none', overflow: 'visible', transitionDuration: vars.motion.duration.feedback, transitionProperty: 'background-color, border-color, color', @@ -177,7 +183,7 @@ const comboboxConfig = { selectors: { [disabled]: { cursor: 'not-allowed', opacity: vars.interaction.disabledOpacity }, - [focusWithin]: { + [inputFocus]: { borderColor: vars.color.border.accent, ...focusRing(vars.color.border.focus), }, @@ -192,7 +198,7 @@ const comboboxConfig = { // `invalidFocusWithin` is a strict subset of `invalid` and nothing else // here touches `::after`, so this already covers the focused case. [`${invalid}::after`]: invalidIndicatorIcon(comboboxErrorIconSize), - [invalidFocusWithin]: { + [invalidInputFocus]: { borderColor: vars.color.background.danger.solid.rest, ...focusRing(vars.color.border.focus), }, @@ -201,7 +207,7 @@ const comboboxConfig = { borderColor: vars.color.border.decorative, boxShadow: 'none', }, - [readOnlyFocusWithin]: { ...focusRing(vars.color.border.focus) }, + [readOnlyInputFocus]: { ...focusRing(vars.color.border.focus) }, }, }, textInput: { diff --git a/packages/@luke-ui/react/src/primitives/input-group/recipe.css.ts b/packages/@luke-ui/react/src/primitives/input-group/recipe.css.ts index 22f4393d..c059333c 100644 --- a/packages/@luke-ui/react/src/primitives/input-group/recipe.css.ts +++ b/packages/@luke-ui/react/src/primitives/input-group/recipe.css.ts @@ -1,4 +1,4 @@ -import { focusRing, restingFocusRing } from '../../styles/focus-ring.js'; +import { focusRing } from '../../styles/focus-ring.js'; import { composeInputStateSelectors, descendantDisabledSelector, @@ -58,7 +58,6 @@ const inputGroupConfig = { letterSpacing: FONT_METRIC_SCALE[16].letterSpacing, lineHeight: FONT_METRIC_SCALE[16].lineHeight, minInlineSize: 0, - ...restingFocusRing('0px'), overflow: 'visible', transitionDuration: vars.motion.duration.feedback, transitionProperty: 'background-color, border-color, color', diff --git a/packages/@luke-ui/react/src/styles/focus-ring.ts b/packages/@luke-ui/react/src/styles/focus-ring.ts index 721ec34d..7fa6a61a 100644 --- a/packages/@luke-ui/react/src/styles/focus-ring.ts +++ b/packages/@luke-ui/react/src/styles/focus-ring.ts @@ -6,13 +6,3 @@ export function focusRing(color: string) { outlineWidth: '2px', } as const; } - -/** The transparent resting focus ring every focusable control starts from. */ -export function restingFocusRing(offset: string = '2px') { - return { - outlineColor: 'transparent', - outlineOffset: offset, - outlineStyle: 'solid', - outlineWidth: '2px', - } as const; -} -- 2.51.2