@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.
- package/dist/Avatar/Avatar.d.ts +35 -1
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +142 -43
- package/dist/AvatarGroup/AvatarGroup.d.ts +7 -1
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.js +58 -6
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +1 -0
- package/dist/astryx.css +5 -0
- package/dist/astryx.umd.js +19 -19
- package/dist/astryx.umd.js.map +4 -4
- package/locales/en.json +4 -0
- package/locales/pseudo.json +3 -0
- package/package.json +1 -1
- package/src/Avatar/Avatar.doc.mjs +33 -0
- package/src/Avatar/Avatar.test.tsx +112 -0
- package/src/Avatar/Avatar.tsx +217 -60
- package/src/AvatarGroup/AvatarGroup.doc.mjs +2 -0
- package/src/AvatarGroup/AvatarGroup.test.tsx +125 -0
- package/src/AvatarGroup/AvatarGroup.tsx +58 -3
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +1 -0
package/dist/Avatar/Avatar.d.ts
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/Avatar/Avatar.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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.
|
|
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
|
-
...
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
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 {
|
|
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__*/
|
|
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,
|
|
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)}
|