← All proposals

Identity Block

Start from the PDS v2 Identity Block and use the Agent Avatar in place of the human Avatar, as Avatar Label does.

Extends a PDS componentOpen in Storybook

proposals/IdentityBlock/IdentityBlock.tsx160 lines · View on GitHub
import {
	ComponentPropsWithoutRef,
	useCallback,
	useEffect,
	useRef,
	useState,
} from 'react';

import { Tooltip } from '@pantheon-systems/pds-toolkit-react';

import { AgentAvatar, AgentAvatarProps } from '../AgentAvatar/AgentAvatar';
import { mergeClasses } from '../Button/utils';

import './identity-block.css';

/**
 * Prop types for IdentityBlock
 */
export interface IdentityBlockProps extends ComponentPropsWithoutRef<'div'> {
	/**
	 * Props to pass to the AgentAvatar component. `label` (the screen-reader
	 * text that says it is an agent) is required. `size` is set automatically
	 * (`m`, or `s` when `isSingleLine`) and cannot be overridden here.
	 */
	avatarProps?: Omit<AgentAvatarProps, 'ariaLabel' | 'linkContent' | 'size'>;
	/**
	 * Additional class names
	 */
	className?: string;
	/**
	 * When true, only the avatar is shown (text is hidden).
	 */
	isSidebarCollapsed?: boolean;
	/**
	 * When true, displays a smaller avatar with only the primary text.
	 * Secondary text is not rendered even if provided.
	 */
	isSingleLine?: boolean;
	/**
	 * First line of text (e.g., organization name).
	 */
	primaryText?: string;
	/**
	 * Second line of text (e.g., user name).
	 */
	secondaryText?: string;
}

/**
 * IdentityBlock UI component
 */
export const IdentityBlock = ({
	avatarProps,
	className,
	isSidebarCollapsed,
	isSingleLine,
	primaryText,
	secondaryText,
	...props
}: IdentityBlockProps) => {
	const baseClass = 'pds-identity-block';
	const collapsedClass = isSidebarCollapsed ? `${baseClass}--collapsed` : null;
	const singleLineClass = isSingleLine ? `${baseClass}--single-line` : null;

	const resolvedAvatarProps = avatarProps
		? {
				...avatarProps,
				size: isSingleLine ? ('s' as const) : ('m' as const),
			}
		: undefined;
	const showSecondaryText = secondaryText && !isSingleLine;

	const primaryRef = useRef<HTMLSpanElement>(null);
	const secondaryRef = useRef<HTMLSpanElement>(null);
	const [isPrimaryTruncated, setIsPrimaryTruncated] = useState(false);
	const [isSecondaryTruncated, setIsSecondaryTruncated] = useState(false);

	const checkTruncation = useCallback(() => {
		if (primaryRef.current) {
			setIsPrimaryTruncated(
				primaryRef.current.scrollWidth > primaryRef.current.clientWidth,
			);
		}
		if (secondaryRef.current) {
			setIsSecondaryTruncated(
				secondaryRef.current.scrollWidth > secondaryRef.current.clientWidth,
			);
		}
	}, []);

	useEffect(() => {
		window.addEventListener('resize', checkTruncation);
		return () => window.removeEventListener('resize', checkTruncation);
	}, [checkTruncation]);

	useEffect(() => {
		if (isSidebarCollapsed) {
			return;
		}
		const id = setTimeout(checkTruncation, 250);
		return () => clearTimeout(id);
	}, [checkTruncation, isSidebarCollapsed, primaryText, secondaryText]);

	const collapsedTooltipContent = [
		primaryText,
		showSecondaryText ? secondaryText : null,
	]
		.filter(Boolean)
		.join(' — ');

	const avatarElement = resolvedAvatarProps ? (
		<div className={`${baseClass}__avatar`}>
			<AgentAvatar {...resolvedAvatarProps} />
		</div>
	) : null;

	return (
		<div
			className={mergeClasses([
				baseClass,
				collapsedClass,
				singleLineClass,
				className ?? '',
			])}
			{...props}
		>
			{avatarElement &&
				(isSidebarCollapsed && collapsedTooltipContent ? (
					<Tooltip
						content={collapsedTooltipContent}
						customTrigger={avatarElement}
						preferredPlacement='right'
					/>
				) : (
					avatarElement
				))}
			{(primaryText || showSecondaryText) && (
				<div className={`${baseClass}__text`}>
					<span
						ref={primaryRef}
						className={`${baseClass}__primary-text`}
						title={isPrimaryTruncated ? primaryText : undefined}
					>
						{primaryText}
					</span>
					{showSecondaryText && (
						<span
							ref={secondaryRef}
							className={`${baseClass}__secondary-text`}
							title={isSecondaryTruncated ? secondaryText : undefined}
						>
							{secondaryText}
						</span>
					)}
				</div>
			)}
		</div>
	);
};