From fb9ac4a34c62840ac55ddbd9f88e9b01ca31cce2 Mon Sep 17 00:00:00 2001 From: Qwen Code Autofix Date: Wed, 29 Jul 2026 17:04:40 +0000 Subject: [PATCH] fix(web-shell): address review feedback on enhanced table controls (#8041) --- .../messages/EnhancedMarkdownTable.module.css | 12 +- .../messages/EnhancedMarkdownTable.test.tsx | 104 ++++++++++++++++++ .../messages/EnhancedMarkdownTable.tsx | 5 +- 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/packages/web-shell/client/components/messages/EnhancedMarkdownTable.module.css b/packages/web-shell/client/components/messages/EnhancedMarkdownTable.module.css index e583cefede..a21ac613b7 100644 --- a/packages/web-shell/client/components/messages/EnhancedMarkdownTable.module.css +++ b/packages/web-shell/client/components/messages/EnhancedMarkdownTable.module.css @@ -1,5 +1,6 @@ .tableShell { position: relative; + --toolbar-height: 38px; max-width: 100%; margin: 8px 0; overflow: visible; @@ -52,7 +53,7 @@ gap: 8px; align-items: center; box-sizing: border-box; - height: 38px; + height: var(--toolbar-height); padding: 4px 10px; border-bottom: 1px solid var(--border); border-radius: 14px 14px 0 0; @@ -128,8 +129,6 @@ font-size: 12px; } -.densityTrigger:focus, -.densityTrigger:focus-visible, .densityTrigger[data-state='open'] { border-color: transparent; box-shadow: none; @@ -179,9 +178,14 @@ outline: none; } +.scroller:focus-visible { + outline: 2px solid var(--agent-blue-500); + outline-offset: -2px; +} + .frozenColumnShadow { position: absolute; - top: 38px; + top: var(--toolbar-height); bottom: 0; z-index: 9; width: 14px; diff --git a/packages/web-shell/client/components/messages/EnhancedMarkdownTable.test.tsx b/packages/web-shell/client/components/messages/EnhancedMarkdownTable.test.tsx index 555232b55e..3820c6da9c 100644 --- a/packages/web-shell/client/components/messages/EnhancedMarkdownTable.test.tsx +++ b/packages/web-shell/client/components/messages/EnhancedMarkdownTable.test.tsx @@ -1448,6 +1448,30 @@ describe('EnhancedMarkdownTable', () => { ); }); + it('spans the detail row across the filler column', () => { + const container = renderTable(); + + for (const column of ['Team', 'Score']) { + act(() => { + button(container, `Resize ${column}`).dispatchEvent( + new KeyboardEvent('keydown', { + bubbles: true, + key: 'ArrowRight', + }), + ); + }); + } + expect(container.querySelector('[class*="fillerColumn"]')).not.toBeNull(); + + click(button(container, 'View details for row 1')); + + const detailCell = container.querySelector( + '[class*="detailCell"]', + ); + expect(detailCell).not.toBeNull(); + expect(detailCell!.colSpan).toBe(4); + }); + it('resizes compact auto columns from their rendered width with keyboard arrows', () => { const container = renderTable(); selectValue(button(container, 'Table density'), 'compact'); @@ -1645,6 +1669,68 @@ describe('EnhancedMarkdownTable', () => { ).toBe('280px'); }); + it('repositions the frozen shadow when the ResizeObserver fires', () => { + const callbacks: ResizeObserverCallback[] = []; + const OriginalResizeObserver = globalThis.ResizeObserver; + class CapturingResizeObserver { + constructor(private readonly callback: ResizeObserverCallback) { + callbacks.push(callback); + } + observe() {} + unobserve() {} + disconnect() {} + } + (globalThis as { ResizeObserver?: unknown }).ResizeObserver = + CapturingResizeObserver; + + try { + const container = renderWideTable(); + const shell = container.querySelector( + '[class*="tableShell"]', + ); + const header = button(container, 'Sort by Team').closest('th'); + expect(shell).not.toBeNull(); + expect(header).not.toBeNull(); + Object.defineProperty(shell, 'clientLeft', { + configurable: true, + value: 1, + }); + Object.defineProperty(shell, 'getBoundingClientRect', { + configurable: true, + value: () => ({ left: 20 }) as DOMRect, + }); + Object.defineProperty(header, 'getBoundingClientRect', { + configurable: true, + value: () => ({ right: 301 }) as DOMRect, + }); + + freezeFirstColumn(container); + + expect( + container.querySelector('[class*="frozenColumnShadow"]') + ?.style.left, + ).toBe('280px'); + + Object.defineProperty(header, 'getBoundingClientRect', { + configurable: true, + value: () => ({ right: 351 }) as DOMRect, + }); + act(() => { + for (const cb of callbacks) { + cb([], {} as ResizeObserver); + } + }); + + expect( + container.querySelector('[class*="frozenColumnShadow"]') + ?.style.left, + ).toBe('330px'); + } finally { + (globalThis as { ResizeObserver?: unknown }).ResizeObserver = + OriginalResizeObserver; + } + }); + it('dismisses the first-column context menu without clearing the active column', () => { const container = renderWideTable(); @@ -2057,6 +2143,24 @@ describe('EnhancedMarkdownTable', () => { expect(detailsButton.getAttribute('aria-expanded')).toBe('true'); }); + it('falls back to window.scrollBy when no scroll ancestor exists', () => { + const container = renderTable(); + const scrollBySpy = vi + .spyOn(window, 'scrollBy') + .mockImplementation(() => {}); + const detailsButton = button(container, 'View details for row 3'); + const row = detailsButton.closest('tr'); + expect(row).not.toBeNull(); + vi.spyOn(row!, 'getBoundingClientRect') + .mockReturnValueOnce({ top: 120 } as DOMRect) + .mockReturnValueOnce({ top: 280 } as DOMRect); + + click(detailsButton); + + expect(scrollBySpy).toHaveBeenCalledWith(0, 160); + expect(detailsButton.getAttribute('aria-expanded')).toBe('true'); + }); + it('shows statistics for a numeric selection', () => { const container = renderTable(); diff --git a/packages/web-shell/client/components/messages/EnhancedMarkdownTable.tsx b/packages/web-shell/client/components/messages/EnhancedMarkdownTable.tsx index b2579d5a87..d1293cb3a4 100644 --- a/packages/web-shell/client/components/messages/EnhancedMarkdownTable.tsx +++ b/packages/web-shell/client/components/messages/EnhancedMarkdownTable.tsx @@ -182,6 +182,7 @@ const NUMBER_FILTER_LABEL_KEYS: Record = { export const MAX_ENHANCED_TABLE_ROWS = 500; export const MAX_ENHANCED_TABLE_COLUMNS = 50; +// Must stay in sync with --action-column-width in EnhancedMarkdownTable.module.css const ACTION_COLUMN_WIDTH = 40; const DEFAULT_COLUMN_WIDTH = 160; const COMPACT_COLUMN_WIDTH = 72; @@ -2175,9 +2176,8 @@ export function EnhancedTable({ minWidth: number, fixedWidth: number, flexibleColumnCount: number, - containerWidth: '100cqw' | '100%', ): string => - `max(${minWidth}px, calc((${containerWidth} - ${ACTION_COLUMN_WIDTH}px - ${fixedWidth}px) / ${flexibleColumnCount}))`; + `max(${minWidth}px, calc((100cqw - ${ACTION_COLUMN_WIDTH}px - ${fixedWidth}px) / ${flexibleColumnCount}))`; const columnGroupStyle = (columnIndex: number): CSSProperties => { const width = columnWidths[columnIndex]; @@ -2190,7 +2190,6 @@ export function EnhancedTable({ minWidth, fixedVisibleColumnWidth, flexibleColumnCount, - '100cqw', ), }; };