import {
cloneElement,
ComponentPropsWithoutRef,
isValidElement,
ReactNode,
} from 'react';
import { AgentAvatar, AgentAvatarProps } from '../AgentAvatar/AgentAvatar';
import './avatar-label.css';
// Stand-in for the v2 `mergeClasses` util, which the published package does
// not export. The port uses the real one.
const mergeClasses = (classes: Array<string | null | undefined>) =>
classes.filter(Boolean).join(' ');
/**
* Prop types for AvatarLabel
*/
export interface AvatarLabelProps extends ComponentPropsWithoutRef<'div'> {
/**
* Aria label for the whole component when used as a link via
* `linkContent`. Only needed if the visible `label` text isn't already a
* sufficient accessible name.
*/
ariaLabel?: string;
/**
* Optional text after the label that says who the agent is acting for (for
* example "on behalf of alex@example.com"). Pass the whole phrase,
* since the wording and word order change by language. Shown in the
* secondary text color and truncates before the label does.
*/
attribution?: ReactNode;
/**
* Props to pass to the AgentAvatar component. `label` (the screen-reader text that says it is an agent) is required. `size` is set automatically
* based on AvatarLabel's own `size` prop and cannot be overridden here.
*/
avatarProps: Omit<AgentAvatarProps, 'ariaLabel' | 'linkContent' | 'size'>;
/**
* Additional class names
*/
className?: string;
/**
* Show the avatar? Set to `false` to show only the name (and attribution),
* for example when the avatar is already shown on a neighboring item. The
* avatar's `label` stays in the page as screen-reader-only text, so it is
* still announced as an agent.
*/
hasAvatar?: boolean;
/**
* Text label displayed next to the avatar.
*/
label: ReactNode;
/**
* Optionally makes the whole component a link. Pass a fully-formed,
* router-agnostic link element (e.g. an anchor or your router's link
* component); the avatar and label render as its children, so clicking
* anywhere on either navigates.
*/
linkContent?: ReactNode;
/**
* Controls both the avatar size and the label's text size.
*/
size?: 'xs' | 's' | 'm';
}
/**
* AvatarLabel UI component
*/
export const AvatarLabel = ({
ariaLabel,
attribution,
avatarProps,
className,
hasAvatar = true,
label,
linkContent,
size = 's',
...props
}: AvatarLabelProps) => {
// Set up classes.
const baseClass = 'pds-avatar-label';
const sizeClass = `${baseClass}--${size}`;
// The avatar plus label — rendered directly, or as the children of
// `linkContent` when a link is provided.
const content = (
<>
{hasAvatar ? (
<AgentAvatar {...avatarProps} size={size} />
) : (
<span className='visually-hidden'>{avatarProps.label}</span>
)}
<span className={`${baseClass}__label`}>{label}</span>
{attribution && (
<>
{' '}
<span
className={`${baseClass}__attribution`}
title={typeof attribution === 'string' ? attribution : undefined}
>
{attribution}
</span>
</>
)}
</>
);
// When a link element is provided, render the content as its children so
// the whole component becomes a link.
const linkMarkup =
linkContent && isValidElement(linkContent)
? cloneElement(
linkContent,
Object.assign({}, linkContent.props, {
className: `${baseClass}__link`,
'aria-label': ariaLabel,
}),
content,
)
: null;
// Render the output
return (
<div
className={mergeClasses([
baseClass,
sizeClass,
attribution ? `${baseClass}--has-attribution` : null,
className ?? '',
])}
{...props}
>
{linkContent ? linkMarkup : content}
</div>
);
};
Avatar Label
The PDS v2 AvatarLabel (src/components/identity/AvatarLabel, branch v2.x) with every Avatar replaced by the Agent Avatar proposal.
Net-new componentOpen in Storybook