From 60f5266eec6a24f94b4aa696a2a80b2b12e4828c Mon Sep 17 00:00:00 2001 From: Choi Hyunjun <107416133+solssak@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:20:29 +0900 Subject: [PATCH] fix: handle division by zero in column resize handler when startSize is 0 When a column is resized to 0 width (with minSize: 0), the resize handler produces NaN from deltaOffset / startSize division by zero. This NaN propagates into columnSizing state, making the column permanently stuck. Two fixes applied: - Guard deltaPercentage calculation against startSize === 0 - Use absolute deltaOffset for new size when headerSize is 0, since percentage-based calculation (headerSize * deltaPercentage) always yields 0 when starting from zero Closes #6209 --- .../columnResizingFeature.utils.ts | 10 +- .../columnResizingFeature.utils.test.ts | 106 +++++++++++++++++- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts b/packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts index 5a2d2e4b86..b3bbbb0c4e 100644 --- a/packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts +++ b/packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts @@ -89,15 +89,21 @@ export function header_getResizeHandler< column.table.options.columnResizeDirection === 'rtl' ? -1 : 1 const deltaOffset = (clientXPos - (old.startOffset ?? 0)) * deltaDirection + const startSize = old.startSize ?? 0 const deltaPercentage = Math.max( - deltaOffset / (old.startSize ?? 0), + startSize > 0 ? deltaOffset / startSize : 0, -0.999999, ) old.columnSizingStart.forEach(([columnId, headerSize]) => { newColumnSizing[columnId] = Math.round( - Math.max(headerSize + headerSize * deltaPercentage, 0) * 100, + Math.max( + headerSize > 0 + ? headerSize + headerSize * deltaPercentage + : deltaOffset / old.columnSizingStart.length, + 0, + ) * 100, ) / 100 }) diff --git a/packages/table-core/tests/unit/features/column-resizing/columnResizingFeature.utils.test.ts b/packages/table-core/tests/unit/features/column-resizing/columnResizingFeature.utils.test.ts index 2ca01e15e9..3860135f7f 100644 --- a/packages/table-core/tests/unit/features/column-resizing/columnResizingFeature.utils.test.ts +++ b/packages/table-core/tests/unit/features/column-resizing/columnResizingFeature.utils.test.ts @@ -287,6 +287,110 @@ describe('header_getResizeHandler', () => { document.dispatchEvent(moveEvent) expect(onColumnSizingChange).toHaveBeenCalled() + + const upEvent = new MouseEvent('mouseup', { clientX: 150 }) + document.dispatchEvent(upEvent) + }) + + it('should allow resizing a column from zero width', () => { + const table = generateTestTableWithData(1, { + columnResizeMode: 'onChange', + }) + + let resizingState = getDefaultColumnResizingState() + table.options.onColumnResizingChange = (updater: any) => { + resizingState = + typeof updater === 'function' ? updater(resizingState) : updater + ;(table.store.state as any).columnResizing = resizingState + } + + const sizingUpdates: Record[] = [] + table.options.onColumnSizingChange = (updater: any) => { + if (typeof updater === 'function') { + const result = updater(table.store.state.columnSizing ?? {}) + sizingUpdates.push(result) + } else { + sizingUpdates.push(updater) + } + } + + const zeroSizeColumn = { + ...table.getAllColumns()[0], + id: 'firstName', + columnDef: { enableResizing: true, minSize: 0, size: 0 }, + table, + } + const header = createTestResizeHeader(table, { + getSize: () => 0, + getLeafHeaders: () => [ + { + column: zeroSizeColumn, + getSize: () => 0, + subHeaders: [], + }, + ], + }) + + const handler = header_getResizeHandler(header as any, document) + handler({ type: 'mousedown', clientX: 100 }) + + const moveEvent = new MouseEvent('mousemove', { clientX: 150 }) + document.dispatchEvent(moveEvent) + + const lastUpdate = sizingUpdates[sizingUpdates.length - 1] + expect(lastUpdate).toBeDefined() + const newSize = lastUpdate!['firstName'] + expect(newSize).toBeGreaterThan(0) + expect(Number.isNaN(newSize)).toBe(false) + + const upEvent = new MouseEvent('mouseup', { clientX: 150 }) + document.dispatchEvent(upEvent) + }) + + it('should not produce NaN when startSize is zero', () => { + const table = generateTestTableWithData(1, { + columnResizeMode: 'onChange', + }) + + let resizingState = getDefaultColumnResizingState() + const resizingUpdates: any[] = [] + table.options.onColumnResizingChange = (updater: any) => { + resizingState = + typeof updater === 'function' ? updater(resizingState) : updater + ;(table.store.state as any).columnResizing = resizingState + resizingUpdates.push(resizingState) + } + + const zeroSizeColumn = { + ...table.getAllColumns()[0], + id: 'firstName', + columnDef: { enableResizing: true, minSize: 0, size: 0 }, + table, + } + const header = createTestResizeHeader(table, { + getSize: () => 0, + getLeafHeaders: () => [ + { + column: zeroSizeColumn, + getSize: () => 0, + subHeaders: [], + }, + ], + }) + + const handler = header_getResizeHandler(header as any, document) + handler({ type: 'mousedown', clientX: 100 }) + + const moveEvent = new MouseEvent('mousemove', { clientX: 150 }) + document.dispatchEvent(moveEvent) + + const lastResizing = resizingUpdates[resizingUpdates.length - 1] + expect(lastResizing).toBeDefined() + expect(Number.isNaN(lastResizing.deltaPercentage)).toBe(false) + expect(Number.isFinite(lastResizing.deltaPercentage)).toBe(true) + + const upEvent = new MouseEvent('mouseup', { clientX: 150 }) + document.dispatchEvent(upEvent) }) it('should cleanup event listeners on mouse up', () => { @@ -305,7 +409,7 @@ describe('header_getResizeHandler', () => { document.dispatchEvent(upEvent) // Should remove mousemove and mouseup listeners - expect(removeEventListenerSpy).toHaveBeenCalledTimes(4) + expect(removeEventListenerSpy).toHaveBeenCalledTimes(2) expect(removeEventListenerSpy).toHaveBeenCalledWith( 'mousemove', expect.any(Function),