Skip to content

Commit 26c258a

Browse files
refactor(Truncate): extract calculateMiddleTruncate to a utility file and clean up styles and stories
1 parent bfe60ca commit 26c258a

File tree

4 files changed

+114
-136
lines changed

4 files changed

+114
-136
lines changed

src/components/Truncate/Truncate.stories.tsx

Lines changed: 50 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -59,57 +59,13 @@ export default meta;
5959

6060
export const Truncate: StoryObj<TruncateProps> = {
6161
args: {
62-
text: "https://www.figma.com/design/T7txseZ8nSmsnjglF5vTye/node-id=7719-343&p=f&t=6Tl4gAOTDaPxfsHE-0",
63-
middle: false,
62+
middle: true,
6463
separator: "...",
6564
},
6665

6766
render: props => <TruncateStoryRender {...props} />,
6867
};
6968

70-
export const Inline: StoryObj<TruncateProps> = {
71-
args: {
72-
text: "Very long text that should be truncated to fit in line with a button",
73-
middle: true,
74-
},
75-
76-
render: props => (
77-
<div style={{display: "flex", flexDirection: "column", gap: "20px"}}>
78-
<Header title="Inline Truncate with Button (flex-start)" />
79-
<div
80-
style={{
81-
display: "flex",
82-
alignItems: "center",
83-
border: "1px solid #ccc",
84-
padding: "10px",
85-
width: "300px",
86-
resize: "horizontal",
87-
overflow: "auto",
88-
}}
89-
>
90-
<TruncateComponent {...props} style={{flexShrink: 1}} />
91-
<button style={{flexShrink: 0, marginLeft: "10px"}}>Button</button>
92-
</div>
93-
94-
<Header title="Inline Truncate with Button (always follows)" />
95-
<div
96-
style={{
97-
display: "flex",
98-
alignItems: "center",
99-
border: "1px solid #ccc",
100-
padding: "10px",
101-
width: "400px",
102-
resize: "horizontal",
103-
overflow: "auto",
104-
}}
105-
>
106-
<TruncateComponent {...props} style={{flexShrink: 1}} />
107-
<button style={{flexShrink: 0, marginLeft: "10px"}}>Action</button>
108-
</div>
109-
</div>
110-
),
111-
};
112-
11369
const TruncateStoryRender = (props: TruncateProps) => {
11470
const [searchWords, setSearchWords] = useState("");
11571

@@ -120,20 +76,6 @@ const TruncateStoryRender = (props: TruncateProps) => {
12076

12177
return (
12278
<div style={{display: "flex", flexDirection: "column", gap: "20px", alignItems: "center"}}>
123-
<div
124-
style={{
125-
resize: "horizontal",
126-
overflow: "auto",
127-
border: "1px solid #ccc",
128-
padding: "15px 10px",
129-
minWidth: "0px",
130-
width: "400px",
131-
maxWidth: "800px",
132-
}}
133-
>
134-
<TruncateComponent {...props} />
135-
</div>
136-
13779
<ViewportProvider
13880
style={{
13981
border: "1px solid black",
@@ -145,27 +87,27 @@ const TruncateStoryRender = (props: TruncateProps) => {
14587
resize: "horizontal",
14688
}}
14789
>
148-
<Header title="Truncate with highlight" style={{paddingBottom: "10px"}} />
90+
<Header title="Truncate with highlight" style={{paddingBottom: "10px"}}/>
14991

15092
<div style={{margin: "0 20px 10px"}}>
151-
<TextField value={searchWords} onChange={e => setSearchWords(e.target.value)} />
93+
<TextField value={searchWords} onChange={e => setSearchWords(e.target.value)}/>
15294
</div>
15395

15496
<ScrollArea style={{borderTop: "1px solid #ccc"}}>
15597
{filteredItems.map(({title, url}) => (
15698
<div key={url} style={{borderBottom: "1px solid #ccc", padding: "10px"}}>
15799
<TruncateComponent
158100
text={title}
159-
render={text => <Highlight textToHighlight={text} searchWords={[searchWords]} />}
101+
render={text => <Highlight textToHighlight={text} searchWords={[searchWords]}/>}
102+
{...props}
160103
/>
161104
<div style={{display: "flex", alignItems: "center", minWidth: 0}}>
162105
<TruncateComponent
163106
text={url}
164-
render={text => <Highlight textToHighlight={text} searchWords={[searchWords]} />}
165-
middle
166-
style={{flexShrink: 1}}
107+
render={text => <Highlight textToHighlight={text} searchWords={[searchWords]}/>}
108+
{...props}
167109
/>
168-
<button style={{flexShrink: 0, marginLeft: "8px"}}>Button</button>
110+
<button style={{marginLeft: '10px'}}>Open</button>
169111
</div>
170112
</div>
171113
))}
@@ -174,3 +116,45 @@ const TruncateStoryRender = (props: TruncateProps) => {
174116
</div>
175117
);
176118
};
119+
120+
export const Inline: StoryObj<TruncateProps> = {
121+
args: {
122+
text: "Very long text that should be truncated to fit in line with a button",
123+
middle: true,
124+
},
125+
126+
render: props => (
127+
<div style={{display: "flex", flexDirection: "column"}}>
128+
<Header title="Inline Truncate"/>
129+
<div
130+
style={{
131+
marginTop: '10px',
132+
resize: "horizontal",
133+
overflow: "auto",
134+
border: "1px solid #ccc",
135+
padding: "10px",
136+
width: "400px",
137+
}}
138+
>
139+
<TruncateComponent {...props} />
140+
</div>
141+
142+
<Header title="Inline Truncate with Button (flex-start)"/>
143+
<div
144+
style={{
145+
marginTop: '10px',
146+
resize: "horizontal",
147+
overflow: "auto",
148+
border: "1px solid #ccc",
149+
padding: "10px",
150+
width: "300px",
151+
display: "flex",
152+
alignItems: "center",
153+
}}
154+
>
155+
<TruncateComponent {...props}/>
156+
<button style={{marginLeft: "10px"}}>Button</button>
157+
</div>
158+
</div>
159+
),
160+
};

src/components/Truncate/Truncate.tsx

Lines changed: 2 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import classnames from "classnames";
1212

1313
import {useComponentProps} from "../../providers";
1414

15+
import {calculateMiddleTruncate} from "./utils";
16+
1517
import styles from "./truncate.module.scss";
1618

1719
export interface TruncateProps extends ComponentProps<"span"> {
@@ -22,69 +24,6 @@ export interface TruncateProps extends ComponentProps<"span"> {
2224
render?: (text: string) => React.ReactNode;
2325
}
2426

25-
const MAX_CACHE_SIZE = 1000;
26-
const cache = new Map<string, string>();
27-
let canvas: HTMLCanvasElement | null = null;
28-
29-
const addToCache = (key: string, value: string) => {
30-
if (cache.size >= MAX_CACHE_SIZE) {
31-
const oldestKey = cache.keys().next().value;
32-
if (oldestKey !== undefined) {
33-
cache.delete(oldestKey);
34-
}
35-
}
36-
cache.set(key, value);
37-
};
38-
39-
const calculateMiddleTruncate = (
40-
text: string,
41-
maxWidth: number,
42-
font: string,
43-
letterSpacing: string,
44-
separator: string
45-
) => {
46-
const cacheKey = `${text}-${maxWidth}-${font}-${letterSpacing}-${separator}`;
47-
if (cache.has(cacheKey)) return cache.get(cacheKey)!;
48-
49-
if (!canvas) {
50-
canvas = document.createElement("canvas");
51-
}
52-
const context = canvas.getContext("2d");
53-
if (!context) return text;
54-
context.font = font;
55-
context.letterSpacing = letterSpacing;
56-
57-
const measure = (txt: string) => context.measureText(txt).width;
58-
59-
if (measure(text) <= maxWidth) {
60-
addToCache(cacheKey, text);
61-
return text;
62-
}
63-
64-
let low = 0;
65-
let high = text.length;
66-
let result = "";
67-
68-
while (low <= high) {
69-
const mid = Math.floor((low + high) / 2);
70-
const leftHalf = Math.ceil(mid / 2);
71-
const rightHalf = Math.floor(mid / 2);
72-
73-
const trimmed = text.slice(0, leftHalf) + separator + text.slice(text.length - rightHalf);
74-
75-
if (measure(trimmed) <= maxWidth) {
76-
result = trimmed;
77-
low = mid + 1;
78-
} else {
79-
high = mid - 1;
80-
}
81-
}
82-
83-
const finalResult = result || text[0] + separator + text.slice(-1);
84-
addToCache(cacheKey, finalResult);
85-
return finalResult;
86-
};
87-
8827
const Truncate: ForwardRefRenderFunction<HTMLSpanElement, TruncateProps> = (props, ref) => {
8928
const {
9029
text = "",

src/components/Truncate/truncate.module.scss

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,6 @@
1414

1515
&--middle {
1616
text-overflow: clip;
17-
18-
padding-right: var(--truncate-around-space, 8px);
19-
20-
@include theme.rtl() {
21-
padding-right: 0;
22-
padding-left: var(--truncate-around-space, 8px);
23-
}
2417
}
2518

2619
&__hidden {

src/components/Truncate/utils.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const MAX_CACHE_SIZE = 1000;
2+
const cache = new Map<string, string>();
3+
let canvas: HTMLCanvasElement | null = null;
4+
5+
const addToCache = (key: string, value: string) => {
6+
if (cache.size >= MAX_CACHE_SIZE) {
7+
const oldestKey = cache.keys().next().value;
8+
if (oldestKey !== undefined) {
9+
cache.delete(oldestKey);
10+
}
11+
}
12+
cache.set(key, value);
13+
};
14+
15+
export const calculateMiddleTruncate = (
16+
text: string,
17+
maxWidth: number,
18+
font: string,
19+
letterSpacing: string,
20+
separator: string
21+
) => {
22+
const cacheKey = `${text}-${maxWidth}-${font}-${letterSpacing}-${separator}`;
23+
if (cache.has(cacheKey)) return cache.get(cacheKey)!;
24+
25+
if (!canvas) {
26+
canvas = document.createElement("canvas");
27+
}
28+
const context = canvas.getContext("2d");
29+
if (!context) return text;
30+
context.font = font;
31+
context.letterSpacing = letterSpacing;
32+
33+
const measure = (txt: string) => context.measureText(txt).width;
34+
35+
if (measure(text) <= maxWidth) {
36+
addToCache(cacheKey, text);
37+
return text;
38+
}
39+
40+
let low = 0;
41+
let high = text.length;
42+
let result = "";
43+
44+
while (low <= high) {
45+
const mid = Math.floor((low + high) / 2);
46+
const leftHalf = Math.ceil(mid / 2);
47+
const rightHalf = Math.floor(mid / 2);
48+
49+
const trimmed = text.slice(0, leftHalf) + separator + text.slice(text.length - rightHalf);
50+
51+
if (measure(trimmed) <= maxWidth) {
52+
result = trimmed;
53+
low = mid + 1;
54+
} else {
55+
high = mid - 1;
56+
}
57+
}
58+
59+
const finalResult = result || text[0] + separator + text.slice(-1);
60+
addToCache(cacheKey, finalResult);
61+
return finalResult;
62+
};

0 commit comments

Comments
 (0)