Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Components: Refine Avatar and AvatarGroup for PR readiness
- Remove built-in `.is-active` status override; all statuses now get
  uniform dimming so the consumer controls differentiation via the
  dynamic `is-{status}` class.
- Preserve Tooltip on badge avatars when `label` differs from `name`,
  so hovering reveals the full name even when the badge shows a
  contextual label like "You".
- Replace `cloneElement` z-index injection in AvatarGroup with a CSS
  `@for` loop on `:nth-child`, removing the legacy React API and its
  type assertion.
- Add `role="group"` to AvatarGroup for screen reader context;
  consumers provide `aria-label` via props.
- Add `aria-label` to the overflow indicator so "+2" reads as
  "2 more".
- Add MixedBadgeAndTooltip story covering badge, tooltip, and label
  combinations in a group.
- Improve JSDoc: clarify `badge` requires `name`, describe `status`
  as an open-ended styling hook.
- Add CHANGELOG entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • Loading branch information
dabowman and claude committed Feb 16, 2026
commit 7d773805d571663dbff72aadb9ea8c741b0b82e8
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

### Internal

- `Avatar`, `AvatarGroup`: Add new private components ([#75591](https://github.com/WordPress/gutenberg/pull/75591)).
- Remove `Picker` from private APIs ([#75394](https://github.com/WordPress/gutenberg/pull/75394)).
- Expose `useDrag` from `@use-gesture/react` package via private API's ([#66735](https://github.com/WordPress/gutenberg/pull/66735)).
- `Disabled`, `Modal`, `Popover`, `Tooltip`: Move context code to separate files to help docgen prop extraction ([#75316](https://github.com/WordPress/gutenberg/pull/75316)).
Expand Down
23 changes: 7 additions & 16 deletions packages/components/src/avatar-group/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import clsx from 'clsx';
/**
* WordPress dependencies
*/
import { Children, cloneElement, isValidElement } from '@wordpress/element';
import { Children } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -26,25 +26,16 @@ function AvatarGroup( {

return (
<div
role="group"
className={ clsx( 'components-avatar-group', className ) }
{ ...props }
>
{ visible.map( ( child, index ) =>
isValidElement( child )
? cloneElement( child, {
style: {
...( (
child.props as {
style?: React.CSSProperties;
}
).style ?? {} ),
zIndex: visible.length - index,
},
} )
: child
) }
{ visible }
{ overflowCount > 0 && (
<span className="components-avatar-group__overflow">
<span
className="components-avatar-group__overflow"
aria-label={ `${ overflowCount } more` }
>
{ `+${ overflowCount }` }
</span>
) }
Expand Down
28 changes: 28 additions & 0 deletions packages/components/src/avatar-group/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,34 @@ export const WithBadges: Story = {
},
};

export const MixedBadgeAndTooltip: Story = {
render: ( args ) => (
<AvatarGroup { ...args }>
<Avatar
src={ avatarUrl( 'aaa' ) }
name="Alice"
borderColor="#3858e9"
badge
/>
<Avatar src={ avatarUrl( 'bbb' ) } name="Bob" />
<Avatar
src={ avatarUrl( 'ccc' ) }
name="Charlie"
borderColor="#58e938"
badge
label="You"
/>
</AvatarGroup>
),
parameters: {
docs: {
description: {
story: 'Mixing badge avatars with tooltip-only avatars. The third avatar uses `label` to show "You" in the badge while preserving the full name in the tooltip.',
},
},
},
};

export const CustomMax: Story = {
args: {
max: 2,
Expand Down
11 changes: 9 additions & 2 deletions packages/components/src/avatar-group/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@
margin-inline-start: -$grid-unit-10;
}

// Elevate hovered avatar so the expanding badge renders on top.
// Stack earlier avatars on top of later ones so the overlap
// is visually correct, and elevate on hover for badge expansion.
> .components-avatar {
position: relative;

@for $i from 1 through 10 {
&:nth-child(#{$i}) {
z-index: #{11 - $i};
}
}

&:hover {
z-index: 100;
}
Expand All @@ -24,6 +31,6 @@
margin-inline-start: $grid-unit-05;
font-size: $font-size-small;
line-height: $font-line-height-small;
color: $gray-700;
color: $gray-900;
white-space: nowrap;
}
16 changes: 15 additions & 1 deletion packages/components/src/avatar-group/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ describe( 'AvatarGroup', () => {
expect(
screen.queryByRole( 'img', { name: 'Charlie' } )
).not.toBeInTheDocument();
expect( screen.getByText( '+2' ) ).toBeInTheDocument();
const overflow = screen.getByText( '+2' );
expect( overflow ).toBeInTheDocument();
expect( overflow ).toHaveAttribute( 'aria-label', '2 more' );
} );

it( 'should default max to 3', () => {
Expand Down Expand Up @@ -87,4 +89,16 @@ describe( 'AvatarGroup', () => {
expect( group ).toHaveClass( 'components-avatar-group' );
expect( group ).toHaveClass( 'custom' );
} );

it( 'should have group role and support aria-label', () => {
render(
<AvatarGroup aria-label="Collaborators">
<Avatar name="A" />
</AvatarGroup>
);
const group = screen.getByRole( 'group', {
name: 'Collaborators',
} );
expect( group ).toBeInTheDocument();
} );
} );
2 changes: 1 addition & 1 deletion packages/components/src/avatar/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function Avatar( {
</div>
);

if ( ! showBadge && name ) {
if ( name && ( ! showBadge || label ) ) {
return <Tooltip text={ name }>{ avatar }</Tooltip>;
}

Expand Down
10 changes: 0 additions & 10 deletions packages/components/src/avatar/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,6 @@
background-color: var(--components-avatar-border-color);
}

.components-avatar.is-active.has-src > .components-avatar__image {
&::before {
opacity: 0.2;
}
}

.components-avatar.is-active:not(.has-src) > .components-avatar__image {
color: rgba($white, 0.2);
}

.components-avatar__status-indicator {
position: absolute;
inset: 0;
Expand Down
8 changes: 8 additions & 0 deletions packages/components/src/avatar/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ describe( 'Avatar', () => {
const avatar = screen.getByRole( 'img', { name: 'Jane Doe' } );
expect( avatar ).toBeInTheDocument();
} );

it( 'should wrap in tooltip when label differs from name', () => {
render( <Avatar name="Jane Doe" label="You" badge /> );
const avatar = screen.getByRole( 'img', { name: 'Jane Doe' } );
// The Tooltip's Ariakit.TooltipAnchor makes the element
// focusable so the tooltip can be triggered via keyboard.
expect( avatar ).toHaveAttribute( 'tabindex', '0' );
} );
} );

describe( 'status', () => {
Expand Down
12 changes: 5 additions & 7 deletions packages/components/src/avatar/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type AvatarProps = {
label?: string;
Comment on lines +10 to +20

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm wondering about alignment between these props and how we think of the label prop in IconButton, where we intentionally try to discourage mismatch between the accessible name and the tooltip content, though I think the use-case of something like "You" is interesting to consider, and feels similar in purpose to the sort of suffixing that was implemented in IconButton's shortcut support (e.g. maybe still deriving tooltip from name, but allowing the option for additional suffixing detail).

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.

Yeah, I'm happy to name things whatever y'all think is best. The intent was just to have one prop that provides the accessible name that's used for the tooltip and generating any fallback initials. This would be the "main" name or label or whatever. In addition to that I wanted to provide a secondary label/name/whatever to use contextually if we need to call a collaborator/user/entity something different. I can see this being useful when agents or other connected agentic things start appearing as collaborators. Instances where an entities exact name is less important than the role they're playing in the given situation. Happy to align that with how the button or other components handle that difference in meaning.

/**
* Whether to show the hover-expand badge that reveals the user's
* name (or `label`) on hover.
* name (or `label`) on hover. Requires `name` to be set.
*
* @default false
*/
Expand All @@ -38,15 +38,13 @@ export type AvatarProps = {
* CSS color value for an accent border ring around the avatar.
*
* When not provided, no border is rendered and the hover badge
* uses the admin theme color as its background.
* and avatar status uses the admin theme color as its background.
*/
borderColor?: string;
Comment on lines +42 to +48

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Interesting to think how we might want to think about token usage here rather than hard-coded values. In other places we've expressly moved away from supporting hard-coded values in favor of tokens (example). Although in how this is used in real-time collaboration with a bunch of different colors for each collaborator, I'm not sure we can really tokenize this as-is, without either baking in tokens for complementary colors (like what you're exploring in #74875), or wrapping every Avatar with ThemeProvider (excessive), or allowing hard-coded values as you're considering here.

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.

Yeah, ideally this is locked to tokens, but realistically I think it needs to support arbitrary colors. Especially since different uses might need to generate their own colors. If we could provide a method for doing that from the theme package we might be able to tighten that up. But until then it probably needs to accept hex values.

/**
* The current status of the user. When set, the avatar image is
* dimmed to indicate a non-default state.
*
* - `'active'`: 20% image opacity (e.g. typing, editing).
* - `'idle'`: 30% image opacity (e.g. session timeout).
* A status string applied to the avatar. When set, the image is
* dimmed to indicate a non-default state. A corresponding
* `is-{status}` class is added for custom styling.
*/
status?: string;
/**
Expand Down
Loading