Skip to content

feat(Avatar): add Avatar.Group and Avatar.Indicator#245

Open
johnleider wants to merge 12 commits into
masterfrom
worktree-feat-avatar-group
Open

feat(Avatar): add Avatar.Group and Avatar.Indicator#245
johnleider wants to merge 12 commits into
masterfrom
worktree-feat-avatar-group

Conversation

@johnleider
Copy link
Copy Markdown
Member

Summary

  • Adds Avatar.Group and Avatar.Indicator to the existing Avatar compound. Mirrors the Button.Group precedent: a flat sub-component owning its own context, with Avatar.Root optionally injecting the group via createContext's null fallback.
  • Two truncation modes that cooperate (min(max, capacityFromWidth)):
    • max — count-based cap. No observers, no overhead. Disabled avatars are exempt and always render. priority="start" | "end" controls which side keeps avatars.
    • responsive — opt-in width-based truncation. Wraps createOverflow inside useToggleScope so the ResizeObserver path is pay-for-what-you-use. Avatar.Root (the first ticket only) feeds itemWidth; Avatar.Indicator self-measures and feeds reserved.
  • Avatar.Indicator exposes count and hidden on its slot. Renders nothing outside a group or when the group is not overflowing.
  • Standalone Avatar.Root behavior is fully backward-compatible: new props (value, disabled, groupNamespace) default to safe no-ops, new slot props (isHidden, index) default to false / 0, new data attrs only render inside a group.
  • ARIA: group surfaces role="group" + label/labelledby/describedby; hidden avatars carry aria-hidden="true" so screen readers only announce the indicator's count; indicator carries aria-live="polite" and a localized aria-label via Avatar.indicatorLabel.

Docs

  • New ## Examples section with a single realistic team-roster panel (members data in a separate module, hover tooltips on every avatar, and a hover-list of truncated names on the +N chip via Avatar.Indicator's hidden slot prop).
  • Anatomy block now shows both the standalone and grouped shapes, matching the Button anatomy convention.

Adds a headless group container that collapses to a +N indicator under
either count-based (max) or opt-in width-responsive truncation. Mirrors
the Button.Group precedent: a flat sub-component owning its own context,
with Avatar.Root optionally injecting the group via createContext null
fallback. createOverflow is wired inside a useToggleScope so the
ResizeObserver path is pay-for-what-you-use.
Avatar.Group's responsive mode never fed createOverflow any width data,
so capacity collapsed to 0 and every avatar got hidden the moment the
container was measured. Avatar.Root now measures its own offsetWidth
into group.itemWidth (uniform mode), and the responsive docs example
passes :gap="-8" to account for the negative overlap.

Also tightens the ticket type contract: disabled is wrapped with toRef
at the register site to match the declared Readonly<Ref<boolean>>, the
private isHidden field is computed locally in AvatarRoot instead of
post-mutating onto the ticket, and the group's slot prop renames count
to total so it doesn't collide with the indicator's hidden-count slot.
…zero it

Every Avatar.Root was writing offsetWidth into the shared itemWidth ref.
When the responsive truncation hid later avatars via v-show, their
offsetWidth dropped to 0 and the next ResizeObserver tick zeroed
itemWidth, which made createOverflow.capacity revert to Infinity and
unhide every avatar — a measure/hide/unmeasure feedback loop.

Only the index 0 ticket now measures, and writes are skipped when
offsetWidth is 0. The unmount reset is similarly guarded so a
mid-list unmount can't clobber the value.
Bump the responsive demo to 30 avatars so width-based truncation is
visible at typical container sizes. Skip the negative marginInlineStart
on the first child of each group so it no longer hangs off the left
edge. Center the non-responsive examples in the example pane.
Replace the three contrived basic/max/responsive examples with a single
team-roster panel that ships the kind of UI Avatar.Group is actually
for: a labelled members row that fills its container, collapses into a
+N chip with a hover-list of the truncated members, and pairs every
avatar with a name/role tooltip. Data lives in a separate module so the
component file stays focused on composition and ARIA.

The page picks up an ## Examples heading to match the canonical docs
structure. The basic Image+Fallback example is centered in its pane for
visual consistency.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 26, 2026

Open in StackBlitz

commit: df7c51f

@johnleider johnleider self-assigned this May 26, 2026
@johnleider johnleider added this to the v0.2.x milestone May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant