fix(documents): cleanup timeline drag listeners on unmount (#385)
Pointerdown attaches three document-level listeners. Without an explicit teardown, an unmount mid-drag (route change, view toggle, viewport drops below lg) left them attached and they kept writing to torn-down state cells. Wrap the cleanup in $effect's return, which Svelte 5 invokes on unmount. The listener-removal regression test pins this so the bug cannot come back silently. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -160,6 +160,13 @@ function cleanupDragListeners() {
|
||||
document.removeEventListener('pointercancel', handleDocumentCancel);
|
||||
}
|
||||
|
||||
// Strip any in-flight document listeners if the component unmounts mid-drag
|
||||
// (route change, view toggle, breakpoint drop). Without this they survive on
|
||||
// document and keep writing to torn-down state cells.
|
||||
$effect(() => {
|
||||
return cleanupDragListeners;
|
||||
});
|
||||
|
||||
function finalizeDrag() {
|
||||
if (dragStartIndex === null || dragEndIndex === null) return;
|
||||
const start = dragStartIndex;
|
||||
|
||||
@@ -389,6 +389,30 @@ describe('TimelineDensityFilter — aria-live during drag', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('TimelineDensityFilter — listener cleanup on unmount', () => {
|
||||
it('removes document pointer listeners when unmounted mid-drag', async () => {
|
||||
const removed: string[] = [];
|
||||
const realRemove = document.removeEventListener.bind(document);
|
||||
const removeSpy = vi
|
||||
.spyOn(document, 'removeEventListener')
|
||||
.mockImplementation((type: string, listener, options) => {
|
||||
removed.push(type);
|
||||
return realRemove(type, listener as EventListener, options);
|
||||
});
|
||||
|
||||
render(TimelineDensityFilter, makeProps());
|
||||
const bar = document.querySelector('[data-testid="timeline-bar"]') as HTMLElement;
|
||||
bar.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, pointerId: 1, button: 0 }));
|
||||
|
||||
cleanup();
|
||||
|
||||
expect(removed).toContain('pointermove');
|
||||
expect(removed).toContain('pointerup');
|
||||
expect(removed).toContain('pointercancel');
|
||||
removeSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TimelineDensityFilter — drag-to-select-range', () => {
|
||||
function pointerDown(el: HTMLElement) {
|
||||
const event = new PointerEvent('pointerdown', { bubbles: true, pointerId: 1, button: 0 });
|
||||
|
||||
Reference in New Issue
Block a user