@telegraph/truncate 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +719 -41
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -1,85 +1,763 @@
|
|
|
1
|
+
# ✂️ Truncate
|
|
2
|
+
|
|
3
|
+
> Smart text truncation utilities with overflow detection, conditional tooltips, and responsive text handling for optimal user experience.
|
|
4
|
+
|
|
1
5
|

|
|
2
6
|
|
|
3
7
|
[](https://www.npmjs.com/package/@telegraph/truncate)
|
|
8
|
+
[](https://bundlephobia.com/result?p=@telegraph/truncate)
|
|
9
|
+
[](https://github.com/knocklabs/telegraph/blob/main/LICENSE)
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
## Installation
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
```bash
|
|
14
|
+
npm install @telegraph/truncate
|
|
15
|
+
```
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
> **Note**: This package has no stylesheets required.
|
|
10
18
|
|
|
11
|
-
|
|
12
|
-
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import {
|
|
23
|
+
TooltipIfTruncated,
|
|
24
|
+
TruncatedText,
|
|
25
|
+
useTruncate,
|
|
26
|
+
} from "@telegraph/truncate";
|
|
27
|
+
|
|
28
|
+
export const TruncateExamples = () => (
|
|
29
|
+
<div>
|
|
30
|
+
{/* Simple truncated text with tooltip */}
|
|
31
|
+
<TruncatedText maxWidth="40">
|
|
32
|
+
This text will be truncated if it exceeds the container width
|
|
33
|
+
</TruncatedText>
|
|
34
|
+
|
|
35
|
+
{/* Custom content with conditional tooltip */}
|
|
36
|
+
<TooltipIfTruncated label="Full content shown in tooltip">
|
|
37
|
+
<span style={{ maxWidth: "200px", overflow: "hidden" }}>
|
|
38
|
+
This content might be truncated
|
|
39
|
+
</span>
|
|
40
|
+
</TooltipIfTruncated>
|
|
41
|
+
|
|
42
|
+
{/* Using the hook for custom behavior */}
|
|
43
|
+
<CustomTruncatedComponent />
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const CustomTruncatedComponent = () => {
|
|
48
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
49
|
+
const { truncated } = useTruncate({ tgphRef: ref });
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div ref={ref} style={{ maxWidth: "150px", overflow: "hidden" }}>
|
|
53
|
+
Content status: {truncated ? "Truncated" : "Fits"}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
13
57
|
```
|
|
14
58
|
|
|
15
|
-
##
|
|
59
|
+
## API Reference
|
|
16
60
|
|
|
17
|
-
### `<TruncatedText
|
|
61
|
+
### `<TruncatedText>`
|
|
18
62
|
|
|
19
63
|
A text component that automatically truncates content with an ellipsis and shows a tooltip when truncated.
|
|
20
64
|
|
|
65
|
+
| Prop | Type | Default | Description |
|
|
66
|
+
| -------------- | ---------------------------------- | ----------- | --------------------------------------------------------------- |
|
|
67
|
+
| `maxWidth` | `string` | `undefined` | Maximum width of the text container |
|
|
68
|
+
| `tooltipProps` | `Partial<TooltipIfTruncatedProps>` | `{}` | Props to pass to the underlying TooltipIfTruncated component |
|
|
69
|
+
| `...TextProps` | `TgphComponentProps<typeof Text>` | - | All props from [@telegraph/typography](../typography/README.md) |
|
|
70
|
+
|
|
71
|
+
### `<TooltipIfTruncated>`
|
|
72
|
+
|
|
73
|
+
A component that conditionally shows a tooltip only when its content is truncated.
|
|
74
|
+
|
|
75
|
+
| Prop | Type | Default | Description |
|
|
76
|
+
| ----------------- | ------------------------------------ | ----------- | ----------------------------------------------------------------------------- |
|
|
77
|
+
| `label` | `string` | `undefined` | The text to show in the tooltip. If not provided, will use the content's text |
|
|
78
|
+
| `children` | `ReactNode` | - | **Required.** Content to monitor for truncation |
|
|
79
|
+
| `...TooltipProps` | `TgphComponentProps<typeof Tooltip>` | - | All props from [@telegraph/tooltip](../tooltip/README.md) |
|
|
80
|
+
|
|
81
|
+
### `useTruncate`
|
|
82
|
+
|
|
83
|
+
A hook that detects whether an element's content is truncated.
|
|
84
|
+
|
|
85
|
+
#### Parameters
|
|
86
|
+
|
|
87
|
+
| Name | Type | Description |
|
|
88
|
+
| -------- | ------------------------------------------- | --------------------------------------------------------------------- |
|
|
89
|
+
| `params` | `{ tgphRef: React.RefObject<HTMLElement> }` | A ref to the element to check for truncation |
|
|
90
|
+
| `deps` | `React.DependencyList` | Optional dependencies to re-run the truncation check when they change |
|
|
91
|
+
|
|
92
|
+
#### Returns
|
|
93
|
+
|
|
94
|
+
| Name | Type | Description |
|
|
95
|
+
| ----------- | --------- | ------------------------------------------ |
|
|
96
|
+
| `truncated` | `boolean` | Whether the element's content is truncated |
|
|
97
|
+
|
|
98
|
+
## Usage Patterns
|
|
99
|
+
|
|
100
|
+
### Basic Text Truncation
|
|
101
|
+
|
|
21
102
|
```tsx
|
|
22
103
|
import { TruncatedText } from "@telegraph/truncate";
|
|
23
104
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
105
|
+
export const UserName = ({ name }: { name: string }) => (
|
|
106
|
+
<TruncatedText maxWidth="40">{name}</TruncatedText>
|
|
107
|
+
);
|
|
27
108
|
```
|
|
28
109
|
|
|
29
|
-
|
|
110
|
+
### Custom Container Width
|
|
30
111
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
| tooltipProps | Partial<TgphComponentProps<typeof TooltipIfTruncated>> | `{}` | Props to pass to the underlying TooltipIfTruncated component |
|
|
34
|
-
| ...TextProps | TgphComponentProps<typeof Text> | - | All props from [@telegraph/typography](https://github.com/knocklabs/telegraph/tree/main/packages/typography) |
|
|
112
|
+
```tsx
|
|
113
|
+
import { TruncatedText } from "@telegraph/truncate";
|
|
35
114
|
|
|
36
|
-
|
|
115
|
+
export const ResponsiveTruncation = () => (
|
|
116
|
+
<div>
|
|
117
|
+
<TruncatedText maxWidth="20">Short container</TruncatedText>
|
|
118
|
+
<TruncatedText maxWidth="60">Longer container</TruncatedText>
|
|
119
|
+
<TruncatedText maxWidth="100%">Full width container</TruncatedText>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
```
|
|
37
123
|
|
|
38
|
-
|
|
124
|
+
### Conditional Tooltip
|
|
39
125
|
|
|
40
126
|
```tsx
|
|
41
127
|
import { TooltipIfTruncated } from "@telegraph/truncate";
|
|
42
128
|
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
129
|
+
export const FileNameDisplay = ({ fileName }: { fileName: string }) => (
|
|
130
|
+
<TooltipIfTruncated label={fileName}>
|
|
131
|
+
<div
|
|
132
|
+
className="file-name"
|
|
133
|
+
style={{ maxWidth: "200px", overflow: "hidden" }}
|
|
134
|
+
>
|
|
135
|
+
{fileName}
|
|
136
|
+
</div>
|
|
137
|
+
</TooltipIfTruncated>
|
|
138
|
+
);
|
|
46
139
|
```
|
|
47
140
|
|
|
48
|
-
|
|
141
|
+
### Custom Truncation Detection
|
|
49
142
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
| ...TooltipProps | TgphComponentProps<typeof Tooltip> | - | All props from [@telegraph/tooltip](https://github.com/knocklabs/telegraph/tree/main/packages/tooltip) |
|
|
143
|
+
```tsx
|
|
144
|
+
import { useTruncate } from "@telegraph/truncate";
|
|
145
|
+
import { useRef, useState } from "react";
|
|
54
146
|
|
|
55
|
-
|
|
147
|
+
export const DynamicContent = ({ content }: { content: string }) => {
|
|
148
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
149
|
+
const [expanded, setExpanded] = useState(false);
|
|
150
|
+
const { truncated } = useTruncate({ tgphRef: ref }, [content, expanded]);
|
|
56
151
|
|
|
57
|
-
|
|
152
|
+
return (
|
|
153
|
+
<div>
|
|
154
|
+
<div
|
|
155
|
+
ref={ref}
|
|
156
|
+
style={{
|
|
157
|
+
maxWidth: "300px",
|
|
158
|
+
overflow: "hidden",
|
|
159
|
+
whiteSpace: expanded ? "normal" : "nowrap",
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{content}
|
|
163
|
+
</div>
|
|
58
164
|
|
|
59
|
-
|
|
165
|
+
{truncated && !expanded && (
|
|
166
|
+
<button onClick={() => setExpanded(true)}>Show more</button>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{expanded && (
|
|
170
|
+
<button onClick={() => setExpanded(false)}>Show less</button>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Multi-line Truncation
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
import { TooltipIfTruncated } from "@telegraph/truncate";
|
|
181
|
+
|
|
182
|
+
export const ArticlePreview = ({
|
|
183
|
+
title,
|
|
184
|
+
description,
|
|
185
|
+
}: {
|
|
186
|
+
title: string;
|
|
187
|
+
description: string;
|
|
188
|
+
}) => (
|
|
189
|
+
<div className="article-preview">
|
|
190
|
+
<TooltipIfTruncated label={title}>
|
|
191
|
+
<h3
|
|
192
|
+
style={{
|
|
193
|
+
maxWidth: "100%",
|
|
194
|
+
overflow: "hidden",
|
|
195
|
+
whiteSpace: "nowrap",
|
|
196
|
+
textOverflow: "ellipsis",
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
{title}
|
|
200
|
+
</h3>
|
|
201
|
+
</TooltipIfTruncated>
|
|
202
|
+
|
|
203
|
+
<TooltipIfTruncated label={description}>
|
|
204
|
+
<p
|
|
205
|
+
style={{
|
|
206
|
+
display: "-webkit-box",
|
|
207
|
+
WebkitLineClamp: 3,
|
|
208
|
+
WebkitBoxOrient: "vertical",
|
|
209
|
+
overflow: "hidden",
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
{description}
|
|
213
|
+
</p>
|
|
214
|
+
</TooltipIfTruncated>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Advanced Usage
|
|
220
|
+
|
|
221
|
+
### Responsive Truncation
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import { useMediaQuery } from "@telegraph/helpers";
|
|
225
|
+
import { TruncatedText } from "@telegraph/truncate";
|
|
226
|
+
|
|
227
|
+
export const ResponsiveTruncatedText = ({ children }: { children: string }) => {
|
|
228
|
+
const isMobile = useMediaQuery("(max-width: 768px)");
|
|
229
|
+
const isTablet = useMediaQuery("(max-width: 1024px)");
|
|
230
|
+
|
|
231
|
+
const maxWidth = isMobile ? "30" : isTablet ? "50" : "80";
|
|
232
|
+
|
|
233
|
+
return <TruncatedText maxWidth={maxWidth}>{children}</TruncatedText>;
|
|
234
|
+
};
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Dynamic Content Monitoring
|
|
60
238
|
|
|
61
239
|
```tsx
|
|
62
240
|
import { useTruncate } from "@telegraph/truncate";
|
|
241
|
+
import { useEffect, useRef, useState } from "react";
|
|
63
242
|
|
|
64
|
-
const
|
|
65
|
-
const ref =
|
|
66
|
-
const
|
|
243
|
+
export const DynamicTruncationMonitor = ({ items }: { items: string[] }) => {
|
|
244
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
245
|
+
const [visibleItems, setVisibleItems] = useState(items);
|
|
246
|
+
const { truncated } = useTruncate({ tgphRef: ref }, [visibleItems]);
|
|
247
|
+
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (truncated && visibleItems.length > 1) {
|
|
250
|
+
// Remove items until content fits
|
|
251
|
+
setVisibleItems((prev) => prev.slice(0, -1));
|
|
252
|
+
}
|
|
253
|
+
}, [truncated, visibleItems.length]);
|
|
254
|
+
|
|
255
|
+
const hiddenCount = items.length - visibleItems.length;
|
|
67
256
|
|
|
68
257
|
return (
|
|
69
|
-
<div
|
|
258
|
+
<div
|
|
259
|
+
ref={ref}
|
|
260
|
+
style={{
|
|
261
|
+
maxWidth: "300px",
|
|
262
|
+
overflow: "hidden",
|
|
263
|
+
whiteSpace: "nowrap",
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
{visibleItems.join(", ")}
|
|
267
|
+
{hiddenCount > 0 && ` +${hiddenCount} more`}
|
|
268
|
+
</div>
|
|
70
269
|
);
|
|
71
270
|
};
|
|
72
271
|
```
|
|
73
272
|
|
|
74
|
-
|
|
273
|
+
### Table Cell Truncation
|
|
75
274
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
| params | `{ tgphRef: React.RefObject<HTMLElement> }` | A ref to the element to check for truncation |
|
|
79
|
-
| deps | `React.DependencyList` | Optional dependencies to re-run the truncation check when they change |
|
|
275
|
+
```tsx
|
|
276
|
+
import { TooltipIfTruncated } from "@telegraph/truncate";
|
|
80
277
|
|
|
81
|
-
|
|
278
|
+
export const DataTable = ({
|
|
279
|
+
rows,
|
|
280
|
+
}: {
|
|
281
|
+
rows: Array<{ id: string; name: string; email: string }>;
|
|
282
|
+
}) => (
|
|
283
|
+
<table className="data-table">
|
|
284
|
+
<thead>
|
|
285
|
+
<tr>
|
|
286
|
+
<th>Name</th>
|
|
287
|
+
<th>Email</th>
|
|
288
|
+
</tr>
|
|
289
|
+
</thead>
|
|
290
|
+
<tbody>
|
|
291
|
+
{rows.map((row) => (
|
|
292
|
+
<tr key={row.id}>
|
|
293
|
+
<td style={{ maxWidth: "150px" }}>
|
|
294
|
+
<TooltipIfTruncated label={row.name}>
|
|
295
|
+
<div style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
296
|
+
{row.name}
|
|
297
|
+
</div>
|
|
298
|
+
</TooltipIfTruncated>
|
|
299
|
+
</td>
|
|
300
|
+
<td style={{ maxWidth: "200px" }}>
|
|
301
|
+
<TooltipIfTruncated label={row.email}>
|
|
302
|
+
<div style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
303
|
+
{row.email}
|
|
304
|
+
</div>
|
|
305
|
+
</TooltipIfTruncated>
|
|
306
|
+
</td>
|
|
307
|
+
</tr>
|
|
308
|
+
))}
|
|
309
|
+
</tbody>
|
|
310
|
+
</table>
|
|
311
|
+
);
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Breadcrumb Truncation
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
import { TooltipIfTruncated, useTruncate } from "@telegraph/truncate";
|
|
318
|
+
import { useRef, useState } from "react";
|
|
319
|
+
|
|
320
|
+
export const SmartBreadcrumbs = ({
|
|
321
|
+
items,
|
|
322
|
+
}: {
|
|
323
|
+
items: Array<{ label: string; href: string }>;
|
|
324
|
+
}) => {
|
|
325
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
326
|
+
const [showAll, setShowAll] = useState(false);
|
|
327
|
+
const { truncated } = useTruncate({ tgphRef: ref }, [items, showAll]);
|
|
328
|
+
|
|
329
|
+
const displayItems = showAll
|
|
330
|
+
? items
|
|
331
|
+
: truncated && items.length > 3
|
|
332
|
+
? [items[0], { label: "...", href: "#" }, ...items.slice(-2)]
|
|
333
|
+
: items;
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<nav className="breadcrumbs">
|
|
337
|
+
<div
|
|
338
|
+
ref={ref}
|
|
339
|
+
style={{
|
|
340
|
+
display: "flex",
|
|
341
|
+
overflow: "hidden",
|
|
342
|
+
maxWidth: "100%",
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
{displayItems.map((item, index) => (
|
|
346
|
+
<span key={index} className="breadcrumb-item">
|
|
347
|
+
{item.label === "..." ? (
|
|
348
|
+
<button
|
|
349
|
+
onClick={() => setShowAll(true)}
|
|
350
|
+
className="breadcrumb-expand"
|
|
351
|
+
>
|
|
352
|
+
...
|
|
353
|
+
</button>
|
|
354
|
+
) : (
|
|
355
|
+
<TooltipIfTruncated label={item.label}>
|
|
356
|
+
<a
|
|
357
|
+
href={item.href}
|
|
358
|
+
style={{
|
|
359
|
+
maxWidth: "100px",
|
|
360
|
+
overflow: "hidden",
|
|
361
|
+
textOverflow: "ellipsis",
|
|
362
|
+
whiteSpace: "nowrap",
|
|
363
|
+
}}
|
|
364
|
+
>
|
|
365
|
+
{item.label}
|
|
366
|
+
</a>
|
|
367
|
+
</TooltipIfTruncated>
|
|
368
|
+
)}
|
|
369
|
+
{index < displayItems.length - 1 && (
|
|
370
|
+
<span className="separator"> / </span>
|
|
371
|
+
)}
|
|
372
|
+
</span>
|
|
373
|
+
))}
|
|
374
|
+
</div>
|
|
375
|
+
</nav>
|
|
376
|
+
);
|
|
377
|
+
};
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Tag List with Overflow
|
|
381
|
+
|
|
382
|
+
```tsx
|
|
383
|
+
import { TooltipIfTruncated, useTruncate } from "@telegraph/truncate";
|
|
384
|
+
import { useRef, useState } from "react";
|
|
385
|
+
|
|
386
|
+
export const TagList = ({ tags }: { tags: string[] }) => {
|
|
387
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
388
|
+
const [showAll, setShowAll] = useState(false);
|
|
389
|
+
const { truncated } = useTruncate({ tgphRef: ref }, [tags, showAll]);
|
|
390
|
+
|
|
391
|
+
const displayTags = showAll ? tags : tags.slice(0, 3);
|
|
392
|
+
const hiddenCount = tags.length - displayTags.length;
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<div className="tag-list">
|
|
396
|
+
<div
|
|
397
|
+
ref={ref}
|
|
398
|
+
style={{
|
|
399
|
+
display: "flex",
|
|
400
|
+
gap: "8px",
|
|
401
|
+
flexWrap: showAll ? "wrap" : "nowrap",
|
|
402
|
+
overflow: "hidden",
|
|
403
|
+
}}
|
|
404
|
+
>
|
|
405
|
+
{displayTags.map((tag, index) => (
|
|
406
|
+
<TooltipIfTruncated key={index} label={tag}>
|
|
407
|
+
<span
|
|
408
|
+
className="tag"
|
|
409
|
+
style={{
|
|
410
|
+
maxWidth: "100px",
|
|
411
|
+
overflow: "hidden",
|
|
412
|
+
textOverflow: "ellipsis",
|
|
413
|
+
whiteSpace: "nowrap",
|
|
414
|
+
}}
|
|
415
|
+
>
|
|
416
|
+
{tag}
|
|
417
|
+
</span>
|
|
418
|
+
</TooltipIfTruncated>
|
|
419
|
+
))}
|
|
420
|
+
|
|
421
|
+
{!showAll && hiddenCount > 0 && (
|
|
422
|
+
<button onClick={() => setShowAll(true)} className="tag-more">
|
|
423
|
+
+{hiddenCount}
|
|
424
|
+
</button>
|
|
425
|
+
)}
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
};
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Form Field Labels
|
|
433
|
+
|
|
434
|
+
```tsx
|
|
435
|
+
import { Input } from "@telegraph/input";
|
|
436
|
+
import { TruncatedText } from "@telegraph/truncate";
|
|
437
|
+
|
|
438
|
+
export const FormField = ({
|
|
439
|
+
label,
|
|
440
|
+
id,
|
|
441
|
+
...inputProps
|
|
442
|
+
}: {
|
|
443
|
+
label: string;
|
|
444
|
+
id: string;
|
|
445
|
+
} & React.InputHTMLAttributes<HTMLInputElement>) => (
|
|
446
|
+
<div className="form-field">
|
|
447
|
+
<label htmlFor={id}>
|
|
448
|
+
<TruncatedText maxWidth="100%">{label}</TruncatedText>
|
|
449
|
+
</label>
|
|
450
|
+
<Input id={id} {...inputProps} />
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Card Title Truncation
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
import { TooltipIfTruncated, TruncatedText } from "@telegraph/truncate";
|
|
459
|
+
|
|
460
|
+
export const ProductCard = ({
|
|
461
|
+
product,
|
|
462
|
+
}: {
|
|
463
|
+
product: {
|
|
464
|
+
name: string;
|
|
465
|
+
description: string;
|
|
466
|
+
price: string;
|
|
467
|
+
};
|
|
468
|
+
}) => (
|
|
469
|
+
<div className="product-card">
|
|
470
|
+
<TruncatedText
|
|
471
|
+
maxWidth="100%"
|
|
472
|
+
size="3"
|
|
473
|
+
weight="semibold"
|
|
474
|
+
tooltipProps={{ side: "top" }}
|
|
475
|
+
>
|
|
476
|
+
{product.name}
|
|
477
|
+
</TruncatedText>
|
|
478
|
+
|
|
479
|
+
<TooltipIfTruncated label={product.description}>
|
|
480
|
+
<p
|
|
481
|
+
className="product-description"
|
|
482
|
+
style={{
|
|
483
|
+
display: "-webkit-box",
|
|
484
|
+
WebkitLineClamp: 2,
|
|
485
|
+
WebkitBoxOrient: "vertical",
|
|
486
|
+
overflow: "hidden",
|
|
487
|
+
lineHeight: "1.4em",
|
|
488
|
+
maxHeight: "2.8em",
|
|
489
|
+
}}
|
|
490
|
+
>
|
|
491
|
+
{product.description}
|
|
492
|
+
</p>
|
|
493
|
+
</TooltipIfTruncated>
|
|
494
|
+
|
|
495
|
+
<div className="product-price">{product.price}</div>
|
|
496
|
+
</div>
|
|
497
|
+
);
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Accessibility
|
|
501
|
+
|
|
502
|
+
- ✅ **Screen Reader Support**: Full content available via tooltips when truncated
|
|
503
|
+
- ✅ **Keyboard Navigation**: Tooltips work with keyboard navigation
|
|
504
|
+
- ✅ **Focus Management**: Proper focus indicators for interactive elements
|
|
505
|
+
- ✅ **High Contrast**: Compatible with high contrast modes
|
|
506
|
+
- ✅ **Content Preservation**: No content is permanently hidden
|
|
507
|
+
|
|
508
|
+
### Best Practices
|
|
509
|
+
|
|
510
|
+
1. **Provide Full Content**: Always make complete content accessible via tooltips
|
|
511
|
+
2. **Logical Truncation**: Truncate at word boundaries when possible
|
|
512
|
+
3. **Clear Indicators**: Make it obvious when content is truncated
|
|
513
|
+
4. **Responsive Design**: Adjust truncation based on available space
|
|
514
|
+
5. **Keyboard Access**: Ensure truncated content is accessible via keyboard
|
|
515
|
+
|
|
516
|
+
### ARIA Considerations
|
|
517
|
+
|
|
518
|
+
- Tooltips include proper ARIA attributes when content is truncated
|
|
519
|
+
- Screen readers can access full content through tooltip mechanisms
|
|
520
|
+
- Focus management works correctly with truncated interactive elements
|
|
521
|
+
|
|
522
|
+
## Examples
|
|
523
|
+
|
|
524
|
+
### Basic Example
|
|
525
|
+
|
|
526
|
+
```tsx
|
|
527
|
+
import { TruncatedText } from "@telegraph/truncate";
|
|
528
|
+
|
|
529
|
+
export const UserProfile = ({
|
|
530
|
+
user,
|
|
531
|
+
}: {
|
|
532
|
+
user: { name: string; bio: string };
|
|
533
|
+
}) => (
|
|
534
|
+
<div className="user-profile">
|
|
535
|
+
<TruncatedText maxWidth="40" size="4" weight="semibold">
|
|
536
|
+
{user.name}
|
|
537
|
+
</TruncatedText>
|
|
538
|
+
<TruncatedText maxWidth="60" color="gray">
|
|
539
|
+
{user.bio}
|
|
540
|
+
</TruncatedText>
|
|
541
|
+
</div>
|
|
542
|
+
);
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Advanced Example
|
|
546
|
+
|
|
547
|
+
```tsx
|
|
548
|
+
import { TooltipIfTruncated, useTruncate } from "@telegraph/truncate";
|
|
549
|
+
import { useRef, useState } from "react";
|
|
550
|
+
|
|
551
|
+
export const SmartProductList = ({
|
|
552
|
+
products,
|
|
553
|
+
}: {
|
|
554
|
+
products: Array<{
|
|
555
|
+
id: string;
|
|
556
|
+
name: string;
|
|
557
|
+
description: string;
|
|
558
|
+
tags: string[];
|
|
559
|
+
}>;
|
|
560
|
+
}) => {
|
|
561
|
+
return (
|
|
562
|
+
<div className="product-list">
|
|
563
|
+
{products.map((product) => (
|
|
564
|
+
<ProductListItem key={product.id} product={product} />
|
|
565
|
+
))}
|
|
566
|
+
</div>
|
|
567
|
+
);
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const ProductListItem = ({ product }: { product: any }) => {
|
|
571
|
+
const [expanded, setExpanded] = useState(false);
|
|
572
|
+
const descriptionRef = useRef<HTMLParagraphElement>(null);
|
|
573
|
+
const { truncated } = useTruncate({ tgphRef: descriptionRef }, [expanded]);
|
|
574
|
+
|
|
575
|
+
return (
|
|
576
|
+
<div className="product-item">
|
|
577
|
+
<TooltipIfTruncated label={product.name}>
|
|
578
|
+
<h3
|
|
579
|
+
className="product-name"
|
|
580
|
+
style={{
|
|
581
|
+
maxWidth: "250px",
|
|
582
|
+
overflow: "hidden",
|
|
583
|
+
textOverflow: "ellipsis",
|
|
584
|
+
whiteSpace: "nowrap",
|
|
585
|
+
}}
|
|
586
|
+
>
|
|
587
|
+
{product.name}
|
|
588
|
+
</h3>
|
|
589
|
+
</TooltipIfTruncated>
|
|
590
|
+
|
|
591
|
+
<div className="product-description-container">
|
|
592
|
+
<p
|
|
593
|
+
ref={descriptionRef}
|
|
594
|
+
className="product-description"
|
|
595
|
+
style={{
|
|
596
|
+
display: "-webkit-box",
|
|
597
|
+
WebkitLineClamp: expanded ? "none" : 2,
|
|
598
|
+
WebkitBoxOrient: "vertical",
|
|
599
|
+
overflow: "hidden",
|
|
600
|
+
lineHeight: "1.4em",
|
|
601
|
+
}}
|
|
602
|
+
>
|
|
603
|
+
{product.description}
|
|
604
|
+
</p>
|
|
605
|
+
|
|
606
|
+
{truncated && (
|
|
607
|
+
<button
|
|
608
|
+
onClick={() => setExpanded(!expanded)}
|
|
609
|
+
className="expand-button"
|
|
610
|
+
>
|
|
611
|
+
{expanded ? "Show less" : "Show more"}
|
|
612
|
+
</button>
|
|
613
|
+
)}
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
<div className="product-tags">
|
|
617
|
+
<SmartTagList tags={product.tags} maxVisible={3} />
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
);
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const SmartTagList = ({
|
|
624
|
+
tags,
|
|
625
|
+
maxVisible = 3,
|
|
626
|
+
}: {
|
|
627
|
+
tags: string[];
|
|
628
|
+
maxVisible?: number;
|
|
629
|
+
}) => {
|
|
630
|
+
const [showAll, setShowAll] = useState(false);
|
|
631
|
+
const visibleTags = showAll ? tags : tags.slice(0, maxVisible);
|
|
632
|
+
const hiddenCount = tags.length - visibleTags.length;
|
|
633
|
+
|
|
634
|
+
return (
|
|
635
|
+
<div className="tag-list">
|
|
636
|
+
{visibleTags.map((tag, index) => (
|
|
637
|
+
<TooltipIfTruncated key={index} label={tag}>
|
|
638
|
+
<span
|
|
639
|
+
className="tag"
|
|
640
|
+
style={{
|
|
641
|
+
maxWidth: "80px",
|
|
642
|
+
overflow: "hidden",
|
|
643
|
+
textOverflow: "ellipsis",
|
|
644
|
+
whiteSpace: "nowrap",
|
|
645
|
+
}}
|
|
646
|
+
>
|
|
647
|
+
{tag}
|
|
648
|
+
</span>
|
|
649
|
+
</TooltipIfTruncated>
|
|
650
|
+
))}
|
|
651
|
+
|
|
652
|
+
{!showAll && hiddenCount > 0 && (
|
|
653
|
+
<button onClick={() => setShowAll(true)} className="show-more-tags">
|
|
654
|
+
+{hiddenCount} more
|
|
655
|
+
</button>
|
|
656
|
+
)}
|
|
657
|
+
</div>
|
|
658
|
+
);
|
|
659
|
+
};
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### Real-world Example
|
|
663
|
+
|
|
664
|
+
```tsx
|
|
665
|
+
import {
|
|
666
|
+
TooltipIfTruncated,
|
|
667
|
+
TruncatedText,
|
|
668
|
+
useTruncate,
|
|
669
|
+
} from "@telegraph/truncate";
|
|
670
|
+
import { useRef, useState } from "react";
|
|
671
|
+
|
|
672
|
+
export const EmailInbox = ({
|
|
673
|
+
emails,
|
|
674
|
+
}: {
|
|
675
|
+
emails: Array<{
|
|
676
|
+
id: string;
|
|
677
|
+
from: string;
|
|
678
|
+
subject: string;
|
|
679
|
+
preview: string;
|
|
680
|
+
timestamp: string;
|
|
681
|
+
isRead: boolean;
|
|
682
|
+
}>;
|
|
683
|
+
}) => {
|
|
684
|
+
return (
|
|
685
|
+
<div className="email-inbox">
|
|
686
|
+
{emails.map((email) => (
|
|
687
|
+
<EmailRow key={email.id} email={email} />
|
|
688
|
+
))}
|
|
689
|
+
</div>
|
|
690
|
+
);
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const EmailRow = ({ email }: { email: any }) => {
|
|
694
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
695
|
+
const previewRef = useRef<HTMLDivElement>(null);
|
|
696
|
+
const { truncated } = useTruncate({ tgphRef: previewRef }, [isExpanded]);
|
|
697
|
+
|
|
698
|
+
return (
|
|
699
|
+
<div
|
|
700
|
+
className={`email-row ${!email.isRead ? "unread" : ""}`}
|
|
701
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
702
|
+
>
|
|
703
|
+
<div className="email-header">
|
|
704
|
+
<div className="email-from">
|
|
705
|
+
<TruncatedText
|
|
706
|
+
maxWidth="30"
|
|
707
|
+
weight={email.isRead ? "regular" : "semibold"}
|
|
708
|
+
>
|
|
709
|
+
{email.from}
|
|
710
|
+
</TruncatedText>
|
|
711
|
+
</div>
|
|
712
|
+
|
|
713
|
+
<div className="email-subject">
|
|
714
|
+
<TruncatedText
|
|
715
|
+
maxWidth="50"
|
|
716
|
+
weight={email.isRead ? "regular" : "medium"}
|
|
717
|
+
>
|
|
718
|
+
{email.subject}
|
|
719
|
+
</TruncatedText>
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
<div className="email-timestamp">{email.timestamp}</div>
|
|
723
|
+
</div>
|
|
724
|
+
|
|
725
|
+
<div className="email-preview-container">
|
|
726
|
+
<div
|
|
727
|
+
ref={previewRef}
|
|
728
|
+
className="email-preview"
|
|
729
|
+
style={{
|
|
730
|
+
display: "-webkit-box",
|
|
731
|
+
WebkitLineClamp: isExpanded ? "none" : 1,
|
|
732
|
+
WebkitBoxOrient: "vertical",
|
|
733
|
+
overflow: "hidden",
|
|
734
|
+
color: email.isRead ? "#666" : "#333",
|
|
735
|
+
}}
|
|
736
|
+
>
|
|
737
|
+
{email.preview}
|
|
738
|
+
</div>
|
|
739
|
+
|
|
740
|
+
{truncated && !isExpanded && (
|
|
741
|
+
<TooltipIfTruncated label={email.preview}>
|
|
742
|
+
<button className="expand-preview">...</button>
|
|
743
|
+
</TooltipIfTruncated>
|
|
744
|
+
)}
|
|
745
|
+
</div>
|
|
746
|
+
</div>
|
|
747
|
+
);
|
|
748
|
+
};
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
## References
|
|
752
|
+
|
|
753
|
+
- [Storybook Demo](https://storybook.telegraph.dev/?path=/docs/truncate)
|
|
754
|
+
- [Typography Component](../typography/README.md) - Used by TruncatedText
|
|
755
|
+
- [Tooltip Component](../tooltip/README.md) - Used for showing full content
|
|
756
|
+
|
|
757
|
+
## Contributing
|
|
758
|
+
|
|
759
|
+
See our [Contributing Guide](../../CONTRIBUTING.md) for more details.
|
|
760
|
+
|
|
761
|
+
## License
|
|
82
762
|
|
|
83
|
-
|
|
84
|
-
| --------- | --------- | ------------------------------------------ |
|
|
85
|
-
| truncated | `boolean` | Whether the element's content is truncated |
|
|
763
|
+
MIT License - see [LICENSE](../../LICENSE) for details.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telegraph/truncate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"description": "Utility components for detecting and responding to content overflow, truncation, and visibility states in the UI.",
|
|
5
5
|
"repository": "https://github.com/knocklabs/telegraph/tree/main/packages/truncate",
|
|
6
6
|
"author": "@knocklabs",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@telegraph/helpers": "^0.0.13",
|
|
36
|
-
"@telegraph/tooltip": "^0.0.
|
|
36
|
+
"@telegraph/tooltip": "^0.0.55",
|
|
37
37
|
"@telegraph/typography": "^0.1.23"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|