Compress UserDisplayName Into Two Layouts (#425)

Export-Source-Commit: 6a8da4b292a0a8f5bb2fea6a01ebe72fdafba1e0
This commit is contained in:
Lion Chen
2026-08-25 11:35:10 -05:00
committed by Gus Rivera
parent 14128e5aac
commit 1a758ff0f6
3 changed files with 57 additions and 188 deletions
@@ -31,12 +31,12 @@ const meta = {
username: '[email protected]',
primaryText: 'Alice Jones',
secondaryText: 'Engineering',
layout: 'tooltip',
layout: 'inline',
},
argTypes: {
layout: {
control: { type: 'radio' },
options: ['inline', 'stacked', 'tooltip'],
options: ['inline', 'stacked'],
},
},
decorators: [
@@ -68,7 +68,7 @@ export const LayoutVariants: Story = {
>
<LayoutExample
value="inline"
description="Username and secondary render inside the parenthetical group."
description="Username renders inside the parenthetical group."
>
<UserDisplayName
username="[email protected]"
@@ -88,17 +88,6 @@ export const LayoutVariants: Story = {
layout="stacked"
/>
</LayoutExample>
<LayoutExample
value="tooltip"
description="Primary stays visible, and username appears in a tooltip."
>
<UserDisplayName
username="[email protected]"
primaryText="Alice Jones"
secondaryText="Engineering"
layout="tooltip"
/>
</LayoutExample>
</Flex>
),
};
@@ -131,16 +120,6 @@ export const LayoutVariantsWithoutSecondary: Story = {
layout="stacked"
/>
</LayoutExample>
<LayoutExample
value="tooltip"
description="Primary stays visible, and username appears in a tooltip."
>
<UserDisplayName
username="[email protected]"
primaryText="Alice Jones"
layout="tooltip"
/>
</LayoutExample>
</Flex>
),
};
@@ -155,7 +134,7 @@ export const LayoutVariantsWithoutPrimary: Story = {
>
<LayoutExample
value="inline"
description="Username and secondary stay on one line."
description="Username is the only visible value."
>
<UserDisplayName
username="[email protected]"
@@ -173,16 +152,6 @@ export const LayoutVariantsWithoutPrimary: Story = {
layout="stacked"
/>
</LayoutExample>
<LayoutExample
value="tooltip"
description="Username stays visible, with secondary below it."
>
<UserDisplayName
username="[email protected]"
secondaryText="Engineering"
layout="tooltip"
/>
</LayoutExample>
</Flex>
),
};
@@ -201,9 +170,6 @@ export const LayoutVariantsWithoutPrimaryOrSecondary: Story = {
<LayoutExample value="stacked" description="Username is the only value.">
<UserDisplayName username="[email protected]" layout="stacked" />
</LayoutExample>
<LayoutExample value="tooltip" description="Username is the only value.">
<UserDisplayName username="[email protected]" layout="tooltip" />
</LayoutExample>
</Flex>
),
};
@@ -235,17 +201,6 @@ export const LongValues: Story = {
layout="stacked"
/>
</LayoutExample>
<LayoutExample
value="tooltip"
description="Primary truncates inline; full username appears on hover."
>
<UserDisplayName
username="[email protected]"
primaryText="Alice Jones-Anderson-Engineering"
secondaryText="Platform Engineering"
layout="tooltip"
/>
</LayoutExample>
</Flex>
),
};
@@ -16,63 +16,57 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { render, screen, userEvent, within } from 'design/utils/testing';
import { render, screen, within } from 'design/utils/testing';
import { UserDisplayName, type UserDisplayNameLayout } from './UserDisplayName';
describe('UserDisplayName', () => {
const username = '[email protected]';
const layouts: UserDisplayNameLayout[] = ['inline', 'stacked', 'tooltip'];
const layouts: UserDisplayNameLayout[] = ['inline', 'stacked'];
const valueScenarios: {
name: string;
primaryText?: string;
secondaryText?: string;
expectedPrimary: string;
expectedSecondary: string | null;
expectedVisibleUsernameCountByLayout: Record<UserDisplayNameLayout, number>;
expectedVisibleSecondaryCountByLayout: Record<
UserDisplayNameLayout,
number
>;
}[] = [
{
name: 'primary, secondary, and username',
primaryText: 'Alice Jones',
secondaryText: 'Engineering',
expectedPrimary: 'Alice Jones',
expectedSecondary: 'Engineering',
expectedVisibleUsernameCountByLayout: {
inline: 1,
expectedVisibleSecondaryCountByLayout: {
inline: 0,
stacked: 1,
tooltip: 0,
},
},
{
name: 'only username',
expectedPrimary: username,
expectedSecondary: null,
expectedVisibleUsernameCountByLayout: {
inline: 1,
stacked: 1,
tooltip: 1,
expectedVisibleSecondaryCountByLayout: {
inline: 0,
stacked: 0,
},
},
{
name: 'primary and username',
primaryText: 'Alice Jones',
expectedPrimary: 'Alice Jones',
expectedSecondary: null,
expectedVisibleUsernameCountByLayout: {
inline: 1,
stacked: 1,
tooltip: 0,
expectedVisibleSecondaryCountByLayout: {
inline: 0,
stacked: 0,
},
},
{
name: 'secondary and username',
secondaryText: 'Engineering',
expectedPrimary: username,
expectedSecondary: 'Engineering',
expectedVisibleUsernameCountByLayout: {
inline: 1,
expectedVisibleSecondaryCountByLayout: {
inline: 0,
stacked: 1,
tooltip: 1,
},
},
];
@@ -91,29 +85,16 @@ describe('UserDisplayName', () => {
expect(screen.getByText(scenario.expectedPrimary)).toBeInTheDocument();
expect(screen.queryAllByText('Engineering')).toHaveLength(
scenario.expectedSecondary ? 1 : 0
scenario.expectedVisibleSecondaryCountByLayout[layout]
);
const expectedVisibleUsernameCount =
scenario.expectedVisibleUsernameCountByLayout[layout];
expect(screen.queryAllByText(username)).toHaveLength(
expectedVisibleUsernameCount
);
const tooltipTriggerLabel = getTooltipAriaLabel(
scenario.expectedPrimary,
scenario.expectedSecondary,
username
);
expect(screen.queryAllByLabelText(tooltipTriggerLabel)).toHaveLength(
layout === 'tooltip' && expectedVisibleUsernameCount === 0 ? 1 : 0
);
expect(screen.queryAllByText(username)).toHaveLength(1);
});
}
}
it('formats inline supporting values with delimiters', () => {
render(
it('ignores secondary text in inline layout', () => {
const { container, rerender } = render(
<UserDisplayName
username={username}
primaryText="Alice Jones"
@@ -126,9 +107,6 @@ describe('UserDisplayName', () => {
.parentElement as HTMLElement;
const inlineUsername = within(primaryLine).getByText(username);
const inlineSupportingValues = inlineUsername.parentElement as HTMLElement;
const inlineSecondary = within(inlineSupportingValues).getByText(
'Engineering'
);
expect(primaryLine).toContainElement(inlineSupportingValues);
expect(inlineSupportingValues).toHaveStyleRule('content', "'('", {
@@ -137,9 +115,17 @@ describe('UserDisplayName', () => {
expect(inlineSupportingValues).toHaveStyleRule('content', "')'", {
modifier: '::after',
});
expect(inlineSecondary).toHaveStyleRule('content', "'•'", {
modifier: '::before',
});
expect(screen.queryByText('Engineering')).not.toBeInTheDocument();
const withSecondaryText = container.innerHTML;
rerender(
<UserDisplayName
username={username}
primaryText="Alice Jones"
layout="inline"
/>
);
expect(container.innerHTML).toBe(withSecondaryText);
});
it('renders stacked supporting values together below the primary line', () => {
@@ -186,49 +172,4 @@ describe('UserDisplayName', () => {
).not.toBeInTheDocument();
expect(screen.getByText('Engineering')).toBeInTheDocument();
});
it('defaults to tooltip layout', async () => {
const user = userEvent.setup();
render(
<UserDisplayName
username={username}
primaryText="Alice Jones"
secondaryText="Engineering"
/>
);
expect(screen.queryByText(username)).not.toBeInTheDocument();
await user.hover(screen.getByText('Alice Jones'));
const tooltip = await screen.findByRole('tooltip');
expect(within(tooltip).getByText(username)).toBeInTheDocument();
});
it('anchors tooltip layout to the primary text', () => {
render(
<UserDisplayName
username={username}
primaryText="Alice Jones"
secondaryText="Engineering"
layout="tooltip"
/>
);
const tooltipTrigger = screen.getByLabelText(
'Alice Jones, Engineering, username [email protected]'
);
expect(tooltipTrigger).toBe(screen.getByText('Alice Jones'));
});
function getTooltipAriaLabel(
primary: string,
secondary: string | null | undefined,
username: string
) {
return [primary, secondary, `username ${username}`]
.filter(Boolean)
.join(', ');
}
});
@@ -20,15 +20,14 @@ import type { ComponentProps } from 'react';
import styled, { css } from 'styled-components';
import { Text } from 'design';
import { HoverTooltip } from 'design/Tooltip';
export type UserDisplayNameLayout = 'inline' | 'stacked' | 'tooltip';
export type UserDisplayNameLayout = 'inline' | 'stacked';
export function UserDisplayName({
username,
primaryText,
secondaryText,
layout = 'tooltip',
layout,
className,
primaryTextProps,
usernameTextProps,
@@ -37,7 +36,7 @@ export function UserDisplayName({
username: string;
primaryText?: string;
secondaryText?: string;
layout?: UserDisplayNameLayout;
layout: UserDisplayNameLayout;
className?: string;
primaryTextProps?: ComponentProps<typeof Text>;
usernameTextProps?: ComponentProps<typeof Text>;
@@ -46,10 +45,9 @@ export function UserDisplayName({
const displayPrimary = normalizeText(primaryText);
const displaySecondary = normalizeText(secondaryText);
const primary = displayPrimary || username;
const tooltipLabel = getTooltipAriaLabel(primary, displaySecondary, username);
const primaryValue = (ariaLabel?: string) => (
<PrimaryValue {...primaryTextProps} title={primary} aria-label={ariaLabel}>
const primaryValue = () => (
<PrimaryValue {...primaryTextProps} title={primary}>
{primary}
</PrimaryValue>
);
@@ -66,35 +64,35 @@ export function UserDisplayName({
</SeparatedSecondaryValue>
);
const supportingValues = displayPrimary ? (
<>
<UsernameValue {...usernameTextProps} title={username}>
{username}
</UsernameValue>
{separatedSecondaryValue}
</>
) : (
secondaryValue
);
switch (layout) {
case 'inline':
return (
<Root className={className}>
<DisplayLine>
{primaryValue()}
{displayPrimary ? (
{displayPrimary && (
<InlineSupportingValues>
{supportingValues}
<UsernameValue {...usernameTextProps} title={username}>
{username}
</UsernameValue>
</InlineSupportingValues>
) : (
supportingValues
)}
</DisplayLine>
</Root>
);
case 'stacked':
case 'stacked': {
const supportingValues = displayPrimary ? (
<>
<UsernameValue {...usernameTextProps} title={username}>
{username}
</UsernameValue>
{separatedSecondaryValue}
</>
) : (
secondaryValue
);
return (
<Root className={className}>
<DisplayLine>{primaryValue()}</DisplayLine>
@@ -105,22 +103,7 @@ export function UserDisplayName({
)}
</Root>
);
case 'tooltip':
return (
<Root className={className}>
{displayPrimary ? (
<DisplayLine>
<HoverTooltip tipContent={username}>
{primaryValue(tooltipLabel)}
</HoverTooltip>
</DisplayLine>
) : (
<DisplayLine>{primaryValue()}</DisplayLine>
)}
{secondaryValue}
</Root>
);
}
default:
layout satisfies never;
@@ -133,16 +116,6 @@ function normalizeText(text?: string) {
return trimmedText || undefined;
}
function getTooltipAriaLabel(
primary: string,
secondary: string | undefined,
username: string
) {
return [primary, secondary, `username ${username}`]
.filter(Boolean)
.join(', ');
}
// `min-width: 0` lets a flex item shrink below its content size, which is what
// allows the child text to actually trigger ellipsis instead of forcing the
// container to grow.