mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
Compress UserDisplayName Into Two Layouts (#425)
Export-Source-Commit: 6a8da4b292a0a8f5bb2fea6a01ebe72fdafba1e0
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user