Skip to content

fix(ClickableCard, SelectableCard): a control inside rings itself, not the card too - #8872

Draft
cixzhang wants to merge 7 commits into
fix/focus-ring-one-owner-item-tokenfrom
fix/focus-ring-one-owner-cards
Draft

cixzhang wants to merge 7 commits into
fix/focus-ring-one-owner-item-tokenfrom
fix/focus-ring-one-owner-cards

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Part 2 of 3 for #8850: ClickableCard and SelectableCard. Stacked on #8869 (part 1, which adds focusWithinOwner); retarget to main once it lands.

User impact

The card docs say "Nested interactive elements work independently", but a keyboard user who tabs to a button inside a ClickableCard or SelectableCard sees two rings: the button's, and a second around the whole card. Now the button shows one ring, and the card rings only when its own link, button or checkbox is focused.

"Add to cart" focused in a ClickableCard Before After
1280
390
"Details" focused in a SelectableCard Before After
1280
390

Problem and solution fit

  • Cause: both cards ring with focusWithin (:has(:focus-visible)), which matches any focused descendant.
  • Fix: each card spreads part 1's shared ring focusWithinOwner from privateAttributes/focusOwner.stylex.ts and marks its own primary (ClickableCard's screen-reader-only link or button, SelectableCard's checkbox) with focusOwnerProps. Both primaries are direct children of the card root.

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: gains a ClickableCard case (Core/ClickableCard › Nested Interactive Elements) and a SelectableCard case (the new Core/SelectableCard › With Nested Button), at 1280 and 390 px. On the previous head both show two rings while the inner button is focused; now 8/8 pass.
  • focusRings.ts: the helper now ignores an outline on a visually hidden control (the cards' screen-reader-only primaries), since a person can't see it.
  • ClickableCard and SelectableCard unit suites pass.

Scope

  • One defect, two components; Thumbnail and Selector are part 3.
  • No public API.
  • Public text and artifacts contain no internal Meta context.

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

…t the card too

architecture:interaction-modality INV3: one focus move has one owning
indicator. Both cards drew their ring with focusWithin, so a button in
their children (which the docs allow: "Nested interactive elements work
independently") drew its own ring and the card drew a second around it.
Each card now uses focusWithinOwner and marks its own link, button or
checkbox with FOCUS_OWNER_ATTRIBUTE.

SelectableCard gains a With Nested Button story; FocusRingOwner's browser
spec covers both cards at 1280 and 390 px and fails on the previous head.
focusRings ignores a visually hidden control's outline (the cards'
screen-reader-only primaries), which paints nothing a person sees.

Stacked on #8869 (focusWithinOwner).
@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:24 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 looks ready to approve: a keyboard-focused control inside a ClickableCard or SelectableCard now shows one focus ring instead of two, and the card still rings for its own link, button or checkbox, as the cards' focus contracts and architecture:interaction-modality INV3 require. The new browser cases pass on this branch and catch the double ring without the fix. As a non-blocking follow-up, both component records still describe the double ring as an open implementation gap and can drop those notes once this lands.

[Automated review]

@cixzhang
cixzhang marked this pull request as draft October 30, 2026 21:06
Follows #8869: focusWithinOwner and FOCUS_OWNER_ATTRIBUTE now come from the
unexported internal/focusOutline.stylex.ts, 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.

This looks ready to approve: a keyboard-focused control inside a ClickableCard or SelectableCard now shows one focus ring instead of two, and the card still rings for its own link, button or checkbox, as the cards' focus contracts and architecture:interaction-modality INV3 require. The new browser cases pass on this branch and fail when the fix is reverted. Two non-blocking follow-ups: both component records still describe the double ring as an open implementation gap, and the spec's @pr-a11y scope line could also route on packages/core/src/internal/focusOutline.stylex.ts, where the owner ring now lives.

[Automated review]

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: the owner ring these card cases exercise now lives in packages/core/src/internal/focusOutline.stylex.ts, which none of these tokens matches, so a pull request that edits only that file will not select this spec. Adding ^packages/core/src/internal/focusOutline would route it.

@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 looks ready to approve: a keyboard-focused control inside a ClickableCard or SelectableCard now shows one focus ring instead of two, and the card still rings for its own link, button or checkbox, as the cards' focus contracts and architecture:interaction-modality INV3 require. The new browser cases fail on the unfixed components and pass on this branch. Non-blocking follow-ups:

  • Both component records still describe the double ring as an open implementation gap.
  • The spec's @pr-a11y scope line could also route on packages/core/src/privateAttributes/focusOwner.stylex.ts and packages/core/src/Card/, which the card ring now depends on.
  • focusRings.ts skips any outlined box 1px or narrower even when it is not clipped; the clip checks alone keep every case passing.

[Automated review]

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: the card ring these cases exercise now lives in packages/core/src/privateAttributes/focusOwner.stylex.ts, and its > selector depends on Card rendering the hidden control straight inside its root, but no token here matches either path. Adding ^packages/core/src/(privateAttributes/focusOwner|Card/) would run this spec on a pull request that edits only one of them.

Comment on lines +45 to +46
box.width <= 1 ||
box.height <= 1 ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: this skips any outlined box 1px or narrower even when it is not clipped, and an unclipped tiny control's outline still paints, so a real second ring there would go uncounted. The two clip checks below are enough for the cards' hidden controls: with these two lines removed all 12 cases still pass.

This branch was successfully deployed

1 active deployment
Preview — a34faf12 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant