Skip to content

Add CopyAddress component to the component library #2750

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@use "../theme";

.srOnly {
@include theme.sr-only;
}

.truncateAddressDynamic {
// Defaults may get overridden by style on span
--min-chars-start-ch: 0ch;
--min-chars-end-ch: 0ch;

&::before,
&::after {
display: inline-block;
overflow: hidden;
white-space: pre;
max-width: 50%;
}

&::before {
content: attr(data-text-start);
text-overflow: ellipsis;
min-width: var(--min-chars-start-ch);
}

&::after {
content: attr(data-text-end);
text-overflow: clip;
direction: rtl;
min-width: var(--min-chars-end-ch);
}
}
169 changes: 169 additions & 0 deletions packages/component-library/src/TruncateAddress/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import type { Meta, StoryObj } from "@storybook/react";

import { TruncateAddress as TruncateAddressComponent } from "./index"; // Only import the main component

const meta = {
component: TruncateAddressComponent,
argTypes: {
text: {
control: "text",
description: "The address string to truncate.",
table: {
category: "Data",
},
},
fixed: {
control: "boolean",
description:
"Determines if the truncation uses a fixed number of characters (true) or is dynamic (false).",
table: {
category: "Behavior",
defaultValue: { summary: "false" },
},
},
minCharsStart: {
control: "number",
description:
"Minimum characters to show at the start. Default for dynamic is 0, for fixed is 6.",
table: {
category: "Behavior",
},
},
minCharsEnd: {
control: "number",
description:
"Minimum characters to show at the end. Default for dynamic is 0, for fixed is 6.",
table: {
category: "Behavior",
},
},
},
parameters: {
docs: {
description: {
component:
"A component to truncate long strings, typically addresses, in a user-friendly way. It supports both dynamic (CSS-based, responsive) and fixed (JS-based, specific character counts) truncation.",
},
},
},
tags: ["autodocs"],
} satisfies Meta<typeof TruncateAddressComponent>;

export default meta;

type Story = StoryObj<typeof meta>;

const defaultAddress = "0x1234567890abcdef1234567890abcdef12345678";
const longEnsName = "verylongethereumdomainnamethatshouldbetruncated.eth";
const shortAddress = "0xABC";

export const DynamicDefault: Story = {
args: {
text: defaultAddress,
fixed: false,
},
name: "Dynamic (Default Behavior)",
};

export const FixedDefault: Story = {
args: {
text: defaultAddress,
fixed: true,
},
name: "Fixed (Default Behavior)",
};

export const DynamicCustomChars: Story = {
args: {
text: defaultAddress,
fixed: false,
minCharsStart: 4,
minCharsEnd: 4,
},
name: "Dynamic (Custom minChars: 4 start, 4 end)",
};

export const FixedCustomChars: Story = {
args: {
text: defaultAddress,
fixed: true,
minCharsStart: 8,
minCharsEnd: 8,
},
name: "Fixed (Custom minChars: 8 start, 8 end)",
};

export const DynamicEnsName: Story = {
args: {
text: longEnsName,
fixed: false,
minCharsStart: 10, // Show more for ENS names if dynamic
minCharsEnd: 3,
},
name: "Dynamic (ENS Name)",
};

export const FixedEnsName: Story = {
args: {
text: longEnsName,
fixed: true,
minCharsStart: 10,
minCharsEnd: 8, // .eth + 5 chars
},
name: "Fixed (ENS Name)",
};

export const DynamicShortAddress: Story = {
args: {
text: shortAddress,
fixed: false,
// minCharsStart/End will effectively show the whole string if it's shorter
},
name: "Dynamic (Short Address)",
};

export const FixedShortAddress: Story = {
args: {
text: shortAddress,
fixed: true,
minCharsStart: 2, // Will show 0x...BC if text is 0xABC
minCharsEnd: 2,
},
name: "Fixed (Short Address)",
};

export const FixedVeryShortAddressShowsAll: Story = {
args: {
text: "0x1",
fixed: true,
minCharsStart: 6,
minCharsEnd: 6,
},
name: "Fixed (Very Short Address, Shows All)",
parameters: {
docs: {
description:
"If the text is shorter than or equal to minCharsStart + minCharsEnd, the original text is shown.",
},
},
};

export const DynamicZeroMinChars: Story = {
args: {
text: defaultAddress,
fixed: false,
minCharsStart: 0,
minCharsEnd: 0,
},
name: "Dynamic (Zero minChars)",
};

export const FixedZeroMinChars: Story = {
args: {
text: defaultAddress,
fixed: true,
minCharsStart: 0,
minCharsEnd: 0,
},
name: "Fixed (Zero minChars, shows only ellipsis)",
};
71 changes: 71 additions & 0 deletions packages/component-library/src/TruncateAddress/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { useMemo } from "react";

import styles from "./index.module.scss";

export type Props = {
text: string;
fixed?: boolean;
minCharsStart?: number | undefined;
minCharsEnd?: number | undefined;
};

export const TruncateAddress = ({ fixed = false, ...rest }: Props) => {
return fixed ? (
<TruncateAddressFixed {...rest} />
) : (
<TruncateAddressDynamic {...rest} />
);
};

const TruncateAddressDynamic = ({
text,
minCharsStart = 0,
minCharsEnd = 0,
}: Props) => {
// We're setting a minimum width using CSS 'ch' units, which are relative to the
// width of the '0' character. This provides a good approximation for showing
// a certain number of characters. However, since character widths vary
// (e.g., 'i' is narrower than 'W'), the exact count of visible characters
// might differ slightly from the specified 'ch' value.
const style = {
"--min-chars-start-ch": `${minCharsStart.toString()}ch`,
"--min-chars-end-ch": `${minCharsEnd.toString()}ch`,
} as React.CSSProperties;

return (
<>
<span
className={styles.truncateAddressDynamic}
style={style}
data-text-start={text.slice(0, Math.floor(text.length / 2))}
data-text-end={text.slice(Math.floor(text.length / 2) * -1)}
aria-hidden="true"
/>
<span className={styles.srOnly}>{text}</span>
</>
);
};

const truncate = (text: string, minCharsStart: number, minCharsEnd: number) => {
const start = minCharsStart <= 0 ? "" : text.slice(0, minCharsStart);
const end = minCharsEnd <= 0 ? "" : text.slice(minCharsStart * -1);

return `${start}…${end}`;
}

const TruncateAddressFixed = ({
text,
minCharsStart = 6,
minCharsEnd = 6,
}: Props) => {
const truncatedValue = useMemo(() => truncate(text, minCharsStart, minCharsEnd), [text, minCharsStart, minCharsEnd])

return truncatedValue === text ? (
<span>{text}</span>
) : (
<>
<span aria-hidden="true">{truncatedValue}</span>
<span className={styles.srOnly}>{text}</span>
</>
);
};
Loading