From 53fb175855e3e9b599353c4a48966f7022a05bc4 Mon Sep 17 00:00:00 2001
From: estelledc <150260202+estelledc@users.noreply.github.com>
Date: Sun, 31 May 2026 13:17:52 +0800
Subject: [PATCH] fix(web): truncate long filenames in file list (no-preview
state) (#3370)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
When the design files preview pane was closed, a very long filename
would expand its `
` and push the kind / mtime / menu
columns off-screen — the auto-layout `
` had no width
constraint on the cell, so the existing `text-overflow: ellipsis`
on `.df-row-name` never engaged.
The `:not(.no-preview)` overrides in `routines.css` already pinned
the row's children to `width: 100%; max-width: 100%` when a preview
pane was open, but the no-preview state — the one shown in the issue
screenshot — had no equivalent guard.
CSS:
- `.df-cell-name`: add `max-width: 0; min-width: 0` so the table
cell collapses to its column allocation in auto-layout.
- `.df-row-name-wrap`: add `max-width: 100%`.
- `.df-row-name`: add `max-width: 100%; min-width: 0` so the flex
child clamps to the wrap and the existing ellipsis engages.
JSX (DesignFilesPanel.tsx):
- Add `title={f.name}` (and the equivalent for directory rows and
live-artifact rows) on `.df-row-name`. The browser surfaces the
full filename on hover even when the visible text is truncated, so
users can read the leading characters without opening the preview
pane. `` already renders the full name with
`word-break: break-word`.
Closes #3260
Validation:
- pnpm exec vitest run tests/components/DesignFilesPanel.long-name-truncate.test.tsx
→ 3/3 passed (1 was red on main: title attr was absent)
- pnpm --filter @open-design/web test → 2501/2501 passed (260 files)
- pnpm --filter @open-design/web typecheck → green
- pnpm guard → green
Note: jsdom does not measure layout, so the truncation itself can't
be asserted directly. The specs encode the structural contract the
CSS depends on (cell / wrap / name nesting + the `title` attr) so a
JSX shape change won't silently regress the fix.
---
apps/web/src/components/DesignFilesPanel.tsx | 13 ++-
.../web/src/styles/workspace/design-files.css | 26 ++++-
...signFilesPanel.long-name-truncate.test.tsx | 109 ++++++++++++++++++
3 files changed, 144 insertions(+), 4 deletions(-)
create mode 100644 apps/web/tests/components/DesignFilesPanel.long-name-truncate.test.tsx
diff --git a/apps/web/src/components/DesignFilesPanel.tsx b/apps/web/src/components/DesignFilesPanel.tsx
index e45c53517..f1c521af3 100644
--- a/apps/web/src/components/DesignFilesPanel.tsx
+++ b/apps/web/src/components/DesignFilesPanel.tsx
@@ -766,7 +766,12 @@ export function DesignFilesPanel({
}}
>
- {currentDir === '' ? f.name : f.name.slice(currentDir.length + 1)}
+
+ {currentDir === '' ? f.name : f.name.slice(currentDir.length + 1)}
+ {humanBytes(f.size)}
@@ -828,7 +833,7 @@ export function DesignFilesPanel({
setCurrentDir(fullPath)}>
@@ -1240,7 +1245,9 @@ export function DesignFilesPanel({
◉
- {artifact.title}
+
+ {artifact.title}
+ {t('designFiles.kindLiveArtifact')}` and push the
+// kind / mtime / menu columns off-screen. The CSS fix locks
+// `.df-cell-name` to `max-width: 0; min-width: 0` so the auto-layout
+// table truncates the name with the existing `text-overflow: ellipsis`
+// instead of growing the cell. The JSX fix adds `title={f.name}` so the
+// browser surfaces the full filename on hover even when the visible
+// text is truncated. (`` already renders the full name with
+// `word-break: break-word` for users who open the preview pane.)
+//
+// jsdom does not measure layout, so the truncation itself can't be
+// asserted directly. These specs encode the contract: the rendered DOM
+// keeps the structural classes the CSS relies on, and the `title` is
+// present on every name span so hover-tooltip is available even on the
+// very long row.
+
+const lsStore = new Map();
+vi.stubGlobal('localStorage', {
+ getItem: (key: string) => lsStore.get(key) ?? null,
+ setItem: (key: string, value: string) => { lsStore.set(key, value); },
+ removeItem: (key: string) => { lsStore.delete(key); },
+ clear: () => { lsStore.clear(); },
+});
+
+function file(overrides: Partial & Pick): ProjectFile {
+ return {
+ path: overrides.name,
+ type: 'file',
+ size: 1024,
+ mtime: Date.now(),
+ kind: 'image',
+ mime: 'image/png',
+ ...overrides,
+ };
+}
+
+function renderPanel(files: ProjectFile[]) {
+ return render(
+ ,
+ );
+}
+
+beforeEach(() => {
+ lsStore.clear();
+});
+
+afterEach(() => {
+ cleanup();
+});
+
+const LONG_NAME =
+ 'mpqdcf5m-A-1-year-old-boy-_standing_-with-short-black-hair_-big-eyes-with-black-pupils_-wearing-a-watermelon-shaped-helmet.jpeg';
+
+describe('DesignFilesPanel long filename truncation (#3260)', () => {
+ it('renders the file row for a long filename without crashing', () => {
+ const { container } = renderPanel([file({ name: LONG_NAME })]);
+ const row = container.querySelector(`[data-testid="design-file-row-${LONG_NAME}"]`);
+ expect(row).toBeTruthy();
+ });
+
+ it('exposes the full filename via a `title` attribute on the name span (hover tooltip)', () => {
+ const { container } = renderPanel([file({ name: LONG_NAME })]);
+ const nameSpan = container.querySelector('.df-row-name') as HTMLElement | null;
+ expect(nameSpan).toBeTruthy();
+ // The tooltip contract: hovering a truncated row reveals the full
+ // filename. Without this users see "...g-helmet.jpeg" with no way
+ // to read the leading characters until they open the preview pane.
+ expect(nameSpan?.getAttribute('title')).toBe(LONG_NAME);
+ });
+
+ it('keeps the truncate-friendly DOM structure (.df-cell-name > .df-row-name-btn > .df-row-name-wrap > .df-row-name)', () => {
+ const { container } = renderPanel([file({ name: LONG_NAME })]);
+ // The CSS fix relies on this nesting: `td.df-cell-name` constrains
+ // its width, the wrap is min-width:0 / max-width:100%, and
+ // `.df-row-name` carries `text-overflow: ellipsis`. If the JSX
+ // shape ever changes the CSS regression risk returns silently —
+ // this asserts the chain stays intact.
+ const cell = container.querySelector('td.df-cell-name');
+ expect(cell).toBeTruthy();
+ const btn = cell!.querySelector('button.df-row-name-btn');
+ expect(btn).toBeTruthy();
+ const wrap = btn!.querySelector('span.df-row-name-wrap');
+ expect(wrap).toBeTruthy();
+ const name = wrap!.querySelector('span.df-row-name');
+ expect(name).toBeTruthy();
+ });
+});