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:
+82
-3
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+7
-8
@@ -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],
|
||||
);
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export type PointerEventListener = ({
|
||||
x,
|
||||
y,
|
||||
event,
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
event: MouseEvent | TouchEvent;
|
||||
}) => void;
|
||||
Reference in New Issue
Block a user