Skip to content

Commit bdd63f8

Browse files
committed
feat: add TruncateAddress component to component library
1 parent 289c304 commit bdd63f8

File tree

3 files changed

+271
-0
lines changed

3 files changed

+271
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
@use "../theme";
2+
3+
.srOnly {
4+
@include theme.sr-only;
5+
}
6+
7+
.truncateAddressDynamic {
8+
// Defaults may get overridden by style on span
9+
--min-chars-start-ch: 0ch;
10+
--min-chars-end-ch: 0ch;
11+
12+
&::before,
13+
&::after {
14+
display: inline-block;
15+
overflow: hidden;
16+
white-space: pre;
17+
max-width: 50%;
18+
}
19+
20+
&::before {
21+
content: attr(data-text-start);
22+
text-overflow: ellipsis;
23+
min-width: var(--min-chars-start-ch);
24+
}
25+
26+
&::after {
27+
content: attr(data-text-end);
28+
text-overflow: clip;
29+
direction: rtl;
30+
min-width: var(--min-chars-end-ch);
31+
}
32+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
3+
import { TruncateAddress as TruncateAddressComponent } from "./index"; // Only import the main component
4+
5+
const meta = {
6+
component: TruncateAddressComponent,
7+
argTypes: {
8+
text: {
9+
control: "text",
10+
description: "The address string to truncate.",
11+
table: {
12+
category: "Data",
13+
},
14+
},
15+
fixed: {
16+
control: "boolean",
17+
description:
18+
"Determines if the truncation uses a fixed number of characters (true) or is dynamic (false).",
19+
table: {
20+
category: "Behavior",
21+
defaultValue: { summary: "false" },
22+
},
23+
},
24+
minCharsStart: {
25+
control: "number",
26+
description:
27+
"Minimum characters to show at the start. Default for dynamic is 0, for fixed is 6.",
28+
table: {
29+
category: "Behavior",
30+
},
31+
},
32+
minCharsEnd: {
33+
control: "number",
34+
description:
35+
"Minimum characters to show at the end. Default for dynamic is 0, for fixed is 6.",
36+
table: {
37+
category: "Behavior",
38+
},
39+
},
40+
},
41+
parameters: {
42+
docs: {
43+
description: {
44+
component:
45+
"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.",
46+
},
47+
},
48+
},
49+
tags: ["autodocs"],
50+
} satisfies Meta<typeof TruncateAddressComponent>;
51+
52+
export default meta;
53+
54+
type Story = StoryObj<typeof meta>;
55+
56+
const defaultAddress = "0x1234567890abcdef1234567890abcdef12345678";
57+
const longEnsName = "verylongethereumdomainnamethatshouldbetruncated.eth";
58+
const shortAddress = "0xABC";
59+
60+
export const DynamicDefault: Story = {
61+
args: {
62+
text: defaultAddress,
63+
fixed: false,
64+
},
65+
name: "Dynamic (Default Behavior)",
66+
};
67+
68+
export const FixedDefault: Story = {
69+
args: {
70+
text: defaultAddress,
71+
fixed: true,
72+
},
73+
name: "Fixed (Default Behavior)",
74+
};
75+
76+
export const DynamicCustomChars: Story = {
77+
args: {
78+
text: defaultAddress,
79+
fixed: false,
80+
minCharsStart: 4,
81+
minCharsEnd: 4,
82+
},
83+
name: "Dynamic (Custom minChars: 4 start, 4 end)",
84+
};
85+
86+
export const FixedCustomChars: Story = {
87+
args: {
88+
text: defaultAddress,
89+
fixed: true,
90+
minCharsStart: 8,
91+
minCharsEnd: 8,
92+
},
93+
name: "Fixed (Custom minChars: 8 start, 8 end)",
94+
};
95+
96+
export const DynamicEnsName: Story = {
97+
args: {
98+
text: longEnsName,
99+
fixed: false,
100+
minCharsStart: 10, // Show more for ENS names if dynamic
101+
minCharsEnd: 3,
102+
},
103+
name: "Dynamic (ENS Name)",
104+
};
105+
106+
export const FixedEnsName: Story = {
107+
args: {
108+
text: longEnsName,
109+
fixed: true,
110+
minCharsStart: 10,
111+
minCharsEnd: 8, // .eth + 5 chars
112+
},
113+
name: "Fixed (ENS Name)",
114+
};
115+
116+
export const DynamicShortAddress: Story = {
117+
args: {
118+
text: shortAddress,
119+
fixed: false,
120+
// minCharsStart/End will effectively show the whole string if it's shorter
121+
},
122+
name: "Dynamic (Short Address)",
123+
};
124+
125+
export const FixedShortAddress: Story = {
126+
args: {
127+
text: shortAddress,
128+
fixed: true,
129+
minCharsStart: 2, // Will show 0x...BC if text is 0xABC
130+
minCharsEnd: 2,
131+
},
132+
name: "Fixed (Short Address)",
133+
};
134+
135+
export const FixedVeryShortAddressShowsAll: Story = {
136+
args: {
137+
text: "0x1",
138+
fixed: true,
139+
minCharsStart: 6,
140+
minCharsEnd: 6,
141+
},
142+
name: "Fixed (Very Short Address, Shows All)",
143+
parameters: {
144+
docs: {
145+
description:
146+
"If the text is shorter than or equal to minCharsStart + minCharsEnd, the original text is shown.",
147+
},
148+
},
149+
};
150+
151+
export const DynamicZeroMinChars: Story = {
152+
args: {
153+
text: defaultAddress,
154+
fixed: false,
155+
minCharsStart: 0,
156+
minCharsEnd: 0,
157+
},
158+
name: "Dynamic (Zero minChars)",
159+
};
160+
161+
export const FixedZeroMinChars: Story = {
162+
args: {
163+
text: defaultAddress,
164+
fixed: true,
165+
minCharsStart: 0,
166+
minCharsEnd: 0,
167+
},
168+
name: "Fixed (Zero minChars, shows only ellipsis)",
169+
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React, { useMemo } from "react";
2+
3+
import styles from "./index.module.scss";
4+
5+
export type Props = {
6+
text: string;
7+
fixed?: boolean;
8+
minCharsStart?: number | undefined;
9+
minCharsEnd?: number | undefined;
10+
};
11+
12+
export const TruncateAddress = ({ fixed = false, ...rest }: Props) => {
13+
return fixed ? (
14+
<TruncateAddressFixed {...rest} />
15+
) : (
16+
<TruncateAddressDynamic {...rest} />
17+
);
18+
};
19+
20+
const TruncateAddressDynamic = ({
21+
text,
22+
minCharsStart = 0,
23+
minCharsEnd = 0,
24+
}: Props) => {
25+
// We're setting a minimum width using CSS 'ch' units, which are relative to the
26+
// width of the '0' character. This provides a good approximation for showing
27+
// a certain number of characters. However, since character widths vary
28+
// (e.g., 'i' is narrower than 'W'), the exact count of visible characters
29+
// might differ slightly from the specified 'ch' value.
30+
const style = {
31+
"--min-chars-start-ch": `${minCharsStart.toString()}ch`,
32+
"--min-chars-end-ch": `${minCharsEnd.toString()}ch`,
33+
} as React.CSSProperties;
34+
35+
return (
36+
<>
37+
<span
38+
className={styles.truncateAddressDynamic}
39+
style={style}
40+
data-text-start={text.slice(0, Math.floor(text.length / 2))}
41+
data-text-end={text.slice(Math.floor(text.length / 2) * -1)}
42+
aria-hidden="true"
43+
/>
44+
<span className={styles.srOnly}>{text}</span>
45+
</>
46+
);
47+
};
48+
49+
const TruncateAddressFixed = ({
50+
text,
51+
minCharsStart = 6,
52+
minCharsEnd = 6,
53+
}: Props) => {
54+
const truncatedValue = useMemo(
55+
() =>
56+
text.length <= minCharsStart + minCharsEnd
57+
? text
58+
: `${text.slice(0, minCharsStart)}${text.slice(minCharsEnd * -1)}`,
59+
[text, minCharsStart, minCharsEnd],
60+
);
61+
62+
return truncatedValue === text ? (
63+
<span>{text}</span>
64+
) : (
65+
<>
66+
<span aria-hidden="true">{truncatedValue}</span>
67+
<span className={styles.srOnly}>{text}</span>
68+
</>
69+
);
70+
};

0 commit comments

Comments
 (0)