@astryxdesign/core 0.1.9-canary.75d2d03 → 0.1.9-canary.ae1eaed

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,9 +10,12 @@
10
10
  * - /packages/core/src/Avatar/index.ts (exports if types change)
11
11
  * - /apps/storybook/stories/Avatar.stories.tsx (storybook stories)
12
12
  * - /packages/cli/templates/blocks/components/Avatar/ (showcase blocks)
13
+ *
14
+ * Last synced props: alt, fallbackSrc, name, size, src, status, href, as, target, rel, onClick
13
15
  */
14
16
  import { type ReactNode } from 'react';
15
17
  import type { BaseProps } from '../BaseProps';
18
+ import type { LinkComponentType } from '../Link/types';
16
19
  /**
17
20
  * Named size options.
18
21
  *
@@ -84,6 +87,35 @@ export interface AvatarProps extends BaseProps<HTMLDivElement> {
84
87
  * @default true
85
88
  */
86
89
  tooltip?: string | boolean;
90
+ /**
91
+ * When provided, the avatar becomes an interactive link (`<a>` or custom
92
+ * link component) pointing at `href`. Follows the same element-swap rules as
93
+ * Button: `href` renders a link, otherwise `onClick` renders a
94
+ * `<button type="button">`, otherwise the avatar stays a static (non-focusable)
95
+ * element. An interactive avatar requires a meaningful accessible name via
96
+ * `alt` or `name`.
97
+ */
98
+ href?: string;
99
+ /**
100
+ * Custom link component to use when `href` is provided. Overrides the
101
+ * provider-level default set by LinkProvider. Useful for Next.js `<Link>` or
102
+ * other router-aware components. Only applies when `href` is provided.
103
+ */
104
+ as?: LinkComponentType;
105
+ /**
106
+ * HTML target attribute for the link. Only applies when `href` is provided.
107
+ */
108
+ target?: string;
109
+ /**
110
+ * HTML rel attribute for the link. Only applies when `href` is provided.
111
+ */
112
+ rel?: string;
113
+ /**
114
+ * Click handler. When provided without `href`, renders the avatar as a
115
+ * focusable `<button type="button">`. An interactive avatar requires a
116
+ * meaningful accessible name via `alt` or `name`.
117
+ */
118
+ onClick?: React.MouseEventHandler<HTMLElement>;
87
119
  }
88
120
  /**
89
121
  * Avatar component for displaying user profile pictures.
@@ -98,9 +130,11 @@ export interface AvatarProps extends BaseProps<HTMLDivElement> {
98
130
  * <Avatar src="/user.jpg" status={<OnlineIndicator />} />
99
131
  * <Avatar name="jsmith" tooltip="Jane Smith, Staff Engineer" />
100
132
  * <Avatar name="Jane" tooltip={false} />
133
+ * <Avatar src="/user.jpg" name="John Doe" href="/users/john" />
134
+ * <Avatar src="/user.jpg" name="John Doe" onClick={() => openProfile()} />
101
135
  * ```
102
136
  */
