Skip to content

Commit 70d0ddb

Browse files
committed
refactor(ExpandableText): enhance text truncation logic and update default labels
- Improved text truncation mechanism using a binary search for better performance and accuracy. - Updated default expand and collapse button labels to lowercase for consistency. - Increased default maxLines from 3 to 5 in MapInspector for improved content visibility. - Removed unnecessary CSS clamping styles to streamline the component's appearance.
1 parent 0b62447 commit 70d0ddb

File tree

3 files changed

+97
-39
lines changed

3 files changed

+97
-39
lines changed

packages/ui/src/components/ExpandableText/ExpandableText.module.css

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,17 @@
1111
color: var(--ai-color-text-primary);
1212
white-space: pre-wrap;
1313
word-wrap: break-word;
14-
}
15-
16-
/* Clamped State - Use line-clamp */
17-
.clamped {
18-
display: -webkit-box;
19-
-webkit-box-orient: vertical;
20-
overflow: hidden;
21-
text-overflow: ellipsis;
14+
overflow-wrap: break-word;
2215
}
2316

2417
/* Toggle Button */
2518
.toggleButton {
2619
display: inline;
27-
margin-left: var(--ai-spacing-1);
2820
padding: 0;
2921
border: none;
3022
background: none;
3123
color: var(--ai-color-text-primary);
32-
font-size: var(--ai-font-size-body-small);
24+
font-size: inherit;
3325
font-weight: var(--ai-font-weight-medium);
3426
cursor: pointer;
3527
text-decoration: underline;

packages/ui/src/components/ExpandableText/ExpandableText.tsx

Lines changed: 94 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ export interface ExpandableTextProps {
1717

1818
/**
1919
* Label for expand button
20-
* @default 'View more'
20+
* @default 'view more'
2121
*/
2222
expandLabel?: string;
2323

2424
/**
2525
* Label for collapse button
26-
* @default 'View less'
26+
* @default 'view less'
2727
*/
2828
collapseLabel?: string;
2929

@@ -69,28 +69,80 @@ export interface ExpandableTextProps {
6969
export const ExpandableText: React.FC<ExpandableTextProps> = ({
7070
text,
7171
maxLines = 5,
72-
expandLabel = 'View more',
73-
collapseLabel = 'View less',
72+
expandLabel = 'view more',
73+
collapseLabel = 'view less',
7474
className,
7575
expanded: controlledExpanded,
7676
onExpandChange,
7777
}) => {
7878
const [internalExpanded, setInternalExpanded] = useState(false);
7979
const [needsTruncation, setNeedsTruncation] = useState(false);
80+
const [truncatedText, setTruncatedText] = useState(text);
8081
const textRef = useRef<HTMLDivElement>(null);
8182

8283
const isControlled = controlledExpanded !== undefined;
8384
const expanded = isControlled ? controlledExpanded : internalExpanded;
8485

85-
// Detect if text needs truncation
86+
// JavaScript-based truncation to insert button inline
8687
useEffect(() => {
8788
const element = textRef.current;
88-
if (!element) return;
89+
if (!element || expanded) return;
8990

90-
// Compare scroll height with client height to determine if text is truncated
91-
const isOverflowing = element.scrollHeight > element.clientHeight;
92-
setNeedsTruncation(isOverflowing);
93-
}, [text, maxLines]);
91+
const checkAndTruncate = () => {
92+
// Get line height
93+
const styles = getComputedStyle(element);
94+
const lineHeight = parseFloat(styles.lineHeight);
95+
const maxHeight = lineHeight * maxLines;
96+
97+
// Create a temporary element to measure
98+
const tempElement = element.cloneNode(true) as HTMLDivElement;
99+
tempElement.style.position = 'absolute';
100+
tempElement.style.visibility = 'hidden';
101+
tempElement.style.width = element.offsetWidth + 'px';
102+
tempElement.style.whiteSpace = 'pre-wrap';
103+
tempElement.style.wordWrap = 'break-word';
104+
document.body.appendChild(tempElement);
105+
106+
// Check if full text fits
107+
tempElement.innerHTML = '';
108+
tempElement.appendChild(document.createTextNode(text));
109+
110+
if (tempElement.scrollHeight <= maxHeight) {
111+
setNeedsTruncation(false);
112+
setTruncatedText(text);
113+
document.body.removeChild(tempElement);
114+
return;
115+
}
116+
117+
// Binary search for truncation point
118+
let low = 0;
119+
let high = text.length;
120+
let bestFit = text.substring(0, 50); // Default fallback
121+
122+
while (low <= high) {
123+
const mid = Math.floor((low + high) / 2);
124+
const testText = text.substring(0, mid);
125+
126+
tempElement.innerHTML = '';
127+
tempElement.textContent = testText + '... ' + expandLabel;
128+
129+
if (tempElement.scrollHeight <= maxHeight) {
130+
bestFit = testText;
131+
low = mid + 1;
132+
} else {
133+
high = mid - 1;
134+
}
135+
}
136+
137+
document.body.removeChild(tempElement);
138+
setTruncatedText(bestFit);
139+
setNeedsTruncation(true);
140+
};
141+
142+
requestAnimationFrame(() => {
143+
requestAnimationFrame(checkAndTruncate);
144+
});
145+
}, [text, maxLines, expanded, expandLabel]);
94146

95147
const handleToggle = () => {
96148
if (isControlled) {
@@ -103,25 +155,39 @@ export const ExpandableText: React.FC<ExpandableTextProps> = ({
103155

104156
return (
105157
<div className={cn(styles.expandableText, className)}>
106-
<div
107-
ref={textRef}
108-
className={cn(styles.text, !expanded && styles.clamped)}
109-
style={{
110-
WebkitLineClamp: expanded ? 'unset' : maxLines,
111-
}}
112-
>
113-
<SafeBrText text={text} />
158+
<div ref={textRef} className={styles.text}>
159+
{!expanded && needsTruncation ? (
160+
<>
161+
<SafeBrText text={truncatedText} />
162+
...{' '}
163+
<button
164+
className={styles.toggleButton}
165+
onClick={handleToggle}
166+
type="button"
167+
aria-expanded={expanded}
168+
>
169+
{expandLabel}
170+
</button>
171+
</>
172+
) : (
173+
<>
174+
<SafeBrText text={text} />
175+
{needsTruncation && expanded && (
176+
<>
177+
{' '}
178+
<button
179+
className={styles.toggleButton}
180+
onClick={handleToggle}
181+
type="button"
182+
aria-expanded={expanded}
183+
>
184+
{collapseLabel}
185+
</button>
186+
</>
187+
)}
188+
</>
189+
)}
114190
</div>
115-
{needsTruncation && (
116-
<button
117-
className={styles.toggleButton}
118-
onClick={handleToggle}
119-
type="button"
120-
aria-expanded={expanded}
121-
>
122-
{expanded ? collapseLabel : expandLabel}
123-
</button>
124-
)}
125191
</div>
126192
);
127193
};

packages/ui/src/components/Map/MapInspector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export const MapInspector: React.FC<MapInspectorProps> = ({ location, onClose, c
154154
{location.description && (
155155
<ExpandableText
156156
text={location.description}
157-
maxLines={3}
157+
maxLines={5}
158158
className={styles.description}
159159
/>
160160
)}

0 commit comments

Comments
 (0)