import { useEffect, type RefObject } from 'react'; import type { FiltersMap } from '@wix/bex-core'; import type { EditableTableState } from '../../state/EditableTable/EditableTableState'; const EDGE_ZONE = 60; const MAX_SCROLL_SPEED = 32; const DRAG_THRESHOLD = 5; function computeEdgeDelta( clientPos: number, rectStart: number, rectEnd: number, ): number { if (clientPos > rectEnd - EDGE_ZONE) { const ratio = Math.min(1, (clientPos - (rectEnd - EDGE_ZONE)) / EDGE_ZONE); return ratio * MAX_SCROLL_SPEED; } if (clientPos < rectStart + EDGE_ZONE) { const ratio = Math.min(1, (rectStart + EDGE_ZONE - clientPos) / EDGE_ZONE); return -ratio * MAX_SCROLL_SPEED; } return 0; } export function useMouseSweepSelection( state: EditableTableState, containerRef: RefObject, ) { useEffect(() => { const container = containerRef.current; if (!container) { return; } const { cellInteraction } = state; const getVerticalScroller = () => state.collection.scrollableContent as HTMLElement | null; let rafId = 0; let currentDx = 0; let currentDy = 0; // Cached because cellEl is null when the pointer is in the edge zone // (outside cells), but we still need to scroll horizontally. let lastHorizontalScroller: HTMLElement | null = null; // Recorded on pointerdown — sweep only activates after drag threshold. let sweepStart: { x: number; y: number; rowKey: string; columnId: string; } | null = null; const stopAutoScroll = () => { cancelAnimationFrame(rafId); rafId = 0; currentDx = 0; currentDy = 0; }; // Capture phase + stopPropagation blocks dnd-kit's bubble-phase // listener on . Only record the start here — no startSweep // or focusCell, so existing click/edit/popover handlers are untouched. const onPointerDown = (e: PointerEvent) => { if (e.button !== 0 || e.shiftKey || e.pointerType !== 'mouse') { return; } const target = (e.target as HTMLElement).closest( '[data-row-key][data-column-id]', ); if (!target) { return; } const rowKey = target.getAttribute('data-row-key')!; const columnId = target.getAttribute('data-column-id')!; if (cellInteraction.isCellEditing(rowKey, columnId)) { return; } e.stopPropagation(); sweepStart = { x: e.clientX, y: e.clientY, rowKey, columnId }; }; const onPointerUp = () => { sweepStart = null; cellInteraction.stopSweep(); stopAutoScroll(); }; const onPointerMove = (e: PointerEvent) => { // Before sweep is active: check if drag threshold is exceeded. if (sweepStart && !cellInteraction.isSelecting) { const dx = e.clientX - sweepStart.x; const dy = e.clientY - sweepStart.y; if (Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) { return; } cellInteraction.startSweep(sweepStart.rowKey, sweepStart.columnId); container.setPointerCapture(e.pointerId); } if (!cellInteraction.isSelecting) { return; } // Resolve the cell under the pointer — handles fast mouse movement const el = document.elementFromPoint(e.clientX, e.clientY); const cellEl = el?.closest('[data-row-key][data-column-id]'); if (cellEl) { const rowKey = cellEl.getAttribute('data-row-key')!; const columnId = cellEl.getAttribute('data-column-id')!; cellInteraction.sweepTo(rowKey, columnId); } updateAutoScroll(e, cellEl ?? null); }; // Auto-scroll when near container edges. // Vertical and horizontal scroll are on separate elements: // - Vertical: collection.scrollableContent (overflowY: auto) // - Horizontal: the table's scrollWrapper from WDS (overflowX: auto) const updateAutoScroll = (e: PointerEvent, cellEl: HTMLElement | null) => { const verticalScroller = getVerticalScroller(); const hScroller = cellEl?.closest('table')?.parentElement; if (hScroller) { lastHorizontalScroller = hScroller; } const vRect = verticalScroller?.getBoundingClientRect(); const hRect = lastHorizontalScroller?.getBoundingClientRect(); const vRectTop = vRect?.top ?? 0; const headerBottom = state.tableState.headerElement?.getBoundingClientRect().bottom ?? 0; const vTopRef = headerBottom > vRectTop ? headerBottom - EDGE_ZONE : vRectTop; currentDy = vRect ? computeEdgeDelta(e.clientY, vTopRef, vRect.bottom) : 0; currentDx = hRect ? computeEdgeDelta(e.clientX, hRect.left, hRect.right) : 0; if (currentDx === 0 && currentDy === 0) { cancelAnimationFrame(rafId); rafId = 0; } else if (rafId === 0) { const scroll = () => { if (rafId === 0) { return; } if (currentDy !== 0) { getVerticalScroller()?.scrollBy(0, currentDy); } if (currentDx !== 0) { lastHorizontalScroller?.scrollBy(currentDx, 0); } rafId = requestAnimationFrame(scroll); }; rafId = requestAnimationFrame(scroll); } }; container.addEventListener('pointerdown', onPointerDown, true); container.addEventListener('pointermove', onPointerMove); container.addEventListener('pointerup', onPointerUp); container.addEventListener('lostpointercapture', onPointerUp); document.addEventListener('pointerup', onPointerUp); return () => { container.removeEventListener('pointerdown', onPointerDown, true); container.removeEventListener('pointermove', onPointerMove); container.removeEventListener('pointerup', onPointerUp); container.removeEventListener('lostpointercapture', onPointerUp); document.removeEventListener('pointerup', onPointerUp); stopAutoScroll(); cellInteraction.stopSweep(); }; }, [state, containerRef]); }