diff --git a/src/hooks/useScrollDrag.ts b/src/hooks/useScrollDrag.ts index 08510c5..226c555 100644 --- a/src/hooks/useScrollDrag.ts +++ b/src/hooks/useScrollDrag.ts @@ -13,6 +13,22 @@ export function getPageXY( return obj[horizontal ? 'pageX' : 'pageY'] - window[horizontal ? 'scrollX' : 'scrollY']; } +/** + * Check the element itself or any of its ancestors is draggable. + * Use the IDL attribute instead of `[draggable]` selector so that + * implicitly draggable elements (`a[href]`, `img`) are also covered. + */ +function isDraggable(ele: HTMLElement | null): boolean { + let current = ele; + while (current) { + if (current.draggable) { + return true; + } + current = current.parentElement; + } + return false; +} + export default function useScrollDrag( inVirtual: boolean, componentRef: React.RefObject, @@ -45,7 +61,9 @@ export default function useScrollDrag( const onMouseDown = (e: MouseEvent) => { // Skip if element set draggable - if ((e.target as HTMLElement).draggable || e.button !== 0) { + // Note the mousedown target is the deepest node in the event path (e.g. the + // title span inside a Tree node), while `draggable` is set on the ancestor. + if (isDraggable(e.target as HTMLElement) || e.button !== 0) { return; } // Skip if nest List has handled this event diff --git a/tests/scroll.test.js b/tests/scroll.test.js index 57ea5d5..4f7a347 100644 --- a/tests/scroll.test.js +++ b/tests/scroll.test.js @@ -819,6 +819,83 @@ describe('List.Scroll', () => { // Assert that scroll did not change after drag expect(getScrollTop(container)).toEqual(0); }); + + it('can not move when dragging a child of a draggable item', () => { + const onScroll = jest.fn(); + const { container } = render( + + {({ id }) => ( +
  • + {id} +
  • + )} +
    , + ); + + // Initial scroll should be 0 + expect(getScrollTop(container)).toEqual(0); + + // Mousedown / mousemove happen on the inner child, while `draggable` + // is set on the
  • ancestor. The child's own `draggable` is false, + // so the fix must walk up to the ancestor to skip the drag scroll. + const child = container.querySelector('.drag-child'); + fireEvent.mouseDown(child, { button: 0 }); + const moveEvent = createEvent.mouseMove(child); + moveEvent.pageY = 100; + fireEvent(child, moveEvent); + act(() => { + jest.advanceTimersByTime(100); + }); + fireEvent.mouseUp(child); + + // Should not scroll because an ancestor is draggable + expect(getScrollTop(container)).toEqual(0); + }); + + it('can not move when dragging a child of an implicitly draggable item', () => { + const onScroll = jest.fn(); + const { container } = render( + + {({ id }) => ( +
  • + + {id} + +
  • + )} + , + ); + + expect(getScrollTop(container)).toEqual(0); + + // `a[href]` is draggable by default and carries no `draggable` attribute, + // so matching on the attribute would miss it. + const child = container.querySelector('.drag-child'); + fireEvent.mouseDown(child, { button: 0 }); + const moveEvent = createEvent.mouseMove(child); + moveEvent.pageY = 100; + fireEvent(child, moveEvent); + act(() => { + jest.advanceTimersByTime(100); + }); + fireEvent.mouseUp(child); + + expect(getScrollTop(container)).toEqual(0); + }); }); it('not scroll jump for item height change', async () => {