Skip to content

Commit 0170047

Browse files
authored
fix: handle division by zero in column resize handler when startSize is 0 (#6215)
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
1 parent eca6341 commit 0170047

File tree

2 files changed

+113
-3
lines changed

2 files changed

+113
-3
lines changed

packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,21 @@ export function header_getResizeHandler<
8989
column.table.options.columnResizeDirection === 'rtl' ? -1 : 1
9090
const deltaOffset =
9191
(clientXPos - (old.startOffset ?? 0)) * deltaDirection
92+
const startSize = old.startSize ?? 0
9293
const deltaPercentage = Math.max(
93-
deltaOffset / (old.startSize ?? 0),
94+
startSize > 0 ? deltaOffset / startSize : 0,
9495
-0.999999,
9596
)
9697

9798
old.columnSizingStart.forEach(([columnId, headerSize]) => {
9899
newColumnSizing[columnId] =
99100
Math.round(
100-
Math.max(headerSize + headerSize * deltaPercentage, 0) * 100,
101+
Math.max(
102+
headerSize > 0
103+
? headerSize + headerSize * deltaPercentage
104+
: deltaOffset / old.columnSizingStart.length,
105+
0,
106+
) * 100,
101107
) / 100
102108
})
103109

packages/table-core/tests/unit/features/column-resizing/columnResizingFeature.utils.test.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,110 @@ describe('header_getResizeHandler', () => {
287287
document.dispatchEvent(moveEvent)
288288

289289
expect(onColumnSizingChange).toHaveBeenCalled()
290+
291+
const upEvent = new MouseEvent('mouseup', { clientX: 150 })
292+
document.dispatchEvent(upEvent)
293+
})
294+
295+
it('should allow resizing a column from zero width', () => {
296+
const table = generateTestTableWithData<TestFeatures>(1, {
297+
columnResizeMode: 'onChange',
298+
})
299+
300+
let resizingState = getDefaultColumnResizingState()
301+
table.options.onColumnResizingChange = (updater: any) => {
302+
resizingState =
303+
typeof updater === 'function' ? updater(resizingState) : updater
304+
;(table.store.state as any).columnResizing = resizingState
305+
}
306+
307+
const sizingUpdates: Record<string, number>[] = []
308+
table.options.onColumnSizingChange = (updater: any) => {
309+
if (typeof updater === 'function') {
310+
const result = updater(table.store.state.columnSizing ?? {})
311+
sizingUpdates.push(result)
312+
} else {
313+
sizingUpdates.push(updater)
314+
}
315+
}
316+
317+
const zeroSizeColumn = {
318+
...table.getAllColumns()[0],
319+
id: 'firstName',
320+
columnDef: { enableResizing: true, minSize: 0, size: 0 },
321+
table,
322+
}
323+
const header = createTestResizeHeader(table, {
324+
getSize: () => 0,
325+
getLeafHeaders: () => [
326+
{
327+
column: zeroSizeColumn,
328+
getSize: () => 0,
329+
subHeaders: [],
330+
},
331+
],
332+
})
333+
334+
const handler = header_getResizeHandler(header as any, document)
335+
handler({ type: 'mousedown', clientX: 100 })
336+
337+
const moveEvent = new MouseEvent('mousemove', { clientX: 150 })
338+
document.dispatchEvent(moveEvent)
339+
340+
const lastUpdate = sizingUpdates[sizingUpdates.length - 1]
341+
expect(lastUpdate).toBeDefined()
342+
const newSize = lastUpdate!['firstName']
343+
expect(newSize).toBeGreaterThan(0)
344+
expect(Number.isNaN(newSize)).toBe(false)
345+
346+
const upEvent = new MouseEvent('mouseup', { clientX: 150 })
347+
document.dispatchEvent(upEvent)
348+
})
349+
350+
it('should not produce NaN when startSize is zero', () => {
351+
const table = generateTestTableWithData<TestFeatures>(1, {
352+
columnResizeMode: 'onChange',
353+
})
354+
355+
let resizingState = getDefaultColumnResizingState()
356+
const resizingUpdates: any[] = []
357+
table.options.onColumnResizingChange = (updater: any) => {
358+
resizingState =
359+
typeof updater === 'function' ? updater(resizingState) : updater
360+
;(table.store.state as any).columnResizing = resizingState
361+
resizingUpdates.push(resizingState)
362+
}
363+
364+
const zeroSizeColumn = {
365+
...table.getAllColumns()[0],
366+
id: 'firstName',
367+
columnDef: { enableResizing: true, minSize: 0, size: 0 },
368+
table,
369+
}
370+
const header = createTestResizeHeader(table, {
371+
getSize: () => 0,
372+
getLeafHeaders: () => [
373+
{
374+
column: zeroSizeColumn,
375+
getSize: () => 0,
376+
subHeaders: [],
377+
},
378+
],
379+
})
380+
381+
const handler = header_getResizeHandler(header as any, document)
382+
handler({ type: 'mousedown', clientX: 100 })
383+
384+
const moveEvent = new MouseEvent('mousemove', { clientX: 150 })
385+
document.dispatchEvent(moveEvent)
386+
387+
const lastResizing = resizingUpdates[resizingUpdates.length - 1]
388+
expect(lastResizing).toBeDefined()
389+
expect(Number.isNaN(lastResizing.deltaPercentage)).toBe(false)
390+
expect(Number.isFinite(lastResizing.deltaPercentage)).toBe(true)
391+
392+
const upEvent = new MouseEvent('mouseup', { clientX: 150 })
393+
document.dispatchEvent(upEvent)
290394
})
291395

292396
it('should cleanup event listeners on mouse up', () => {
@@ -305,7 +409,7 @@ describe('header_getResizeHandler', () => {
305409
document.dispatchEvent(upEvent)
306410

307411
// Should remove mousemove and mouseup listeners
308-
expect(removeEventListenerSpy).toHaveBeenCalledTimes(4)
412+
expect(removeEventListenerSpy).toHaveBeenCalledTimes(2)
309413
expect(removeEventListenerSpy).toHaveBeenCalledWith(
310414
'mousemove',
311415
expect.any(Function),

0 commit comments

Comments
 (0)