Repository navigation
fix(Item, Token): a control inside rings itself, not the row or token too #8869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4078534
0018322
8e216ca
0b8105d
978b057
51d461d
cf7501f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] Focusing a control inside an `Item` (an action in `startContent` or `endContent`) or a `Token`'s remove button no longer also rings the whole row or token: each tab stop shows one focus ring. A row or token rings only for its own action, and a row without one draws no ring. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Delegation rows ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Delegation rows ( |
||
|
|
||
| @cixzhang | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ import {useState, type ReactNode} from 'react'; | |
| import {Button} from '@astryxdesign/core/Button'; | ||
| import * as stylex from '@stylexjs/stylex'; | ||
| import type {Meta, StoryObj} from '@storybook/react'; | ||
| import {IconButton} from '@astryxdesign/core/IconButton'; | ||
| import {expect} from 'storybook/test'; | ||
| import {Item} from '@astryxdesign/core/Item'; | ||
| import {Avatar} from '@astryxdesign/core/Avatar'; | ||
|
|
@@ -21,6 +22,7 @@ import {Stack} from '@astryxdesign/core/Layout'; | |
| import {List, ListItem} from '@astryxdesign/core/List'; | ||
| import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList'; | ||
| import {StatusDot} from '@astryxdesign/core/StatusDot'; | ||
| import {Token} from '@astryxdesign/core/Token'; | ||
|
|
||
| const storyStyles = stylex.create({ | ||
| markerAnatomy: { | ||
|
|
@@ -51,6 +53,8 @@ import { | |
| ArchiveBoxIcon, | ||
| EnvelopeIcon, | ||
| TrashIcon, | ||
| ShareIcon, | ||
| ArrowUturnLeftIcon, | ||
| } from '@heroicons/react/24/outline'; | ||
|
|
||
| const meta: Meta<typeof Item> = { | ||
|
|
@@ -321,6 +325,69 @@ export const SearchResults: Story = { | |
| ), | ||
| }; | ||
|
|
||
| /** | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block landed under |
||
| * Rows holding a control of their own in `endContent`: a row with an action | ||
| * rings for that action, a row without one draws no ring, and the end | ||
| * control rings itself, so each tab stop shows one focus ring. | ||
| */ | ||
| export const WithEndActions: Story = { | ||
| render: () => ( | ||
| <div style={{maxWidth: 420}}> | ||
| <Item | ||
| label="Quarterly report" | ||
| description="Opens the report" | ||
| onClick={() => {}} | ||
| endContent={ | ||
| <IconButton | ||
| label="Share Quarterly report" | ||
| icon={<Icon icon={ShareIcon} />} | ||
| variant="ghost" | ||
| size="sm" | ||
| /> | ||
| } | ||
| /> | ||
| <Item | ||
| label="Archived notes" | ||
| description="No row action" | ||
| endContent={ | ||
| <IconButton | ||
| label="Restore Archived notes" | ||
| icon={<Icon icon={ArrowUturnLeftIcon} />} | ||
| variant="ghost" | ||
| size="sm" | ||
| /> | ||
| } | ||
| /> | ||
| </div> | ||
| ), | ||
| }; | ||
|
|
||
| /** A row with its own action and a clickable Token in its end slot. */ | ||
| export const WithTokenAction: Story = { | ||
| render: () => ( | ||
| <div style={{maxWidth: 420}}> | ||
| <Item | ||
| label="Launch plan" | ||
| description="Opens the plan" | ||
| onClick={() => {}} | ||
| endContent={<Token label="Design" onClick={() => {}} />} | ||
| /> | ||
| </div> | ||
| ), | ||
| }; | ||
|
|
||
| /** Numbered rows with their own action: the marker sits beside the row. */ | ||
| export const NumberedWithActions: Story = { | ||
| render: () => ( | ||
| <div style={{maxWidth: 420}}> | ||
| <List listStyle="decimal"> | ||
| <ListItem label="Open the report" onClick={() => {}} /> | ||
| <ListItem label="Read the summary" href="#summary" /> | ||
| </List> | ||
| </div> | ||
| ), | ||
| }; | ||
|
|
||
| /** Disabled items. */ | ||
| export const Disabled: Story = { | ||
| render: () => ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,7 +59,12 @@ import {useLinkComponent} from '../Link/useLinkComponent'; | |
| import {useClickableContainer} from '../hooks/useClickableContainer'; | ||
| import {useDevWarning} from '../hooks/useDevWarning'; | ||
| import {themeProps} from '../utils/themeProps'; | ||
| import {focusOutlineProps} from '../utils/focusOutline.stylex'; | ||
| import {focusOutlineStyles} from '../utils/focusOutline.stylex'; | ||
| import { | ||
| focusOwnerGroupProps, | ||
| focusOwnerProps, | ||
| focusWithinOwner, | ||
| } from '../privateAttributes/focusOwner.stylex'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex'; | ||
| import {usePressFeedback} from '../hooks/usePressFeedback'; | ||
| import type {ItemSwipeBridge} from './ItemSwipeLayer'; | ||
|
|
@@ -800,6 +805,7 @@ export function Item({ | |
| {...(isDisabled | ||
| ? {role: 'link', 'aria-disabled': true, tabIndex: -1} | ||
| : {href, target, rel})} | ||
| {...focusOwnerProps} | ||
| {...stylex.props( | ||
| styles.invisibleAnchor, | ||
| isInline && styles.inlineContent, | ||
|
|
@@ -812,6 +818,7 @@ export function Item({ | |
| type="button" | ||
| onClick={onClick} | ||
| disabled={isDisabled} | ||
| {...focusOwnerProps} | ||
| {...stylex.props( | ||
| styles.invisibleButton, | ||
| isInline && styles.inlineContent, | ||
|
|
@@ -909,7 +916,12 @@ export function Item({ | |
| {...(isInteractive ? pressable : undefined)} | ||
| {...mergeProps( | ||
| themeProps('item', {density, align}), | ||
| focusOutlineProps.focusWithin( | ||
| // The row rings for its own primary only: its invisible button or | ||
| // link, or in delegation mode the nested control that is its sole tab | ||
| // stop. Any other control in it (a start or end slot) rings itself | ||
| // (`architecture:interaction-modality` INV3). | ||
| stylex.props( | ||
| isDelegate ? focusOutlineStyles.focusWithin : focusWithinOwner, | ||
| styles.root, | ||
| densityStyles[density], | ||
| align === 'start' && styles.alignStart, | ||
|
|
@@ -933,6 +945,9 @@ export function Item({ | |
| {isRenderable(marker) && marker} | ||
| {isRenderable(marker) && columnMarker === marker ? ( | ||
| <span | ||
| // The row's own button or link sits in this cell, so the row's | ||
| // ring reaches it here. | ||
| {...focusOwnerGroupProps} | ||
| {...stylex.props( | ||
| styles.markerContent, | ||
| align === 'first-line' && styles.markerContentFirstLine, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: delegation rows (
CheckboxListItem,RadioListItem, any Item withinteractiveRef) keep the any-descendant ring, so anendContentcontrol there still shows the row's ring too. Consider scoping this sentence to rows and tokens that have their own button or link.