Skip to content

feat(Avatar): add tooltip prop (string | boolean) for name-on-hover (#4164)#4219

Draft
cixzhang wants to merge 1 commit into
mainfrom
feat/avatar-name-tooltip
Draft

feat(Avatar): add tooltip prop (string | boolean) for name-on-hover (#4164)#4219
cixzhang wants to merge 1 commit into
mainfrom
feat/avatar-name-tooltip

Conversation

@cixzhang

Copy link
Copy Markdown
Contributor

What

Adds one prop to Avatar: tooltip?: string | boolean (default effectively true), giving every named avatar an identify-on-hover tooltip for free.

  • omitted / true → show the avatar's name on hover and keyboard focus
  • a string → show that text instead (no need to wrap in <Tooltip>)
  • false → no tooltip
  • no name/alt (decorative), or a true/omitted tooltip with an empty/whitespace name → no tooltip

Closes #4164.

Why

Avatar already exposes identity to assistive tech via the root aria-label (alt || name), but a sighted mouse/keyboard user hovering an unfamiliar face gets nothing, and every consumer that wants it has to hand-roll <Tooltip content={name}><Avatar .../></Tooltip> — boilerplate that drifts and is applied inconsistently. Making the name tooltip a built-in default matches how mature avatar UIs (facepiles, comment headers, contributor stacks) behave.

API — convention reuse

tooltip?: string | boolean reuses two established system conventions rather than inventing a new one:

  • tooltip?: string ships on Button, Link, StatusDot, ToggleButton.
  • string | boolean dual-purpose union ships on Link.download / TopNavItem.download (bool toggles, string configures).

The only intentional divergence: the existing tooltip?: string props default to off, whereas Avatar defaults to show the name — because Avatar uniquely already holds the name. For rich/ReactNode tooltips, wrap in Tooltip/HoverCard and set tooltip={false} (kept text-only on purpose; no placement/delay knobs on Avatar).

Avatar owns the tooltip (no wrapper DOM)

Avatar owns its tooltip via the existing useTooltip hook — the same pattern Button uses — attaching the hook's ref to the root and rendering the tooltip as a sibling. No extra wrapper <div>, no changes to Tooltip/HoverCard/Layer.

Implementation note: the spec sketched the lazy <Tooltip anchorRef=…> pattern from Timestamp. I used the useTooltip hook (Button's pattern) instead because it is the only Avatar-only way to satisfy the a11y decision below: <Tooltip anchorRef> unconditionally sets aria-describedby on the anchor, and the spec forbids editing Tooltip. The hook returns describedBy as a value Avatar chooses whether to apply — giving per-case control. Trade-off: the hook isn't lazy-loaded (Button ships it eagerly too); this is the correct a11y-compliant path.

Deliberate: no auto-disable when wrapped

Per the approved spec, there is no automatic overlay detection and no shared OverlayAncestorContext. The name tooltip is a purely visual concern; accessibility never depends on it (the root aria-label is always correct). If a consumer layers their own Tooltip/HoverCard, they explicitly opt out with tooltip={false} — trivial and discoverable with this same prop. Wrapping without opting out yields at most a visual double-popup, never an a11y problem. (If this proves a real foot-gun in practice, detection can be revisited later.)

Accessibility

  • Root aria-label stays alt || name unconditionally, independent of the tooltip. Decorative avatars stay role="presentation" + aria-hidden and get no tooltip.
  • Default name tooltip is visual-only: it does not wire aria-describedby (its text duplicates the root's accessible name, so describing it too would double-announce the same name).
  • Custom string tooltip is wired to aria-describedby (it adds information), composing with any consumer-supplied aria-describedby — matching Button.
  • When a tooltip is active, the root (div[role="img"], not natively focusable) gains tabIndex={0} + a focus-visible ring so keyboard users can reveal it (WCAG 1.4.13 / 2.1.1) — matching Timestamp/Button. No tab stop when the tooltip is off.

Testing

New tests (all green, plus the existing Avatar suite unchanged):

  • default shows name; tooltip="…" shows the custom string (not the name); tooltip={false} renders no tooltip
  • decorative (no name/alt) and whitespace-only-name-with-default → no tooltip; custom string with no name → still shows
  • root aria-label unchanged across default / disabled / custom-string / alt cases
  • no aria-describedby for the default name tooltip; present (and pointing at the tooltip layer) for a custom string; consumer aria-describedby composed for custom, preserved untouched for default
  • root focusable only when a tooltip is attached; consumer ref forwarded to the root

Also verified Tooltip and AvatarGroup suites stay green.

Files (Avatar-only)

  • packages/core/src/Avatar/Avatar.tsx
  • packages/core/src/Avatar/Avatar.test.tsx
  • packages/core/src/Avatar/Avatar.doc.mjs
  • apps/storybook/stories/Avatar.stories.tsx (default / custom / disabled stories)
  • .changeset/avatar-name-tooltip.md

No changes to Tooltip, HoverCard, or Layer.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 22, 2026
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jul 22, 2026 11:05pm

Request Review

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Jul 22, 2026
@cixzhang

Copy link
Copy Markdown
Contributor Author

Self-review — Quick Self-Review checklist (Component Build Protocol)

  1. Icons from registry — n/a; no icons added. The existing DefaultIcon SVG is untouched.
  2. themeProps present — the root already carries themeProps('avatar', …); the tooltip layer is rendered by the shared useTooltip hook (already themed via themeProps('tooltip')). No new interactive element bypasses theming.
  3. No wrapper divs — the tooltip attaches to the existing Avatar root via the hook's ref (mergeRefs(ref, tooltipHook.ref)); it renders as a sibling. No extra wrapper DOM.
  4. No CSS shorthands — the added focusable style uses explicit outline/outlineOffset only; no flex/shorthand introduced.
  5. ARIA still correct — verified by tests: root aria-label unchanged in all cases; aria-describedby absent for the default name tooltip (avoids double-announce), present for a custom string (matches Button), and consumer aria-describedby is composed (custom) / preserved (default). Root gains a tab stop only when a tooltip is active.
  6. Tests pass locallyAvatar (20 tests) green; Tooltip and AvatarGroup suites re-run green. tsc --noEmit, eslint on changed files, and pnpm -F @astryxdesign/core build all clean.
  7. Sandbox/stories updated — added NameTooltip, CustomTooltip, TooltipDisabled stories and a tooltip argType; doc (Avatar.doc.mjs) updated across props / docsDense.propDescriptions / best practices; SYNC header refreshed.
  8. Popover background — n/a; reuses the existing Tooltip layer styling (inverted palette) via useTooltip.renderTooltip, unchanged.

Spec compliance / deviations:

  • Followed the contract: tooltip?: string | boolean, default-on, Avatar-owned, no auto-disable / no OverlayAncestorContext, no edits to Tooltip/HoverCard/Layer, minor-category ([feat], pre-1.0 patch bump per repo convention).
  • One deviation from the spec's 5.3 sketch: used the useTooltip hook (Button's pattern) rather than the lazy <Tooltip anchorRef> (Timestamp's pattern). Reason: the a11y decision (OQ-4) requires per-case aria-describedby — none for the default name tooltip, present for a custom string. <Tooltip anchorRef> sets aria-describedby unconditionally and editing Tooltip is out of scope, so the hook (which exposes describedBy as an opt-in value) is the only Avatar-only way to satisfy it. Trade-off: not lazy-loaded (Button ships it eagerly too). Flagged in the PR description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Avatar: show name tooltip by default, auto-disable when wrapped in a tooltip/hovercard

1 participant