Skip to content

fix(Thumbnail, Selector, MultiSelector): a nested button rings itself, not the thumbnail or trigger too - #8874

Draft
cixzhang wants to merge 5 commits into
fix/focus-ring-one-owner-cardsfrom
fix/focus-ring-one-owner-thumbnail-selector
Draft

cixzhang wants to merge 5 commits into
fix/focus-ring-one-owner-cardsfrom
fix/focus-ring-one-owner-thumbnail-selector

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Part 3 of 3 for #8850: Thumbnail, Selector and MultiSelector. Stacked on #8872 (part 2), which is stacked on #8869 (part 1, focusWithinOwner); retarget to main once they land.

User impact

A keyboard user who tabs to a Thumbnail's remove button, or to the clear or status button of a ghost or secondary Selector or MultiSelector, sees two rings: the button's, and a second around the whole thumbnail or trigger. Now each tab stop shows one ring.

Clear focused, ghost Selector Before After
1280
390
Remove focused, Thumbnail Before After
1280
390

Problem and solution fit

  • Cause: Thumbnail's image container and the ghost and secondary trigger containers ring with focusWithin, which matches any focused descendant.
  • Fix: each container spreads part 1's shared ring focusWithinOwner from privateAttributes/focusOwner.stylex.ts. The Thumbnail's Open button and each trigger button (both direct children) spread focusOwnerProps. The input variant, which draws its focus as a border, is unchanged.

Current specification

architecture:interaction-modality INV3 (current): one focus move has one owning indicator; sibling controls paint their own focus.

Evidence

  • FocusRingOwner.a11y.chromium.spec.ts: new cases for Thumbnail › With Remove and for ghost and secondary Clearable Selector and MultiSelector (story args variant:ghost and variant:secondary), at 1280 and 390 px. On the previous head each shows two rings while Remove or Clear is focused. All 18 cases pass.
  • Selector, MultiSelector and Thumbnail unit suites pass, except the two MultiSelector bottom-sheet tests that time out under load on main too.

Scope

Changeset: [fix] patch for @astryxdesign/core.

…, not the thumbnail or trigger too

architecture:interaction-modality INV3: one focus move has one owning
indicator. Thumbnail's image container and the ghost and secondary
Selector and MultiSelector trigger containers rang with focusWithin, so a
Thumbnail's remove button, and a trigger's clear or status button, drew
their own ring and the container a second around it. Each container now
uses focusWithinOwner, and the Thumbnail's Open button and each trigger
button carry FOCUS_OWNER_ATTRIBUTE.

FocusRingOwner's browser spec gains a Thumbnail With Remove case and ghost
and secondary Clearable cases for both selectors, at 1280 and 390 px; they
fail on the previous head (two rings while Remove or Clear is focused).

Stacked on #8872.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 28, 2026
@vercel

vercel Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 31, 2026 7:17pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟡 Reviewed (for maintainers only) Oct 31, 2026, 7:31 PM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change looks ready to merge once #8869 and #8872 land: focusing a Thumbnail's Remove, or the Clear or status button of a ghost or secondary Selector or MultiSelector, now shows a single ring, and the new browser cases fail before the change and pass after it. One optional follow-up on the same lines: the Thumbnail's Open button still keeps the browser's default focus ring inside the container's ring, so a Thumbnail with onClick shows two rings on Open; applying focusOutlineStyles.suppressed to that button, as focusWithinOwner expects, plus an Open-and-Remove browser case would close the rest of that gap.

[Automated review]

aria-label={t('@astryx.thumbnail.open', {accessibleName})}
aria-disabled={isInteractive ? undefined : 'true'}
{...{[FOCUS_OWNER_ATTRIBUTE]: ''}}
{...stylex.props(styles.interactiveButton)}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional, the same gap component:Thumbnail FR4 records: this Open button keeps the browser's default focus ring (outline: auto 1px in Chromium), so focusing Open still paints two rings, the container's focusWithinOwner ring and this one. focusWithinOwner asks for the owner to be paired with focusOutlineStyles.suppressed, which the already-imported helper applies:

{...focusOutlineProps.suppressed(styles.interactiveButton)}>

A Thumbnail story with both onClick and onRemove would let the browser spec cover Open too.

@cixzhang
cixzhang marked this pull request as draft October 30, 2026 21:06
… ring

Follows #8869: focusWithinOwner and FOCUS_OWNER_ATTRIBUTE come from the
unexported internal/focusOutline.stylex.ts (focusOwnerStyles for the two
selectors' stylex.props lists), so nothing new is published.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested for the Thumbnail part; the Selector and MultiSelector changes look ready once #8869 and #8872 land. With the image container no longer ringing for Remove, Remove's own focus ring is its only indicator, and over a dark image it is nearly invisible (about 1.08:1 against the image in the default light theme), so keyboard users lose track of focus on thumbnails with showRemoveOn="always" or with both onClick and onRemove. Please keep the container ring for Remove (leave Thumbnail.tsx out of this pull request) until Remove has a focus ring that reads on any image, or add such a ring with design review. Optionally, add ^packages/core/src/internal/focusOutline to the spec's @pr-a11y scope line so changes to the owner ring rerun these browser cases.

[Automated review]

focusOutlineProps.focusWithin(
// Rings for the Open button only; the remove button rings itself
// (`architecture:interaction-modality` INV3).
focusWithinOwnerProps(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Over a dark image this leaves Remove without a visible focus indicator: the container's ring, drawn on the page background, no longer shows for Remove, and Remove's own accent ring paints over the image inside the container's overflow: hidden. With a #222222 image in the default light theme the ring is about 1.08:1 against the image, so focused and resting Remove look the same, and tabbing from Open to Remove makes the only visible ring disappear. component:Thumbnail FR4 asks for one indicator per focus move. Keeping the container ring for Remove until Remove has a ring that reads on any image (or adding one with design review) would avoid the regression.

@@ -1,19 +1,20 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// @pr-a11y scope ^packages/core/src/(Item|Token|ClickableCard|SelectableCard)/ ^packages/core/src/utils/focusOutline ^apps/storybook/stories/(Item|Token|ClickableCard|SelectableCard)
// @pr-a11y scope ^packages/core/src/(Item|Token|ClickableCard|SelectableCard|Thumbnail|Selector|MultiSelector)/ ^packages/core/src/utils/focusOutline ^apps/storybook/stories/(Item|Token|ClickableCard|SelectableCard|Thumbnail|Selector|MultiSelector)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: the owner ring now lives in internal/focusOutline.stylex.ts, which ^packages/core/src/utils/focusOutline does not match, so a later change to that file alone will not run this spec. Adding ^packages/core/src/internal/focusOutline here would cover it.

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 31, 2026
…-owner-thumbnail-selector; Thumbnail, Selector and MultiSelector spread the focus-owner owner module's ring and props

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested for the Thumbnail part; the Selector and MultiSelector changes look ready once #8869 and #8872 land. Over a dark image, Remove's own focus ring (about 1.08:1 against the image in the default theme) is now its only indicator, so keyboard users lose track of focus on thumbnails with showRemoveOn="always" or with both onClick and onRemove; please keep the container ring for Remove until Remove has a ring that reads on any image, or leave Thumbnail.tsx out of this pull request. Please also narrow the changeset's "each tab stop shows one focus ring": a Thumbnail's Open button still paints two rings (the container's and the browser default), unless you suppress Open's default ring and add a browser case for it.

[Automated review]

// Rings for the Open button only; the remove button rings itself
// (`architecture:interaction-modality` INV3).
stylex.props(
focusWithinOwner,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the container no longer ringing for Remove, Remove's own ring is its only indicator, and it paints over the image inside the container's overflow: hidden. Over a dark image (#222222) in the default theme that ring measures about 1.08:1, so a focused Remove looks the same as a resting one, and tabbing from Open to Remove makes the only visible ring disappear. Keeping the container ring for Remove until Remove has a ring that reads on any image would avoid this.

'@astryxdesign/core': patch
---

[fix] Focusing a `Thumbnail`'s remove button, or the clear or status button of a ghost or secondary `Selector` or `MultiSelector`, no longer also rings the thumbnail or the whole trigger: each tab stop shows one focus ring.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"each tab stop shows one focus ring" is broader than the fix: a Thumbnail with onClick still paints two rings on Open (the container's and the browser's default auto ring on the button, which has no outline reset). Either reword to say the focused remove, clear or status button shows the only ring, or suppress Open's default ring and add an Open-and-Remove browser case.

This branch was successfully deployed

1 active deployment
Preview — 2d214416 Deployed Oct 31, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant