Skip to content

Commit 11f7d5a

Browse files
committed
feat(FR-1462): enhance BAIText with CSS-based ellipsis and Safari compatibility (#4397)
Resolves #4266 ([FR-1462](https://lablup.atlassian.net/browse/FR-1462)) # Enhanced BAIText Component with CSS-based Ellipsis Support This PR enhances the `BAIText` component to provide better cross-browser compatibility for text truncation with ellipsis. The implementation uses CSS-based ellipsis rendering that works consistently across browsers, including Safari. Key improvements: - Added support for single and multi-line text truncation with ellipsis - Implemented tooltip functionality for truncated text - Ensured compatibility with copyable text functionality - Maintained proper alignment and styling when used with other components - Migrated existing usages from Typography.Text to BAIText for consistent behavior The component now detects text overflow using ResizeObserver and conditionally applies tooltips only when text is actually truncated, improving performance and user experience. ## How to test > [!IMPORTANT] > Some storybook scenarios will be crashed with useTranslation hook. Please comment out 209-210 lines of `BAIText` and test them. 1. storybook ``` cd <webui directory>/packages/backend.ai-ui pnpm run storybook ``` 2. ellipsis in webui 1. Log in with demo-admin account (dogbowl). 2. Go to serving page and visit chatTest. 3. See token info. **Checklist:** - [ ] Documentation - [ ] Minium required manager version - [ ] Specific setting for review (eg., KB link, endpoint or how to setup) - [ ] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after [FR-1462]: https://lablup.atlassian.net/browse/FR-1462?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent f3e361b commit 11f7d5a

File tree

29 files changed

+895
-72
lines changed

29 files changed

+895
-72
lines changed

packages/backend.ai-ui/src/components/BAIText.stories.tsx

Lines changed: 640 additions & 0 deletions
Large diffs are not rendered by default.

packages/backend.ai-ui/src/components/BAIText.tsx

Lines changed: 192 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,216 @@
1-
import { Typography } from 'antd';
1+
import { theme, Tooltip, Typography } from 'antd';
2+
import type { TooltipProps } from 'antd/es/tooltip';
3+
import type { EllipsisConfig } from 'antd/es/typography/Base';
24
import type { TextProps as AntdTextProps } from 'antd/es/typography/Text';
3-
import React from 'react';
5+
import React, { useEffect, useRef, useState, ReactNode } from 'react';
6+
import { useTranslation } from 'react-i18next';
47

5-
export interface BAITextProps extends AntdTextProps {
8+
export interface BAITextProps extends Omit<AntdTextProps, 'ellipsis'> {
69
monospace?: boolean;
10+
// Enable CSS-based ellipsis with Safari compatibility (multi-line via -webkit-line-clamp)
11+
ellipsis?: boolean | EllipsisConfig;
12+
}
13+
14+
// Derive Tooltip props from the ellipsis config.
15+
function resolveTooltipProps(
16+
children: ReactNode,
17+
ellipsis: boolean | EllipsisConfig,
18+
): TooltipProps | null {
19+
if (!ellipsis || typeof ellipsis !== 'object') return null;
20+
21+
const { tooltip } = ellipsis;
22+
if (tooltip === true) return { title: children };
23+
if (
24+
tooltip &&
25+
typeof tooltip === 'object' &&
26+
!React.isValidElement(tooltip)
27+
) {
28+
return { title: children, ...(tooltip as TooltipProps) };
29+
}
30+
return null;
731
}
832

933
const BAIText: React.FC<BAITextProps> = ({
10-
type,
1134
style,
1235
monospace,
36+
ellipsis,
37+
copyable,
1338
children,
39+
strong,
40+
italic,
41+
underline,
42+
delete: deleteProp,
43+
mark,
44+
code,
45+
keyboard,
1446
...restProps
1547
}) => {
16-
// If monospace prop is true, apply monospace font styling
17-
if (monospace) {
48+
const { token } = theme.useToken();
49+
const { t } = useTranslation();
50+
const textRef = useRef<HTMLSpanElement>(null);
51+
const [isOverflowing, setIsOverflowing] = useState(false);
52+
const [isExpanded, setIsExpanded] = useState(false);
53+
54+
const expandable = typeof ellipsis === 'object' ? ellipsis.expandable : false;
55+
const onExpand = typeof ellipsis === 'object' ? ellipsis.onExpand : undefined;
56+
57+
useEffect(() => {
58+
if (!ellipsis || !textRef.current || isExpanded) return;
59+
const element = textRef.current;
60+
const rows = typeof ellipsis === 'object' ? ellipsis.rows || 1 : 1;
61+
const check = () => {
62+
if (!element) return;
63+
if (rows === 1) {
64+
setIsOverflowing(element.scrollWidth > element.clientWidth);
65+
} else {
66+
setIsOverflowing(element.scrollHeight > element.clientHeight);
67+
}
68+
};
69+
check();
70+
const ro = new ResizeObserver(check);
71+
ro.observe(element);
72+
return () => ro.disconnect();
73+
}, [ellipsis, children, isExpanded]);
74+
75+
const handleExpand = (e: React.MouseEvent<HTMLElement>) => {
76+
const newExpandedState = !isExpanded;
77+
setIsExpanded(newExpandedState);
78+
onExpand?.(e, { expanded: newExpandedState });
79+
};
80+
81+
if (!ellipsis) {
1882
return (
1983
<Typography.Text
20-
type={type}
84+
style={{ ...(monospace && { fontFamily: 'monospace' }), ...style }}
85+
copyable={copyable}
86+
strong={strong}
87+
italic={italic}
88+
underline={underline}
89+
delete={deleteProp}
90+
mark={mark}
91+
code={code}
92+
keyboard={keyboard}
2193
{...restProps}
22-
style={{
23-
fontFamily: 'monospace',
24-
...style,
25-
}}
2694
>
2795
{children}
2896
</Typography.Text>
2997
);
3098
}
3199

32-
// For non-monospace text, pass all props directly to antd Text
100+
const rows = typeof ellipsis === 'object' ? ellipsis.rows || 1 : 1;
101+
const tooltipProps = resolveTooltipProps(children, ellipsis);
102+
103+
// Styles for the container when code or keyboard is used
104+
const containerStyles: React.CSSProperties = {
105+
...(code && {
106+
backgroundColor: token.colorFillTertiary,
107+
border: `1px solid ${token.colorBorder}`,
108+
borderRadius: token.borderRadiusSM,
109+
padding: '0.2em 0.4em',
110+
fontFamily: token.fontFamilyCode,
111+
fontSize: '0.9em',
112+
display: 'inline-flex',
113+
alignItems: 'center',
114+
gap: token.marginXXS,
115+
}),
116+
...(keyboard && {
117+
backgroundColor: token.colorFillContent,
118+
border: `1px solid ${token.colorBorder}`,
119+
borderRadius: token.borderRadiusSM,
120+
padding: '0.15em 0.4em',
121+
fontFamily: token.fontFamilyCode,
122+
fontSize: '0.9em',
123+
boxShadow: `0 2px 0 ${token.colorBorderSecondary}`,
124+
display: 'inline-flex',
125+
alignItems: 'center',
126+
gap: token.marginXXS,
127+
}),
128+
};
129+
130+
// Styles for text content (excluding code/keyboard as they're on container)
131+
const textDecorationStyles: React.CSSProperties = {
132+
...(monospace && { fontFamily: 'monospace' }),
133+
...(strong && { fontWeight: token.fontWeightStrong }),
134+
...(italic && { fontStyle: 'italic' }),
135+
...(underline && deleteProp
136+
? { textDecoration: 'underline line-through' }
137+
: underline
138+
? { textDecoration: 'underline' }
139+
: deleteProp
140+
? { textDecoration: 'line-through' }
141+
: {}),
142+
...(mark && {
143+
backgroundColor: token.colorWarningBg,
144+
padding: '0 0.2em',
145+
}),
146+
};
147+
33148
return (
34-
<Typography.Text type={type} style={style} {...restProps}>
35-
{children}
149+
<Typography.Text
150+
{...restProps}
151+
copyable={(() => {
152+
if (!copyable) return undefined;
153+
if (typeof copyable === 'object') {
154+
return { ...copyable, text: copyable.text ?? String(children) };
155+
}
156+
return { text: String(children) }; // copyable === true
157+
})()}
158+
style={{
159+
display: 'inline-flex',
160+
alignItems: 'center',
161+
maxWidth: '100%',
162+
...containerStyles,
163+
...style,
164+
}}
165+
ellipsis={false}
166+
>
167+
<Tooltip
168+
{...tooltipProps}
169+
open={tooltipProps && isOverflowing && !isExpanded ? undefined : false}
170+
>
171+
<span
172+
ref={textRef}
173+
style={{
174+
...textDecorationStyles,
175+
overflow: isExpanded ? 'visible' : 'hidden',
176+
textOverflow: isExpanded ? 'clip' : 'ellipsis',
177+
flex: 1,
178+
minWidth: 0,
179+
...(isExpanded
180+
? {
181+
whiteSpace: 'normal',
182+
wordBreak: 'break-word',
183+
}
184+
: rows === 1
185+
? {
186+
whiteSpace: 'nowrap',
187+
display: 'block',
188+
}
189+
: {
190+
display: '-webkit-box',
191+
WebkitLineClamp: rows,
192+
WebkitBoxOrient: 'vertical',
193+
overflow: 'hidden',
194+
wordBreak: 'break-word',
195+
}),
196+
}}
197+
>
198+
{children}
199+
</span>
200+
</Tooltip>
201+
{expandable && isOverflowing && (
202+
<Typography.Link
203+
onClick={handleExpand}
204+
style={{
205+
marginLeft: token.marginXXS,
206+
flexShrink: 0,
207+
}}
208+
>
209+
{isExpanded
210+
? t('general.button.Collapse')
211+
: t('general.button.Expand')}
212+
</Typography.Link>
213+
)}
36214
</Typography.Text>
37215
);
38216
};

packages/backend.ai-ui/src/locale/de.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@
150150
"button": {
151151
"Cancel": "Stornieren",
152152
"Close": "Schließen",
153+
"Collapse": "Zusammenbruch",
153154
"CopyAll": "Alle kopieren",
154155
"Create": "Erstellen",
155156
"Delete": "Löschen",
157+
"Expand": "Expandieren",
156158
"Remove": "Entfernen",
157159
"Upload": "Hochladen"
158160
},

packages/backend.ai-ui/src/locale/el.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@
150150
"button": {
151151
"Cancel": "Ματαίωση",
152152
"Close": "Κοντά",
153+
"Collapse": "Κατάρρευση",
153154
"CopyAll": "Αντιγράψτε όλα",
154155
"Create": "Δημιουργώ",
155156
"Delete": "Διαγράφω",
157+
"Expand": "Διαστέλλω",
156158
"Remove": "Αφαιρώ",
157159
"Upload": "Μεταφορτώσω"
158160
},

packages/backend.ai-ui/src/locale/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,11 @@
153153
"button": {
154154
"Cancel": "Cancel",
155155
"Close": "Close",
156+
"Collapse": "Collapse",
156157
"CopyAll": "Copy All",
157158
"Create": "Create",
158159
"Delete": "Delete",
160+
"Expand": "Expand",
159161
"Remove": "Remove",
160162
"Upload": "Upload"
161163
},

packages/backend.ai-ui/src/locale/es.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@
150150
"button": {
151151
"Cancel": "Cancelar",
152152
"Close": "Cerca",
153+
"Collapse": "Colapsar",
153154
"CopyAll": "Copiar todo",
154155
"Create": "Crear",
155156
"Delete": "Borrar",
157+
"Expand": "Expandir",
156158
"Remove": "Eliminar",
157159
"Upload": "Subir"
158160
},

packages/backend.ai-ui/src/locale/fi.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@
150150
"button": {
151151
"Cancel": "Peruuttaa",
152152
"Close": "Lähellä",
153+
"Collapse": "Romahdus",
153154
"CopyAll": "Kopioida kaikki",
154155
"Create": "Luoda",
155156
"Delete": "Poistaa",
157+
"Expand": "Laajentaa",
156158
"Remove": "Poistaa",
157159
"Upload": "Ladata"
158160
},

packages/backend.ai-ui/src/locale/fr.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@
150150
"button": {
151151
"Cancel": "Annuler",
152152
"Close": "Fermer",
153+
"Collapse": "Effondrement",
153154
"CopyAll": "Copier tout",
154155
"Create": "Créer",
155156
"Delete": "Supprimer",
157+
"Expand": "Développer",
156158
"Remove": "Retirer",
157159
"Upload": "Télécharger"
158160
},

packages/backend.ai-ui/src/locale/id.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@
150150
"button": {
151151
"Cancel": "Membatalkan",
152152
"Close": "Menutup",
153+
"Collapse": "Runtuh",
153154
"CopyAll": "Salin semua",
154155
"Create": "Membuat",
155156
"Delete": "Menghapus",
157+
"Expand": "Memperluas",
156158
"Remove": "Menghapus",
157159
"Upload": "Mengunggah"
158160
},

packages/backend.ai-ui/src/locale/it.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@
150150
"button": {
151151
"Cancel": "Cancellare",
152152
"Close": "Vicino",
153+
"Collapse": "Crollo",
153154
"CopyAll": "Copia tutto",
154155
"Create": "Creare",
155156
"Delete": "Eliminare",
157+
"Expand": "Espandere",
156158
"Remove": "Rimuovere",
157159
"Upload": "Caricamento"
158160
},

0 commit comments

Comments
 (0)