Refactor drag selection: Replace external library with custom implementation and add auto-scroll (#12134)

Closes #12076
Closes #11764

Replaced the `@air/react-drag-to-select` library with a custom
implementation to get better control over the selection behavior and add
auto-scroll functionality.

**What changed:**
- Removed external drag selection dependency 
- Built custom drag selection from scratch using pointer events --
@charlesBochet
- Added auto-scroll when dragging near container edges
- Fixed boundary detection so selection stays within intended areas
- Added proper `data-select-disable` support for checkboxes and other
non-selectable elements

The new implementation gives us full control over the selection logic
and eliminates the external dependency while adding the auto-scroll
feature that was **not** requested 😂

**Auto Scroll**



https://github.com/user-attachments/assets/3509966d-5b6e-4f6c-a77a-f9a2bf26049f



related to #12076 


https://github.com/user-attachments/assets/2837f80e-728c-4739-a0e2-b8d7bc83a21a

**Also fixed:**
- Record board column height not extending to the bottom (styling issue
I found while working on this)

before:

<img width="1512" alt="Screenshot 2025-05-19 at 23 58 54"
src="https://github.com/user-attachments/assets/602b310f-7ef6-44f6-99e9-da5ff59b31d3"
/>

after:

<img width="1512" alt="Screenshot 2025-05-19 at 23 56 40"
src="https://github.com/user-attachments/assets/1d0ecb5c-49e0-4f03-be3b-154a6f16a7a4"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
nitin
2025-05-26 15:28:22 +05:30
committed by GitHub
parent 621a779526
commit 524a1d78d2
24 changed files with 1244 additions and 100 deletions
@@ -17,7 +17,11 @@ describe('useTrackPointer', () => {
document.dispatchEvent(event);
});
expect(onMouseDown).toHaveBeenCalledWith(150, 250);
expect(onMouseDown).toHaveBeenCalledWith({
x: 150,
y: 250,
event: expect.any(MouseEvent),
});
});
it('Should call onMouseUp when mouse up event is triggered', () => {
@@ -34,7 +38,11 @@ describe('useTrackPointer', () => {
document.dispatchEvent(event);
});
expect(onMouseUp).toHaveBeenCalledWith(200, 300);
expect(onMouseUp).toHaveBeenCalledWith({
x: 200,
y: 300,
event: expect.any(MouseEvent),
});
});
it('Should call onInternalMouseMove when mouse move event is triggered', () => {
@@ -51,6 +59,77 @@ describe('useTrackPointer', () => {
document.dispatchEvent(event);
});
expect(onInternalMouseMove).toHaveBeenCalledWith(150, 250);
expect(onInternalMouseMove).toHaveBeenCalledWith({
x: 150,
y: 250,
event: expect.any(MouseEvent),
});
});
it('Should pass the correct event object to the callback', () => {
const onMouseDown = jest.fn();
renderHook(() =>
useTrackPointer({
onMouseDown,
}),
);
act(() => {
const event = new MouseEvent('mousedown', { clientX: 100, clientY: 200 });
document.dispatchEvent(event);
});
const calledWith = onMouseDown.mock.calls[0][0];
expect(calledWith.event).toBeInstanceOf(MouseEvent);
expect(calledWith.event.type).toBe('mousedown');
});
it('Should handle touch events correctly', () => {
const onMouseDown = jest.fn();
renderHook(() =>
useTrackPointer({
onMouseDown,
}),
);
act(() => {
const touchEvent = new TouchEvent('touchstart', {
changedTouches: [
{
clientX: 120,
clientY: 180,
} as Touch,
],
});
document.dispatchEvent(touchEvent);
});
if (onMouseDown.mock.calls.length > 0) {
const calledWith = onMouseDown.mock.calls[0][0];
expect(calledWith.x).toBe(120);
expect(calledWith.y).toBe(180);
expect(calledWith.event).toBeInstanceOf(TouchEvent);
}
});
it('Should not track pointer when shouldTrackPointer is false', () => {
const onMouseDown = jest.fn();
renderHook(() =>
useTrackPointer({
shouldTrackPointer: false,
onMouseDown,
}),
);
act(() => {
const event = new MouseEvent('mousedown', { clientX: 150, clientY: 250 });
document.dispatchEvent(event);
});
expect(onMouseDown).not.toHaveBeenCalled();
});
});
@@ -1,7 +1,6 @@
import { PointerEventListener } from '@/ui/utilities/pointer-event/types/PointerEventListener';
import { useCallback, useEffect } from 'react';
type MouseListener = (positionX: number, positionY: number) => void;
export const useTrackPointer = ({
shouldTrackPointer = true,
onMouseMove,
@@ -9,9 +8,9 @@ export const useTrackPointer = ({
onMouseUp,
}: {
shouldTrackPointer?: boolean;
onMouseMove?: MouseListener;
onMouseDown?: MouseListener;
onMouseUp?: MouseListener;
onMouseMove?: PointerEventListener;
onMouseDown?: PointerEventListener;
onMouseUp?: PointerEventListener;
}) => {
const extractPosition = useCallback((event: MouseEvent | TouchEvent) => {
const clientX =
@@ -25,7 +24,7 @@ export const useTrackPointer = ({
const onInternalMouseMove = useCallback(
(event: MouseEvent | TouchEvent) => {
const { clientX, clientY } = extractPosition(event);
onMouseMove?.(clientX, clientY);
onMouseMove?.({ x: clientX, y: clientY, event });
},
[onMouseMove, extractPosition],
);
@@ -33,7 +32,7 @@ export const useTrackPointer = ({
const onInternalMouseDown = useCallback(
(event: MouseEvent | TouchEvent) => {
const { clientX, clientY } = extractPosition(event);
onMouseDown?.(clientX, clientY);
onMouseDown?.({ x: clientX, y: clientY, event });
},
[onMouseDown, extractPosition],
);
@@ -41,7 +40,7 @@ export const useTrackPointer = ({
const onInternalMouseUp = useCallback(
(event: MouseEvent | TouchEvent) => {
const { clientX, clientY } = extractPosition(event);
onMouseUp?.(clientX, clientY);
onMouseUp?.({ x: clientX, y: clientY, event });
},
[onMouseUp, extractPosition],
);
@@ -0,0 +1,9 @@
export type PointerEventListener = ({
x,
y,
event,
}: {
x: number;
y: number;
event: MouseEvent | TouchEvent;
}) => void;