Skip to content

Commit 2102944

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

File tree

3 files changed

+272
-0
lines changed

3 files changed

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