Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/focus-outline-consistency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@astryxdesign/core': patch
---

[fix] Consolidate general interactive focus outlines so components share the same accent-colored ring geometry.
@cixzhang
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ function renderedClassLiterals() {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (entry.name.endsWith('.tsx') && !entry.name.endsWith('.test.tsx')) {
} else if (
(entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) &&
!entry.name.includes('.test.')
) {
const text = fs.readFileSync(full, 'utf8');
for (const re of [
/themeProps\(\s*'([^']+)'/g,
Expand Down Expand Up @@ -110,6 +113,7 @@ describe('theme build emits a live TextInput selector (#4109)', () => {
` 'side-nav-item': { base: { borderRadius: '12px' } },\n` +
` 'chat-composer': { base: { padding: '10px' } },\n` +
` 'chat-message-bubble': { 'variant:ghost': { borderRadius: '18px' } },\n` +
` 'focus-outline': { base: { outlineOffset: '4px' } },\n` +
` },\n` +
`};\n`,
);
Expand All @@ -122,6 +126,7 @@ describe('theme build emits a live TextInput selector (#4109)', () => {
expect(css).toContain('.astryx-side-nav-item');
expect(css).toContain('.astryx-chat-composer');
expect(css).toContain('.astryx-chat-message-bubble.ghost');
expect(css).toContain('.astryx-focus-outline');
});

it('emits .astryx-text-input (the rendered class), not the dead .astryx-textinput', async () => {
Expand Down
30 changes: 3 additions & 27 deletions packages/core/src/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {AvatarSizeContext} from './AvatarSizeContext';
import {useAvatarGroup} from '../AvatarGroup/AvatarGroupContext';
import {mergeProps, mergeRefs} from '../utils';
import {themeProps} from '../utils/themeProps';
import {focusOutlineProps} from '../utils/focusOutline.stylex';
import {useTooltip} from '../Tooltip/useTooltip';
import {useLinkComponent} from '../Link/useLinkComponent';
import type {LinkComponentType} from '../Link/types';
Expand Down Expand Up @@ -152,16 +153,7 @@ const styles = stylex.create({
// Visible focus ring for the name-tooltip tab stop, matching the repo-wide
// focus-visible outline treatment (see Timestamp, Token, Thumbnail). Only
// applied when a tooltip is active so keyboard users can reveal it.
focusable: {
outline: {
default: null,
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
},
outlineOffset: {
default: '0',
':focus-visible': '2px',
},
},
focusable: {},
// Reset the intrinsic styling of the interactive element (<a>/<button>) so it
// is a transparent, correctly-sized wrapper around the avatar visuals. The
// element carries the focus-visible accent ring for keyboard users.
Expand All @@ -178,22 +170,6 @@ const styles = stylex.create({
cursor: 'pointer',
// Match the avatar's circular shape so the focus ring hugs it.
borderRadius: radiusVars['--radius-full'],
outlineWidth: {
default: 0,
':focus-visible': 2,
},
outlineStyle: {
default: 'none',
':focus-visible': 'solid',
},
outlineColor: {
default: null,
':focus-visible': colorVars['--color-accent'],
},
outlineOffset: {
default: 0,
':focus-visible': 2,
},
},
});

Expand Down Expand Up @@ -580,7 +556,7 @@ export function Avatar({
// `<a>`/`<button>` and the static `<div>` carry the exact same box.
const rootStylexProps = mergeProps(
themeProps('avatar', {size: resolvedSize}),
stylex.props(
focusOutlineProps.focusVisible(
styles.wrapper,
isInteractive && styles.interactive,
!isInteractive && showTooltip && !avatarGroup && styles.focusable,
Expand Down
11 changes: 2 additions & 9 deletions packages/core/src/AvatarGroup/AvatarGroupOverflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {mergeProps} from '../utils';
import {useAvatarGroup} from './AvatarGroupContext';
import type {BaseProps} from '../BaseProps';
import {themeProps} from '../utils/themeProps';
import {focusOutlineProps} from '../utils/focusOutline.stylex';
import {useTranslator} from '../i18n';

const BORDER_WIDTH = 2;
Expand Down Expand Up @@ -93,14 +94,6 @@ const styles = stylex.create({
':active': `linear-gradient(${colorVars['--color-overlay-pressed']}, ${colorVars['--color-overlay-pressed']}), linear-gradient(${colorVars['--color-neutral']}, ${colorVars['--color-neutral']})`,
},
// Focus ring via focus-visible
outline: {
default: 'none',
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
},
outlineOffset: {
default: null,
':focus-visible': '2px',
},
},
overlap: {
marginInlineStart: 'var(--_avatar-group-overlap)',
Expand Down Expand Up @@ -170,7 +163,7 @@ export function AvatarGroupOverflow({
data-avatar-item=""
{...mergeProps(
themeProps('avatar-group-overflow'),
stylex.props(
focusOutlineProps.focusVisible(
styles.base,
styles.button,
styles.overlap,
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/Button/Button.doc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ export const docs = {
],
vars: [
{name: '--_button-radius', description: 'Border radius', default: 'var(--radius-element)', private: true},
{name: '--button-focus-offset', description: 'Focus ring outline offset', default: '3px'},
{name: '--button-icon-only-aspect', description: 'Aspect ratio for icon-only buttons', default: '1 / 1'},
],
derived: [
Expand Down Expand Up @@ -245,7 +244,6 @@ export const docsZh = {
],
vars: [
{name: '--_button-radius', description: '圆角半径', default: 'var(--radius-element)', private: true},
{name: '--button-focus-offset', description: '焦点环轮廓偏移', default: '3px'},
{name: '--button-icon-only-aspect', description: '纯图标按钮的宽高比', default: '1 / 1'},
],
derived: [
Expand Down
44 changes: 4 additions & 40 deletions packages/core/src/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {mergeProps, mergeRefs} from '../utils';
import {useLinkComponent} from '../Link/useLinkComponent';
import type {LinkComponentType} from '../Link/types';
import {themeProps} from '../utils/themeProps';
import {focusOutlineProps} from '../utils/focusOutline.stylex';
import {useTranslator} from '../i18n';

/**
Expand Down Expand Up @@ -178,7 +179,7 @@ const elevationStyles = stylex.create({
* Variant styles using backgroundImage for layered colors
* Pseudo-classes are nested within properties per StyleX recommendation
* Overlay is stacked on top of base color using multiple linear-gradients
* Focus outline color matches variant (destructive uses negative color)
* Focus outline is shared across variants for consistent keyboard affordance.
*/
const variants = stylex.create({
primary: {
Expand All @@ -191,15 +192,6 @@ const variants = stylex.create({
},
':active': `linear-gradient(${colorVars['--color-overlay-pressed']}, ${colorVars['--color-overlay-pressed']})`,
},
outline: {
default: null,
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
},
'--button-focus-offset': '3px',
outlineOffset: {
default: '0',
':focus-visible': 'var(--button-focus-offset)',
},
},
secondary: {
backgroundColor: colorVars['--color-neutral'],
Expand All @@ -211,15 +203,6 @@ const variants = stylex.create({
},
':active': `linear-gradient(${colorVars['--color-overlay-pressed']}, ${colorVars['--color-overlay-pressed']})`,
},
outline: {
default: null,
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
},
'--button-focus-offset': '3px',
outlineOffset: {
default: '0',
':focus-visible': 'var(--button-focus-offset)',
},
},
ghost: {
backgroundColor: 'transparent',
Expand All @@ -231,15 +214,6 @@ const variants = stylex.create({
},
':active': `linear-gradient(${colorVars['--color-overlay-pressed']}, ${colorVars['--color-overlay-pressed']})`,
},
outline: {
default: null,
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
},
'--button-focus-offset': '3px',
outlineOffset: {
default: '0',
':focus-visible': 'var(--button-focus-offset)',
},
},
destructive: {
backgroundColor: colorVars['--color-error'],
Expand All @@ -251,15 +225,6 @@ const variants = stylex.create({
},
':active': `linear-gradient(${colorVars['--color-overlay-pressed']}, ${colorVars['--color-overlay-pressed']})`,
},
outline: {
default: null,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is kind of nice to have which is an error-specific outline color.

':focus-visible': `2px solid ${colorVars['--color-error']}`,
},
'--button-focus-offset': '3px',
outlineOffset: {
default: '0',
':focus-visible': 'var(--button-focus-offset)',
},
},
});

Expand Down Expand Up @@ -695,7 +660,7 @@ export function Button({
const edgeCompAttr = isFlat ? {[EDGE_COMP_ATTR]: ''} : null;

// Shared StyleX props for both button and link rendering
const sharedStylexProps = stylex.props(
const sharedStylexProps = focusOutlineProps.focusVisible(
styles.base,
sizeStyles[size],
variants[variant],
Expand All @@ -712,8 +677,7 @@ export function Button({
(variant === 'primary' || variant === 'destructive') &&
(buttonGroup.orientation === 'horizontal'
? groupStyles.onSolidHorizontal
: groupStyles.onSolidVertical),
// Standalone floating buttons only — a grouped button's elevation is owned
: groupStyles.onSolidVertical), // Standalone floating buttons only — a grouped button's elevation is owned
// by the ButtonGroup so the shared surface lifts as one unit.
!buttonGroup && elevationStyles[elevation],
width != null && dynamicStyles.width(width),
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
DATE_FORMAT_MONTH_YEAR,
} from '../utils/plainDate';
import {mergeProps, composeEventHandlers, rtlStyles} from '../utils';
import {focusOutlineProps} from '../utils/focusOutline.stylex';
import {
computeDayCellState,
computeRangeRounding,
Expand Down Expand Up @@ -1102,7 +1103,7 @@ function DayCell({
// the default rendering.
marker: markerState,
}),
stylex.props(
focusOutlineProps.focusVisible(
dayCellStyles.day,
dayCellTheme.day,
isOutside && dayCellStyles.dayOutside,
Expand Down
8 changes: 0 additions & 8 deletions packages/core/src/Calendar/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,14 +258,6 @@ export const dayCellTheme = stylex.create({
'@media (hover: hover)': `linear-gradient(${colorVars['--color-overlay-hover']}, ${colorVars['--color-overlay-hover']})`,
},
},
outline: {
default: null,
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
},
outlineOffset: {
default: '0',
':focus-visible': '2px',
},
},

// Outside days (adjacent months)
Expand Down
17 changes: 6 additions & 11 deletions packages/core/src/ClickableCard/ClickableCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {useClickableContainer} from '../hooks/useClickableContainer';
import type {BaseProps} from '../BaseProps';
import {useLinkComponent} from '../Link/useLinkComponent';
import {themeProps} from '../utils/themeProps';
import {focusOutlineProps} from '../utils/focusOutline.stylex';

// =============================================================================
// Styles — only the interactive layer, Card handles everything else
Expand All @@ -57,13 +58,6 @@ const styles = stylex.create({
cursor: 'pointer',
textDecoration: 'none',
color: 'inherit',
outlineOffset: '2px',
},
focusWithin: {
':has(:focus-visible)': {
outline: `2px solid ${colorVars['--color-accent']}`,
outlineOffset: '2px',
},
},
// Hover overlay — guarded by @media (hover: hover) so touch devices
// don't show a stuck hover state. Active/pressed state works everywhere.
Expand Down Expand Up @@ -307,14 +301,15 @@ export function ClickableCard({
padding={padding}
variant={variant}
elevation={elevation}
{...mergeProps(themeProps('clickable-card', {variant}), {
className: classNameProp,
{...mergeProps(
themeProps('clickable-card', {variant}),
focusOutlineProps.focusWithin(),
classNameProp,
style,
})}
)}
xstyle={
[
styles.interactive,
styles.focusWithin,
hasBorder ? styles.bordered : styles.borderless,
!isDisabled && styles.overlay,
!isDisabled && styles.hoverOnPointer,
Expand Down
11 changes: 2 additions & 9 deletions packages/core/src/CodeBlock/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import type {SyntaxToken, TokenLine} from './tokenizer';
import {ensureHighlightStyles} from './highlightStyles';
import {applyHighlightRangesChunked} from './highlightRanges';
import {themeProps} from '../utils/themeProps';
import {focusOutlineProps} from '../utils/focusOutline.stylex';
import {useTranslator} from '../i18n';
import {SyntaxTheme, type SyntaxThemeDefinition} from '../theme/syntax';

Expand Down Expand Up @@ -214,14 +215,6 @@ const styles = stylex.create({
// Restore a keyboard-only focus ring with the standard token/offset so this
// disclosure control matches the rest of the system (Collapsible, TabMenu);
// otherwise it falls back to the inconsistent UA default outline.
outline: {
default: null,
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
},
outlineOffset: {
default: '0',
':focus-visible': '2px',
},
},
code: {
display: 'block',
Expand Down Expand Up @@ -867,7 +860,7 @@ export function CodeBlock({
}
: undefined
}
{...stylex.props(
{...focusOutlineProps.focusVisible(
styles.header,
canCollapse && styles.headerCollapsible,
)}>
Expand Down
11 changes: 2 additions & 9 deletions packages/core/src/Collapsible/Collapsible.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {getIcon} from '../Icon/globalIconRegistry';
import {mergeProps} from '../utils';
import type {BaseProps} from '../BaseProps';
import {themeProps} from '../utils/themeProps';
import {focusOutlineProps} from '../utils/focusOutline.stylex';

const styles = stylex.create({
root: {
Expand Down Expand Up @@ -73,14 +74,6 @@ const styles = stylex.create({
paddingBlock: 0,
// `all: unset` above wipes the UA focus outline; restore a keyboard-only
// focus ring using the standard token/offset (WCAG 2.4.7).
outline: {
default: null,
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
},
outlineOffset: {
default: '0',
':focus-visible': '2px',
},
},
// Capsize: trim leading from text triggers
triggerLabel: {
Expand Down Expand Up @@ -327,7 +320,7 @@ export function Collapsible({
themeProps('collapsible-trigger', {
density: density ?? undefined,
}),
stylex.props(
focusOutlineProps.focusVisible(
styles.trigger,
density != null && triggerDensity[density],
isDisabled && styles.triggerDisabled,
Expand Down
Loading