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(); + }); +});