Skip to content

Commit e434bf0

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

File tree

3 files changed

+270
-0
lines changed

3 files changed

+270
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
@use "@pythnetwork/component-library/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: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { TruncateAddress as TruncateAddressComponent } from "./index"; // Only import the main component
3+
4+
const meta = {
5+
component: TruncateAddressComponent,
6+
argTypes: {
7+
text: {
8+
control: "text",
9+
description: "The address string to truncate.",
10+
table: {
11+
category: "Data",
12+
},
13+
},
14+
fixed: {
15+
control: "boolean",
16+
description:
17+
"Determines if the truncation uses a fixed number of characters (true) or is dynamic (false).",
18+
table: {
19+
category: "Behavior",
20+
defaultValue: { summary: "false" },
21+
},
22+
},
23+
minCharsStart: {
24+
control: "number",
25+
description:
26+
"Minimum characters to show at the start. Default for dynamic is 0, for fixed is 6.",
27+
table: {
28+
category: "Behavior",
29+
},
30+
},
31+
minCharsEnd: {
32+
control: "number",
33+
description:
34+
"Minimum characters to show at the end. Default for dynamic is 0, for fixed is 6.",
35+
table: {
36+
category: "Behavior",
37+
},
38+
},
39+
},
40+
parameters: {
41+
docs: {
42+
description: {
43+
component:
44+
"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.",
45+
},
46+
},
47+
},
48+
tags: ["autodocs"],
49+
} satisfies Meta<typeof TruncateAddressComponent>;
50+
51+
export default meta;
52+
53+
type Story = StoryObj<typeof meta>;
54+
55+
const defaultAddress = "0x1234567890abcdef1234567890abcdef12345678";
56+
const longEnsName = "verylongethereumdomainnamethatshouldbetruncated.eth";
57+
const shortAddress = "0xABC";
58+
59+
export const DynamicDefault: Story = {
60+
args: {
61+
text: defaultAddress,
62+
fixed: false,
63+
},
64+
name: "Dynamic (Default Behavior)",
65+
};
66+
67+
export const FixedDefault: Story = {
68+
args: {
69+
text: defaultAddress,
70+
fixed: true,
71+
},
72+
name: "Fixed (Default Behavior)",
73+
};
74+
75+
export const DynamicCustomChars: Story = {
76+
args: {
77+
text: defaultAddress,
78+
fixed: false,
79+
minCharsStart: 4,
80+
minCharsEnd: 4,
81+
},
82+
name: "Dynamic (Custom minChars: 4 start, 4 end)",
83+
};
84+
85+
export const FixedCustomChars: Story = {
86+
args: {
87+
text: defaultAddress,
88+
fixed: true,
89+
minCharsStart: 8,
90+
minCharsEnd: 8,
91+
},
92+
name: "Fixed (Custom minChars: 8 start, 8 end)",
93+
};
94+
95+
export const DynamicEnsName: Story = {
96+
args: {
97+
text: longEnsName,
98+
fixed: false,
99+
minCharsStart: 10, // Show more for ENS names if dynamic
100+
minCharsEnd: 3,
101+
},
102+
name: "Dynamic (ENS Name)",
103+
};
104+
105+
export const FixedEnsName: Story = {
106+
args: {
107+
text: longEnsName,
108+
fixed: true,
109+
minCharsStart: 10,
110+
minCharsEnd: 8, // .eth + 5 chars
111+
},
112+
name: "Fixed (ENS Name)",
113+
};
114+
115+
export const DynamicShortAddress: Story = {
116+
args: {
117+
text: shortAddress,
118+
fixed: false,
119+
// minCharsStart/End will effectively show the whole string if it's shorter
120+
},
121+
name: "Dynamic (Short Address)",
122+
};
123+
124+
export const FixedShortAddress: Story = {
125+
args: {
126+
text: shortAddress,
127+
fixed: true,
128+
minCharsStart: 2, // Will show 0x...BC if text is 0xABC
129+
minCharsEnd: 2,
130+
},
131+
name: "Fixed (Short Address)",
132+
};
133+
134+
export const FixedVeryShortAddressShowsAll: Story = {
135+
args: {
136+
text: "0x1",
137+
fixed: true,
138+
minCharsStart: 6,
139+
minCharsEnd: 6,
140+
},
141+
name: "Fixed (Very Short Address, Shows All)",
142+
parameters: {
143+
docs: {
144+
description:
145+
"If the text is shorter than or equal to minCharsStart + minCharsEnd, the original text is shown.",
146+
},
147+
},
148+
};
149+
150+
export const DynamicZeroMinChars: Story = {
151+
args: {
152+
text: defaultAddress,
153+
fixed: false,
154+
minCharsStart: 0,
155+
minCharsEnd: 0,
156+
},
157+
name: "Dynamic (Zero minChars)",
158+
};
159+
160+
export const FixedZeroMinChars: Story = {
161+
args: {
162+
text: defaultAddress,
163+
fixed: true,
164+
minCharsStart: 0,
165+
minCharsEnd: 0,
166+
},
167+
name: "Fixed (Zero minChars, shows only ellipsis)",
168+
};
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)