Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,21 @@ test('row selection column stays fixed at desktop and narrow widths', async ({ e
expect(selectionWidths[0]).toBeCloseTo(selectionWidths[1]!, 1);

await page.setViewportSize({ height: 900, width: 1440 });
for (const columnId of ['summary', 'user', 'date']) {
const resizableColumnIds = ['summary', 'user', 'date'];
const columnWidthsBeforeResize = await Promise.all(
resizableColumnIds.map(async (columnId) => {
const resizeHandle = table.getByRole('button', { name: `Resize ${columnId} column` });
return (await resizeHandle.locator('xpath=ancestor::th[1]').boundingBox())!.width;
})
);

for (const columnId of resizableColumnIds) {
await table.getByRole('button', { name: `Resize ${columnId} column` }).press('ArrowRight');
}

const explicitlySizedTableBox = await table.boundingBox();
const explicitlySizedSelectionBox = await table.locator('thead th').first().boundingBox();
expect(explicitlySizedTableBox!.width).toBeCloseTo(816, 1);
const expectedTableWidth = explicitlySizedSelectionBox!.width + columnWidthsBeforeResize.reduce((total, width) => total + width + 16, 0);
expect(explicitlySizedTableBox!.width).toBeCloseTo(expectedTableWidth, 1);
expect(explicitlySizedSelectionBox!.width).toBeCloseTo(32, 1);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest';

import { getSessionColumns } from './session-table-columns';

describe('getSessionColumns', () => {
it('marks Summary as the flexible full-width column', () => {
const summaryColumn = getSessionColumns().find((column) => column.id === 'summary');

expect(summaryColumn).toMatchObject({
header: 'Summary',
meta: {
class: 'w-full'
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ export function getSessionColumns(): ColumnDef<StockFeatures, EventSummaryModel<
{
cell: (prop) => renderComponent(Summary, { showStatus: false, showType: false, summary: prop.row.original }),
enableHiding: false,
header: 'Summary'
header: 'Summary',
id: 'summary',
meta: {
class: 'w-full'
}
},
{
cell: (prop) => renderComponent(SessionDurationCell, { summary: prop.row.original }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,13 @@

function getFlexibleDataColumnId(): string | undefined {
const columnSizing = table.atoms.columnSizing?.get() ?? {};
const unsizedColumns = getVisibleDataColumns().filter((column) => columnSizing[column.id] === undefined);
return unsizedColumns.at(-1)?.id;
const visibleDataColumns = getVisibleDataColumns();
const fullWidthColumns = visibleDataColumns.filter((column) => getMetaClass(column.columnDef.meta).split(' ').includes('w-full'));
if (fullWidthColumns.length > 0) {
return fullWidthColumns.find((column) => columnSizing[column.id] === undefined)?.id;
}

return visibleDataColumns.filter((column) => columnSizing[column.id] === undefined).at(-1)?.id;
}

function getVisibleDataColumnCount(): number {
Expand Down Expand Up @@ -152,15 +157,94 @@
event.preventDefault();
event.stopPropagation();
const delta = event.key === 'ArrowLeft' ? -16 : 16;
const currentSize = getResizeStartSize(event, header);
table.setColumnSizing((current) => ({
...current,
[header.column.id]: Math.min(
header.column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER,
Math.max(header.column.columnDef.minSize ?? 20, header.column.getSize() + delta)
Math.max(header.column.columnDef.minSize ?? 20, currentSize + delta)
)
}));
}

function onResizeStart(event: MouseEvent | TouchEvent, header: Header<StockFeatures, TData, unknown>): void {
const currentSize = getResizeStartSize(event, header);
if (currentSize === header.column.getSize()) {
header.getResizeHandler()(event);
return;
}

const startPosition = getClientPosition(event);
const document = (event.currentTarget as HTMLElement | null)?.ownerDocument;
if (startPosition === undefined || !document) {
header.getResizeHandler()(event);
return;
}

const startEvent = event;
const removePendingListeners = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseEnd);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
document.removeEventListener('touchcancel', onTouchEnd);
};

const startResize = (position: number) => {
if (position === startPosition) {
return;
}

removePendingListeners();
table.setColumnSizing((current) => ({
...current,
[header.column.id]: currentSize
}));
Comment on lines +199 to +202

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't persist the flexible width before a drag occurs

When a user presses and releases the resize handle without moving it, this immediately writes the rendered width into the persisted column-sizing state. For the flexible w-full column, that makes getFlexibleDataColumnId() stop treating the column as flexible, so a simple click permanently converts it to a fixed-width column and later viewport resizing can leave unused space or introduce horizontal overflow. The size should only be committed after an actual drag, while still using the rendered width as the drag's starting point.

Useful? React with 👍 / 👎.

header.getResizeHandler()(startEvent);
setColumnSize(header, currentSize + position - startPosition);
};

const onMouseMove = (moveEvent: MouseEvent) => startResize(moveEvent.clientX);
const onMouseEnd = () => removePendingListeners();
const onTouchMove = (moveEvent: TouchEvent) => {
const position = getClientPosition(moveEvent);
if (position !== undefined) {
startResize(position);
}
};

const onTouchEnd = () => removePendingListeners();

if (event instanceof TouchEvent) {
document.addEventListener('touchmove', onTouchMove);
document.addEventListener('touchend', onTouchEnd);
document.addEventListener('touchcancel', onTouchEnd);
} else {
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseEnd);
}
}

function getClientPosition(event: MouseEvent | TouchEvent): number | undefined {
return event instanceof TouchEvent ? event.touches[0]?.clientX : event.clientX;
}

function setColumnSize(header: Header<StockFeatures, TData, unknown>, size: number): void {
table.setColumnSizing((current) => ({
...current,
[header.column.id]: Math.min(header.column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER, Math.max(header.column.columnDef.minSize ?? 20, size))
}));
}

function getResizeStartSize(event: KeyboardEvent | MouseEvent | TouchEvent, header: Header<StockFeatures, TData, unknown>): number {
if (header.column.id !== getFlexibleDataColumnId()) {
return header.column.getSize();
}

const headerElement = (event.currentTarget as HTMLElement | null)?.closest('th');
return headerElement?.getBoundingClientRect().width || header.column.getSize();
}

function removeWidthClasses(className: string): string {
return className
.split(' ')
Expand Down Expand Up @@ -190,8 +274,8 @@
]}
ondblclick={() => header.column.resetSize()}
onkeydown={(event) => onResizeKeydown(event, header)}
onmousedown={header.getResizeHandler()}
ontouchstart={header.getResizeHandler()}
onmousedown={(event) => onResizeStart(event, header)}
ontouchstart={(event) => onResizeStart(event, header)}
title={`Resize ${header.column.id} column`}
type="button"
></button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,67 @@ describe('DataTableBody', () => {
expect(dateHeader.style.cssText).toBe('width: 130px; min-width: 130px; max-width: 130px;');
});

it('uses the full-width data column as the flexible column', () => {
render(DataTableBodyTestHarness, { fullWidthSummary: true, kind: 'event', onRowClick: vi.fn() });

const summaryHeader = screen.getByRole('columnheader', { name: 'Summary' });
const dateHeader = screen.getByRole('columnheader', { name: 'Date' });

expect(summaryHeader.style.cssText).toBe('width: 100%;');
expect(dateHeader.style.cssText).toBe('width: 150px; min-width: 150px; max-width: 150px;');
});

it('does not transfer flexibility after the full-width column is sized', () => {
render(DataTableBodyTestHarness, { fullWidthSummary: true, kind: 'event', onRowClick: vi.fn(), sizedFullWidthSummary: true });

const table = screen.getByRole('table');
const summaryHeader = screen.getByRole('columnheader', { name: 'Summary' });
const dateHeader = screen.getByRole('columnheader', { name: 'Date' });

expect(table.style.cssText).toBe('width: 362px; min-width: 362px;');
expect(summaryHeader.style.cssText).toBe('width: 180px; min-width: 180px; max-width: 180px;');
expect(dateHeader.style.cssText).toBe('width: 150px; min-width: 150px; max-width: 150px;');
});

it('resizes a flexible column from its rendered width', async () => {
render(DataTableBodyTestHarness, { fullWidthSummary: true, kind: 'event', onRowClick: vi.fn() });

const summaryHeader = screen.getByRole('columnheader', { name: 'Summary' });
vi.spyOn(summaryHeader, 'getBoundingClientRect').mockReturnValue({ width: 300 } as DOMRect);

await fireEvent.keyDown(screen.getByRole('button', { name: 'Resize summary column' }), { key: 'ArrowRight' });

expect(summaryHeader.style.cssText).toBe('width: 316px; min-width: 316px; max-width: 316px;');
});

it('keeps a flexible column flexible when its resize handle is clicked without dragging', async () => {
render(DataTableBodyTestHarness, { fullWidthSummary: true, kind: 'event', onRowClick: vi.fn() });

const summaryHeader = screen.getByRole('columnheader', { name: 'Summary' });
vi.spyOn(summaryHeader, 'getBoundingClientRect').mockReturnValue({ width: 300 } as DOMRect);
const resizeHandle = screen.getByRole('button', { name: 'Resize summary column' });

await fireEvent.mouseDown(resizeHandle, { clientX: 100 });
await fireEvent.mouseUp(document, { clientX: 100 });

expect(summaryHeader.style.cssText).toBe('width: 100%;');
});

it('starts dragging a flexible column from its rendered width', async () => {
render(DataTableBodyTestHarness, { fullWidthSummary: true, kind: 'event', onRowClick: vi.fn() });

const summaryHeader = screen.getByRole('columnheader', { name: 'Summary' });
vi.spyOn(summaryHeader, 'getBoundingClientRect').mockReturnValue({ width: 300 } as DOMRect);
const resizeHandle = screen.getByRole('button', { name: 'Resize summary column' });

await fireEvent.mouseDown(resizeHandle, { clientX: 100 });
await fireEvent.mouseMove(document, { clientX: 116 });

expect(summaryHeader.style.cssText).toBe('width: 316px; min-width: 316px; max-width: 316px;');

await fireEvent.mouseUp(document, { clientX: 116 });
});

it('lets a resized header shrink below its metadata width', () => {
render(DataTableBodyTestHarness, { kind: 'event', onRowClick: vi.fn() });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@
import Summary from '$features/events/components/summary/summary.svelte';
import { getSharedTableOptions } from '$features/shared/table.svelte';
import { StackStatus } from '$features/stacks/models';
import { createTable, renderComponent } from '@tanstack/svelte-table';
import { type ColumnSizingState, createTable, renderComponent } from '@tanstack/svelte-table';

import DataTableBody from './data-table-body.svelte';

type TestSummary = EventSummaryModel<'event-error-summary'> | StackSummaryModel<'stack-error-summary'>;

interface Props {
allColumnsSized?: boolean;
fullWidthSummary?: boolean;
kind: 'event' | 'stack';
onRowClick: (row: TestSummary) => void;
sizedFullWidthSummary?: boolean;
}

let { allColumnsSized = false, kind, onRowClick }: Props = $props();
let { allColumnsSized = false, fullWidthSummary = false, kind, onRowClick, sizedFullWidthSummary = false }: Props = $props();

const summaryData = {
Message: 'Unexpected end of Stream, the content may have already been read by another component.',
Expand Down Expand Up @@ -48,6 +50,15 @@
};
const summary: TestSummary = $derived(kind === 'event' ? eventSummary : stackSummary);
const queryParameters = { limit: 20, page: 1 };

function getDefaultColumnSizing(): ColumnSizingState | undefined {
if (allColumnsSized) {
return { date: 130, summary: 140 };
}

return sizedFullWidthSummary ? { summary: 180 } : undefined;
}

const table = createTable(
getSharedTableOptions<TestSummary, 'memory'>({
columnPersistenceKey: 'row-navigation-test',
Expand All @@ -62,7 +73,11 @@
cell: (props) => renderComponent(Summary, { showStatus: false, summary: props.row.original }),
header: 'Summary',
id: 'summary',
meta: { class: 'w-60 min-w-60 max-w-60' },
meta: {
get class() {
return fullWidthSummary ? 'w-full' : 'w-60 min-w-60 max-w-60';
}
},
minSize: 120,
size: 160
},
Expand All @@ -73,7 +88,7 @@
}
],
get defaultColumnSizing() {
return allColumnsSized ? { date: 130, summary: 140 } : undefined;
return getDefaultColumnSizing();
},
enableColumnResizing: true,
paginationStrategy: 'memory',
Expand Down
Loading