103
- export declare function Avatar({ alt, 'data-testid': testId, fallbackSrc, name, size, src, status, tooltip, xstyle, className, style, ref, ...props }: AvatarProps): import("react").JSX.Element;
137
+ export declare function Avatar({ alt, 'data-testid': testId, fallbackSrc, name, size, src, status, tooltip, href, as, target, rel, onClick, xstyle, className, style, ref, ...props }: AvatarProps): import("react").JSX.Element;
104
138
  export declare namespace Avatar {
105
139
  var displayName: string;
106
140
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../../src/Avatar/Avatar.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AACxD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAuC5C;;;;;;GAMG;AACH,KAAK,eAAe,GAAG,KAAK,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEzD;;GAEG;AACH,KAAK,iBAAiB,GACpB,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEzE;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,eAAe,GAAG,iBAAiB,CAAC;AAE7D;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAgBpD;AAkGD,MAAM,WAAW,WAAY,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC5D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC5B;AAsCD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,MAAM,CAAC,EACrB,GAAG,EACH,aAAa,EAAE,MAAM,EACrB,WAAW,EACX,IAAI,EACJ,IAAW,EACX,GAAG,EACH,MAAM,EACN,OAAc,EACd,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,WAAW,+BAqJb;yBAnKe,MAAM"}
1
+ {"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../../src/Avatar/Avatar.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AACxD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAc5C,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAC;AA2BrD;;;;;;GAMG;AACH,KAAK,eAAe,GAAG,KAAK,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEzD;;GAEG;AACH,KAAK,iBAAiB,GACpB,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEzE;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,eAAe,GAAG,iBAAiB,CAAC;AAE7D;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAgBpD;AAmID,MAAM,WAAW,WAAY,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC5D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,EAAE,CAAC,EAAE,iBAAiB,CAAC;IACvB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;CAChD;AAsCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CAAC,EACrB,GAAG,EACH,aAAa,EAAE,MAAM,EACrB,WAAW,EACX,IAAI,EACJ,IAAW,EACX,GAAG,EACH,MAAM,EACN,OAAc,EACd,IAAI,EACJ,EAAE,EACF,MAAM,EACN,GAAG,EACH,OAAO,EACP,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,WAAW,+BAyOb;yBA5Pe,MAAM"}
@@ -14,6 +14,8 @@
14
14
  * - /packages/core/src/Avatar/index.ts (exports if types change)
15
15
  * - /apps/storybook/stories/Avatar.stories.tsx (storybook stories)
16
16
  * - /packages/cli/templates/blocks/components/Avatar/ (showcase blocks)
17
+ *
18
+ * Last synced props: alt, fallbackSrc, name, size, src, status, href, as, target, rel, onClick
17
19
  */
18
20
  import { useMemo, useState } from 'react';
19
21
  import * as stylex from '@stylexjs/stylex';
@@ -24,7 +26,8 @@ import { useAvatarGroup } from "../AvatarGroup/AvatarGroupContext.js";
24
26
  import { mergeProps, mergeRefs } from "../utils/index.js";
25
27
  import { themeProps } from "../utils/themeProps.js";
26
28
  import { useTooltip } from "../Tooltip/useTooltip.js";
27
-
29
+ import { useLinkComponent } from "../Link/useLinkComponent.js";
30
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
28
31
  /**
29
32
  * The offset ratio for positioning elements on a circle's edge at 45°.
30
33
  *
@@ -37,7 +40,6 @@ import { useTooltip } from "../Tooltip/useTooltip.js";
37
40
  *
38
41
  * This constant represents that ratio: (1 - 1/√2) / 2 ≈ 0.146
39
42
  */
40
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
41
43
  const CIRCLE_EDGE_OFFSET_RATIO = (1 - 1 / Math.SQRT2) / 2;
42
44
 
43
45
  /**
@@ -129,6 +131,24 @@ const styles = {
129
131
  kI3sdo: "x17nn4n9",
130
132
  kInvED: "x1wfwxd8 x7s97pk",
131
133
  $$css: true
134
+ },
135
+ interactive: {
136
+ kysU6D: "xjyslct",
137
+ kmVPX3: "x1717udv",
138
+ kogj98: "x1ghz6dp",
139
+ kMzoRj: "xc342km",
140
+ ksu8eU: "xng3xce",
141
+ kWkggS: "xjbqb8w",
142
+ kMwMTN: "x1heor9g",
143
+ kVVagm: "xln7xf2",
144
+ kybGjl: "x1hl2dhg",
145
+ kkrTdU: "x1ypdohk",
146
+ kaIpWk: "xjspbzw",
147
+ kMeerF: "x1k57tk5 x784prv",
148
+ k3XXqK: "x1t137rt x9v5kkp",
149
+ kjBf7l: "xzunyri",
150
+ kInvED: "x1wfwxd8 x7s97pk",
151
+ $$css: true
132
152
  }
133
153
  };
134
154
 
@@ -238,6 +258,8 @@ function DefaultIcon({
238
258
  * <Avatar src="/user.jpg" status={<OnlineIndicator />} />
239
259
  * <Avatar name="jsmith" tooltip="Jane Smith, Staff Engineer" />
240
260
  * <Avatar name="Jane" tooltip={false} />
261
+ * <Avatar src="/user.jpg" name="John Doe" href="/users/john" />
262
+ * <Avatar src="/user.jpg" name="John Doe" onClick={() => openProfile()} />
241
263
  * ```
242
264
  */
243
265
  export function Avatar({
@@ -249,6 +271,11 @@ export function Avatar({
249
271
  src,
250
272
  status,
251
273
  tooltip = true,
274
+ href,
275
+ as,
276
+ target,
277
+ rel,
278
+ onClick,
252
279
  xstyle,
253
280
  className,
254
281
  style,
@@ -293,18 +320,117 @@ export function Avatar({
293
320
  // returns `describedBy` as a value we choose whether to apply — the only way
294
321
  // to satisfy the per-case aria-describedby rule (default name: none; custom
295
322
  // string: describe) without editing Tooltip. `focusTrigger: 'auto'` shows the
296
- // tooltip on keyboard focus once the root is made focusable (below).
323
+ // tooltip on keyboard focus once the root is focusable (natively for the
324
+ // interactive <a>/<button>, or via an explicit tab stop on the static div).
297
325
  const tooltipHook = useTooltip({
298
326
  placement: 'above',
299
327
  isEnabled: showTooltip
300
328
  });
329
+ // The tooltip ref attaches to whichever root element renders (static or
330
+ // interactive), so the tooltip works for link/button avatars too.
301
331
  const rootRef = mergeRefs(ref, showTooltip ? tooltipHook.ref : undefined);
302
332
  const describedByProp = showTooltip && isCustomTooltip ? {
303
333
  'aria-describedby': [props['aria-describedby'], tooltipHook.describedBy].filter(Boolean).join(' ') || undefined
304
334
  } : null;
305
- const avatarElement = /*#__PURE__*/_jsx(AvatarSizeContext, {
306
- value: numericSize,
307
- children: /*#__PURE__*/_jsxs("div", {
335
+
336
+ // Element-swap trichotomy, copied from Button: `href` renders a link,
337
+ // otherwise `onClick` renders a `<button>`, otherwise today's static element
338
+ // is unchanged (the non-breaking default).
339
+ const renderAsLink = href != null;
340
+ const renderAsButton = !renderAsLink && onClick != null;
341
+ const isInteractive = renderAsLink || renderAsButton;
342
+ const LinkComponent = useLinkComponent(as);
343
+
344
+ // An interactive control with no accessible name is an unacceptable control
345
+ // name. Warn in the same client-safe way sibling components do (Field,
346
+ // Timestamp, Popover) — a plain `console.warn`, never gated on `process.env`
347
+ // (which is not available on the client in this codebase).
348
+ if (isInteractive && !accessibleName) {
349
+ console.warn('Avatar: an interactive avatar (with `href` or `onClick`) needs a ' + 'meaningful accessible name. Pass `alt` or `name`.');
350
+ }
351
+
352
+ // The inner visuals are identical across the static and interactive variants.
353
+ const visualContent = /*#__PURE__*/_jsxs(_Fragment, {
354
+ children: [/*#__PURE__*/_jsxs("div", {
355
+ ...stylex.props(styles.content, dynamicStyles.size(numericSize)),
356
+ children: [showImage && /*#__PURE__*/_jsx("img", {
357
+ src: src,
358
+ alt: "",
359
+ onError: () => setErroredSrc(src),
360
+ ...{
361
+ className: "xh8yej3 x5yr21d xl1xv1r"
362
+ }
363
+ }), showFallbackImage && /*#__PURE__*/_jsx("img", {
364
+ src: fallbackSrc,
365
+ alt: "",
366
+ onError: () => setErroredFallbackSrc(fallbackSrc),
367
+ ...{
368
+ className: "xh8yej3 x5yr21d xl1xv1r"
369
+ }
370
+ }), showInitials && /*#__PURE__*/_jsx("div", {
371
+ ...stylex.props(styles.fallback, dynamicStyles.fontSize(numericSize)),
372
+ children: getInitials(name)
373
+ }), showIcon && /*#__PURE__*/_jsx("div", {
374
+ ...{
375
+ className: "x78zum5 x6s0dn4 xl56j7k xh8yej3 x5yr21d x17x4s8c xv1l7n4 x9ynric x1e4wzip xtvhhri"
376
+ },
377
+ children: /*#__PURE__*/_jsx(DefaultIcon, {
378
+ size: numericSize
379
+ })
380
+ })]
381
+ }), status && /*#__PURE__*/_jsx("div", {
382
+ ...stylex.props(styles.status, dynamicStyles.statusPosition(numericSize)),
383
+ children: status
384
+ })]
385
+ });
386
+
387
+ // Shared StyleX + theme props for the root element in every variant. The
388
+ // group ring/overlap, the interactive focus-visible ring, and the
389
+ // tooltip tab-stop focus ring all live here so the interactive
390
+ // `<a>`/`<button>` and the static `<div>` carry the exact same box.
391
+ const rootStylexProps = mergeProps(themeProps('avatar', {
392
+ size: resolvedSize
393
+ }), stylex.props(styles.wrapper, isInteractive && styles.interactive, !isInteractive && showTooltip && !avatarGroup && styles.focusable, avatarGroup && groupStyles.ring, avatarGroup && groupStyles.overlap, avatarGroup && groupDynamicStyles.overlap(-avatarGroup.overlap), xstyle), className, style);
394
+ let rootElement;
395
+
396
+ // `props` is typed for the default `<div>` root (its event handlers are
397
+ // HTMLDivElement-typed). The interactive branches render an `<a>`/`<button>`,
398
+ // so the passthrough props are re-typed to the generic element here — the
399
+ // avatar's own handlers (onClick) are declared on HTMLElement and stay typed.
400
+ const interactivePassthrough = props;
401
+ if (renderAsLink) {
402
+ // The rendered link carries the `data-avatar-item` marker so AvatarGroup's
403
+ // roving focus (which selects on `[data-avatar-item]`, not a tag/role) picks
404
+ // it up while ignoring nested buttons in a custom status/badge slot.
405
+ rootElement = /*#__PURE__*/_jsx(LinkComponent, {
406
+ ...interactivePassthrough,
407
+ ...describedByProp,
408
+ ref: rootRef,
409
+ href: href,
410
+ target: target,
411
+ rel: rel,
412
+ "aria-label": accessibleName,
413
+ "data-avatar-item": "",
414
+ "data-testid": testId,
415
+ onClick: onClick,
416
+ ...rootStylexProps,
417
+ children: visualContent
418
+ });
419
+ } else if (renderAsButton) {
420
+ rootElement = /*#__PURE__*/_jsx("button", {
421
+ ...interactivePassthrough,
422
+ ...describedByProp,
423
+ ref: rootRef,
424
+ type: "button",
425
+ "aria-label": accessibleName,
426
+ "data-avatar-item": "",
427
+ "data-testid": testId,
428
+ onClick: onClick,
429
+ ...rootStylexProps,
430
+ children: visualContent
431
+ });
432
+ } else {
433
+ rootElement = /*#__PURE__*/_jsx("div", {
308
434
  ...props,
309
435
  ref: rootRef,
310
436
  role: isDecorative ? 'presentation' : 'img',
@@ -312,46 +438,19 @@ export function Avatar({
312
438
  "aria-hidden": isDecorative || undefined
313
439
  // The root is a div[role="img"], not natively focusable. When a name
314
440
  // tooltip is active, add a tab stop so keyboard users can reveal it
315
- // (WCAG 1.4.13 / 2.1.1) — matching Timestamp/Button. Only while active.
441
+ // (WCAG 1.4.13 / 2.1.1) — matching Timestamp/Button. Suppressed inside
442
+ // an AvatarGroup, which owns a single roving tab stop for its members.
316
443
  ,
317
- tabIndex: showTooltip ? 0 : undefined,
444
+ tabIndex: showTooltip && !avatarGroup ? 0 : undefined,
318
445
  "data-testid": testId,
319
446
  ...describedByProp,
320
- ...mergeProps(themeProps('avatar', {
321
- size: resolvedSize
322
- }), stylex.props(styles.wrapper, showTooltip && styles.focusable, avatarGroup && groupStyles.ring, avatarGroup && groupStyles.overlap, avatarGroup && groupDynamicStyles.overlap(-avatarGroup.overlap), xstyle), className, style),
323
- children: [/*#__PURE__*/_jsxs("div", {
324
- ...stylex.props(styles.content, dynamicStyles.size(numericSize)),
325
- children: [showImage && /*#__PURE__*/_jsx("img", {
326
- src: src,
327
- alt: "",
328
- onError: () => setErroredSrc(src),
329
- ...{
330
- className: "xh8yej3 x5yr21d xl1xv1r"
331
- }
332
- }), showFallbackImage && /*#__PURE__*/_jsx("img", {
333
- src: fallbackSrc,
334
- alt: "",
335
- onError: () => setErroredFallbackSrc(fallbackSrc),
336
- ...{
337
- className: "xh8yej3 x5yr21d xl1xv1r"
338
- }
339
- }), showInitials && /*#__PURE__*/_jsx("div", {
340
- ...stylex.props(styles.fallback, dynamicStyles.fontSize(numericSize)),
341
- children: getInitials(name)
342
- }), showIcon && /*#__PURE__*/_jsx("div", {
343
- ...{
344
- className: "x78zum5 x6s0dn4 xl56j7k xh8yej3 x5yr21d x17x4s8c xv1l7n4 x9ynric x1e4wzip xtvhhri"
345
- },
346
- children: /*#__PURE__*/_jsx(DefaultIcon, {
347
- size: numericSize
348
- })
349
- })]
350
- }), status && /*#__PURE__*/_jsx("div", {
351
- ...stylex.props(styles.status, dynamicStyles.statusPosition(numericSize)),
352
- children: status
353
- })]
354
- })
447
+ ...rootStylexProps,
448
+ children: visualContent
449
+ });
450
+ }
451
+ const avatarElement = /*#__PURE__*/_jsx(AvatarSizeContext, {
452
+ value: numericSize,
453
+ children: rootElement
355
454
  });
356
455
  if (!showTooltip) {
357
456
  return avatarElement;
@@ -8,6 +8,12 @@
8
8
  * one AvatarGroupOverflow). The group provides overlap styling via
9
9
  * context — no child introspection needed.
10
10
  *
11
+ * When the group contains interactive avatars (rendered as links/buttons) or an
12
+ * interactive AvatarGroupOverflow, it becomes a single Tab stop with roving
13
+ * arrow-key focus (via useListFocus with hasRovingTabIndex) and exposes a
14
+ * screen-reader keyboard hint via aria-describedby. A purely static facepile
15
+ * gets neither — it stays a plain non-focusable group.
16
+ *
11
17
  * SYNC: When modified, update these files to stay in sync:
12
18
  * - /packages/core/src/AvatarGroup/AvatarGroup.doc.mjs (props table, features)
13
19
  * - /packages/core/src/AvatarGroup/index.ts (exports if types change)
@@ -53,7 +59,7 @@ export interface AvatarGroupProps extends BaseProps<HTMLDivElement> {
53
59
  * </AvatarGroup>
54
60
  * ```
55
61
  */
56
- export declare function AvatarGroup({ children, size, 'data-testid': testId, 'aria-label': ariaLabelFromProps, xstyle, className, style, ref, ...props }: AvatarGroupProps): ReactNode;
62
+ export declare function AvatarGroup({ children, size, 'data-testid': testId, 'aria-label': ariaLabelFromProps, 'aria-describedby': ariaDescribedByFromProps, onKeyDown, onFocus, xstyle, className, style, ref, ...props }: AvatarGroupProps): ReactNode;
57
63
  export declare namespace AvatarGroup {
58
64
  var displayName: string;
59
65
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AvatarGroup.d.ts","sourceRoot":"","sources":["../../src/AvatarGroup/AvatarGroup.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAU,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAC9C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAc,KAAK,UAAU,EAAC,MAAM,WAAW,CAAC;AASvD,MAAM,WAAW,gBAAiB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACjE,yCAAyC;IACzC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AASD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,IAAW,EACX,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,kBAAkB,EAChC,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,gBAAgB,GAAG,SAAS,CA6B9B;yBAvCe,WAAW"}
1
+ {"version":3,"file":"AvatarGroup.d.ts","sourceRoot":"","sources":["../../src/AvatarGroup/AvatarGroup.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAA2B,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAC/D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAc,KAAK,UAAU,EAAC,MAAM,WAAW,CAAC;AAavD,MAAM,WAAW,gBAAiB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACjE,yCAAyC;IACzC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AASD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,IAAW,EACX,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,kBAAkB,EAChC,kBAAkB,EAAE,wBAAwB,EAC5C,SAAS,EACT,OAAO,EACP,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,gBAAgB,GAAG,SAAS,CAuE9B;yBApFe,WAAW"}
@@ -11,20 +11,30 @@
11
11
  * one AvatarGroupOverflow). The group provides overlap styling via
12
12
  * context — no child introspection needed.
13
13
  *
14
+ * When the group contains interactive avatars (rendered as links/buttons) or an
15
+ * interactive AvatarGroupOverflow, it becomes a single Tab stop with roving
16
+ * arrow-key focus (via useListFocus with hasRovingTabIndex) and exposes a
17
+ * screen-reader keyboard hint via aria-describedby. A purely static facepile
18
+ * gets neither — it stays a plain non-focusable group.
19
+ *
14
20
  * SYNC: When modified, update these files to stay in sync:
15
21
  * - /packages/core/src/AvatarGroup/AvatarGroup.doc.mjs (props table, features)
16
22
  * - /packages/core/src/AvatarGroup/index.ts (exports if types change)
17
23
  * - /apps/storybook/stories/AvatarGroup.stories.tsx (storybook stories)
18
24
  * - /packages/cli/templates/blocks/components/AvatarGroup/ (showcase blocks)
19
25
  */
20
- import { useMemo } from 'react';
26
+ import { useId, useMemo, useState } from 'react';
21
27
  import { resolveSize } from "../Avatar/index.js";
22
28
  import * as stylex from '@stylexjs/stylex';
23
- import { mergeProps } from "../utils/index.js";
29
+ import { mergeProps, mergeRefs } from "../utils/index.js";
30
+ import { composeEventHandlers } from "../utils/composeEventHandlers.js";
24
31
  import { AvatarGroupContext } from "./AvatarGroupContext.js";
25
32
  import { themeProps } from "../utils/themeProps.js";
26
33
  import { useTranslator } from "../i18n/index.js";
27
- import { jsx as _jsx } from "react/jsx-runtime";
34
+ import { useListFocus } from "../hooks/useListFocus.js";
35
+ import { useIsomorphicLayoutEffect } from "../hooks/useIsomorphicLayoutEffect.js";
36
+ import { VisuallyHidden } from "../VisuallyHidden/index.js";
37
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
28
38
  const OVERLAP_RATIO = 0.25;
29
39
  const styles = {
30
40
  root: {
@@ -57,6 +67,9 @@ export function AvatarGroup({
57
67
  size = 'md',
58
68
  'data-testid': testId,
59
69
  'aria-label': ariaLabelFromProps,
70
+ 'aria-describedby': ariaDescribedByFromProps,
71
+ onKeyDown,
72
+ onFocus,
60
73
  xstyle,
61
74
  className,
62
75
  style,
@@ -72,18 +85,57 @@ export function AvatarGroup({
72
85
  overlap,
73
86
  numericSize
74
87
  }), [size, overlap, numericSize]);
88
+
89
+ // The keyboard hint and roving tab stop only make sense once the group has
90
+ // interactive children. Detect their presence (and the writing direction for
91
+ // RTL-correct arrow navigation) from the rendered DOM after commit, matching
92
+ // how the codebase reads direction elsewhere (`getComputedStyle(el).direction`).
93
+ const [hasInteractiveItems, setHasInteractiveItems] = useState(false);
94
+ const [isRtl, setIsRtl] = useState(false);
95
+
96
+ // Single tab stop + roving arrow focus over the group's interactive items.
97
+ // `itemSelector` targets the shared `[data-avatar-item]` marker stamped on
98
+ // interactive avatars (their rendered <a>/<button>) and on the overflow
99
+ // button — NOT a tag/role selector — so roving never catches a nested button
100
+ // inside a custom status/badge slot (O5).
101
+ const {
102
+ listRef,
103
+ handleKeyDown,
104
+ handleFocus
105
+ } = useListFocus({
106
+ itemSelector: '[data-avatar-item]',
107
+ orientation: 'horizontal',
108
+ hasRovingTabIndex: true,
109
+ isRtl
110
+ });
111
+ useIsomorphicLayoutEffect(() => {
112
+ const root = listRef.current;
113
+ if (!root) {
114
+ return;
115
+ }
116
+ setHasInteractiveItems(root.querySelector('[data-avatar-item]') != null);
117
+ setIsRtl(getComputedStyle(root).direction === 'rtl');
118
+ });
119
+ const hintId = useId();
120
+ const describedBy = [ariaDescribedByFromProps, hasInteractiveItems ? hintId : null].filter(Boolean).join(' ') || undefined;
75
121
  return /*#__PURE__*/_jsx(AvatarGroupContext, {
76
122
  value: contextValue,
77
- children: /*#__PURE__*/_jsx("div", {
123
+ children: /*#__PURE__*/_jsxs("div", {
78
124
  ...props,
79
- ref: ref,
125
+ ref: mergeRefs(ref, listRef),
80
126
  role: "group",
81
127
  "aria-label": ariaLabel,
128
+ "aria-describedby": describedBy,
82
129
  "data-testid": testId,
130
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
131
+ onFocus: composeEventHandlers(onFocus, handleFocus),
83
132
  ...mergeProps(themeProps('avatar-group', {
84
133
  size
85
134
  }), stylex.props(styles.root, xstyle), className, style),
86
- children: children
135
+ children: [children, hasInteractiveItems && /*#__PURE__*/_jsx(VisuallyHidden, {
136
+ id: hintId,
137
+ children: t('@astryx.avatarGroup.keyboardHint')
138
+ })]
87
139
  })
88
140
  });
89
141
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AvatarGroupOverflow.d.ts","sourceRoot":"","sources":["../../src/AvatarGroup/AvatarGroupOverflow.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAW5C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAM5C,MAAM,WAAW,wBAAyB,SAAQ,IAAI,CACpD,SAAS,CAAC,WAAW,CAAC,EACtB,SAAS,CACV;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AA4ED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,GAAG,EACH,KAAK,EACL,OAAO,EACP,QAAQ,EACR,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,wBAAwB,GAAG,SAAS,CAwDtC;yBAjEe,mBAAmB"}
1
+ {"version":3,"file":"AvatarGroupOverflow.d.ts","sourceRoot":"","sources":["../../src/AvatarGroup/AvatarGroupOverflow.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAW5C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAM5C,MAAM,WAAW,wBAAyB,SAAQ,IAAI,CACpD,SAAS,CAAC,WAAW,CAAC,EACtB,SAAS,CACV;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AA4ED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,GAAG,EACH,KAAK,EACL,OAAO,EACP,QAAQ,EACR,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,wBAAwB,GAAG,SAAS,CAyDtC;yBAlEe,mBAAmB"}
@@ -117,6 +117,7 @@ export function AvatarGroupOverflow({
117
117
  onClick: onClick,
118
118
  ...rest,
119
119
  "aria-label": label,
120
+ "data-avatar-item": "",
120
121
  ...mergeProps(themeProps('avatar-group-overflow'), stylex.props(styles.base, styles.button, styles.overlap, dynamicStyles.size(numericSize), dynamicStyles.fontSize(numericSize), dynamicStyles.overlap(-overlap), xstyle), className, style),
121
122
  children: content
122
123
  });
package/dist/astryx.css CHANGED
@@ -867,6 +867,8 @@
867
867
  .x1y3gkto:not(#\#):not(#\#):not(#\#){outline-offset:1px}
868
868
  .x1hl8ikr:not(#\#):not(#\#):not(#\#){outline-offset:2px}
869
869
  .x1g710d4:not(#\#):not(#\#):not(#\#){outline-offset:var(--_tree-focus-outline-offset,0)}
870
+ .x1t137rt:not(#\#):not(#\#):not(#\#){outline-style:none}
871
+ .x1k57tk5:not(#\#):not(#\#):not(#\#){outline-width:0}
870
872
  .x9y5y3v:not(#\#):not(#\#):not(#\#){overflow-clip-margin:1px}
871
873
  .x1mzt3pk:not(#\#):not(#\#):not(#\#){overflow-wrap:break-word}
872
874
  .x1h4wwuj:not(#\#):not(#\#):not(#\#){overflow-wrap:normal}
@@ -1068,11 +1070,14 @@
1068
1070
  .x25t5g8:focus-visible:not(#\#):not(#\#):not(#\#){opacity:1}
1069
1071
  .xof6bs:is(th:hover *):not(#\#):not(#\#):not(#\#){opacity:1}
1070
1072
  .xofkqq2:popover-open:not(#\#):not(#\#):not(#\#){opacity:1}
1073
+ .xzunyri:focus-visible:not(#\#):not(#\#):not(#\#){outline-color:var(--color-accent)}
1071
1074
  .x1edzap1.x1edzap1:where(.x1iwu4tg:has(:focus-visible) *):not(#\#):not(#\#):not(#\#){outline-offset:2px}
1072
1075
  .x7s97pk:focus-visible:not(#\#):not(#\#):not(#\#){outline-offset:2px}
1073
1076
  .x81n7kt.x81n7kt:where(.x1tspnpo:has(:focus-visible) *):not(#\#):not(#\#):not(#\#){outline-offset:2px}
1074
1077
  .x13aywxo:focus-visible:not(#\#):not(#\#):not(#\#){outline-offset:var(--button-focus-offset)}
1075
1078
  .xh2nty3:focus-visible:not(#\#):not(#\#):not(#\#){outline-offset:var(--spacing-0-5)}
1079
+ .x9v5kkp:focus-visible:not(#\#):not(#\#):not(#\#){outline-style:solid}
1080
+ .x784prv:focus-visible:not(#\#):not(#\#):not(#\#){outline-width:2px}
1076
1081
  .xbt4iw:focus-within:not(#\#):not(#\#):not(#\#){pointer-events:auto}
1077
1082
  .x1mqgsch:is([data-astryx-scrolling]):not(#\#):not(#\#):not(#\#){scrollbar-width:none}
1078
1083
  .xgtlewx:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){transform:scaleX(-1)}