From adef894589808b857d4428aa104cf10c7317b8fe Mon Sep 17 00:00:00 2001 From: Nicolas Stepien <567105+nstepien@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:41:13 +0100 Subject: [PATCH] Column resizing: handle coalesced pointer move events (#3594) --- src/HeaderCell.tsx | 30 ++++++++++++------ test/browser/column/resizable.test.tsx | 44 +++++++++++--------------- vitest.workspace.ts | 2 +- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/HeaderCell.tsx b/src/HeaderCell.tsx index e66c6338ec..6fbf3c770a 100644 --- a/src/HeaderCell.tsx +++ b/src/HeaderCell.tsx @@ -122,22 +122,37 @@ export default function HeaderCell({ const headerCell = currentTarget.parentElement!; const { right, left } = headerCell.getBoundingClientRect(); const offset = isRtl ? event.clientX - left : right - event.clientX; + let hasDoubleClicked = false; function onPointerMove(event: PointerEvent) { - const { right, left } = headerCell.getBoundingClientRect(); - const width = isRtl ? right + offset - event.clientX : event.clientX + offset - left; - if (width > 0) { - onColumnResize(column, clampColumnWidth(width, column)); + const { width, right, left } = headerCell.getBoundingClientRect(); + let newWidth = isRtl ? right + offset - event.clientX : event.clientX + offset - left; + newWidth = clampColumnWidth(newWidth, column); + if (width > 0 && newWidth !== width) { + onColumnResize(column, newWidth); } } - function onLostPointerCapture() { + function onDoubleClick() { + hasDoubleClicked = true; + onColumnResize(column, 'max-content'); + } + + function onLostPointerCapture(event: PointerEvent) { + // Handle final pointer position that may have been skipped by coalesced pointer move events. + // Skip move pointer handling if the user double-clicked. + if (!hasDoubleClicked) { + onPointerMove(event); + } + currentTarget.removeEventListener('pointermove', onPointerMove); + currentTarget.removeEventListener('dblclick', onDoubleClick); currentTarget.removeEventListener('lostpointercapture', onLostPointerCapture); } currentTarget.setPointerCapture(pointerId); currentTarget.addEventListener('pointermove', onPointerMove); + currentTarget.addEventListener('dblclick', onDoubleClick); currentTarget.addEventListener('lostpointercapture', onLostPointerCapture); } @@ -186,10 +201,6 @@ export default function HeaderCell({ } } - function onDoubleClick() { - onColumnResize(column, 'max-content'); - } - function handleFocus(event: React.FocusEvent) { onFocus?.(event); if (shouldFocusGrid) { @@ -295,7 +306,6 @@ export default function HeaderCell({
)} diff --git a/test/browser/column/resizable.test.tsx b/test/browser/column/resizable.test.tsx index bca2012c60..bc0b9675e3 100644 --- a/test/browser/column/resizable.test.tsx +++ b/test/browser/column/resizable.test.tsx @@ -10,14 +10,27 @@ interface Row { readonly col2: string; } +function queryResizeHandle(column: HTMLElement) { + return column.querySelector(`.${resizeHandleClassname}`); +} + +function getResizeHandle(column: HTMLElement) { + const resizeHandle = column.querySelector(`.${resizeHandleClassname}`); + + if (resizeHandle === null) { + throw new Error('Resize handle not found'); + } + + return resizeHandle; +} + interface ResizeArgs { readonly column: HTMLElement; readonly resizeBy: number; } async function resize({ column, resizeBy }: ResizeArgs) { - const resizeHandle = column.querySelector(`.${resizeHandleClassname}`); - if (resizeHandle === null) return; + expect(getResizeHandle(column)).toBeInTheDocument(); await act(async () => { // @ts-expect-error @@ -26,8 +39,7 @@ async function resize({ column, resizeBy }: ResizeArgs) { } async function autoResize(column: HTMLElement) { - const resizeHandle = column.querySelector(`.${resizeHandleClassname}`); - if (resizeHandle === null) return; + const resizeHandle = getResizeHandle(column); // eslint-disable-next-line testing-library/no-unnecessary-act await act(async () => { @@ -51,14 +63,10 @@ const columns: readonly Column[] = [ } ]; -test('should not resize column if resizable is not specified', async () => { +test('cannot not resize or auto resize column when resizable is not specified', () => { setup({ columns, rows: [] }); const [col1] = getHeaderCells(); - expect(getGrid()).toHaveStyle({ gridTemplateColumns: '100px 200px' }); - await resize({ column: col1, resizeBy: 50 }); - expect(getGrid()).toHaveStyle({ gridTemplateColumns: '100px 200px' }); - await resize({ column: col1, resizeBy: -50 }); - expect(getGrid()).toHaveStyle({ gridTemplateColumns: '100px 200px' }); + expect(queryResizeHandle(col1)).not.toBeInTheDocument(); }); test('should resize column when dragging the handle', async () => { @@ -86,22 +94,6 @@ test('should use the minWidth if specified', async () => { expect(getGrid()).toHaveStyle({ gridTemplateColumns: '100px 100px' }); }); -test('should not auto resize column if resizable is not specified', async () => { - setup({ - columns, - rows: [ - { - col1: 1, - col2: 'a'.repeat(50) - } - ] - }); - const [col1] = getHeaderCells(); - expect(getGrid()).toHaveStyle({ gridTemplateColumns: '100px 200px' }); - await autoResize(col1); - expect(getGrid()).toHaveStyle({ gridTemplateColumns: '100px 200px' }); -}); - test('should auto resize column when resize handle is double clicked', async () => { setup({ columns, diff --git a/vitest.workspace.ts b/vitest.workspace.ts index f735099090..bf43e7a3cd 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -5,7 +5,7 @@ import type { BrowserCommand } from 'vitest/node'; const resizeColumn: BrowserCommand<[resizeBy: number]> = async (context, resizeBy) => { const page = context.page; const frame = await context.frame(); - const resizeHandle = frame.locator('[role="columnheader"][aria-colindex="2"] div'); + const resizeHandle = frame.locator('[role="columnheader"][aria-colindex="2"] div'); const { x, y } = (await resizeHandle.boundingBox())!; await resizeHandle.hover({ position: { x: 5, y: 5 }