feat(documents): timeline date-range filter with density bars (#385) #478

Merged
marcel merged 52 commits from feat/issue-385-timeline-density-filter into main 2026-05-08 12:27:17 +02:00
2 changed files with 31 additions and 0 deletions
Showing only changes of commit 3b6b117c75 - Show all commits

View File

@@ -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;

View File

@@ -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 });