diff --git a/spec/dd-simple-integration-spec.ts b/spec/dd-simple-integration-spec.ts index 7af0cfa31..cd8c04d7f 100644 --- a/spec/dd-simple-integration-spec.ts +++ b/spec/dd-simple-integration-spec.ts @@ -233,16 +233,90 @@ describe('DD Integration Tests', () => { it('should support event listeners on DDElement', () => { const ddElement = DDElement.init(element); const callback = vi.fn(); - + ddElement.setupDraggable({}); ddElement.on('dragstart', callback); ddElement.off('dragstart'); - + // Should not throw expect(typeof ddElement.on).toBe('function'); expect(typeof ddElement.off).toBe('function'); - + ddElement.cleanDraggable(); }); }); + + describe('DDDraggable._getClipping autoscroll', () => { + let draggable: DDDraggable; + let helper: HTMLElement; + let scrollEl: HTMLElement; + + // Helper to mock getBoundingClientRect on an element + const mockRect = (el: HTMLElement, rect: Partial) => { + vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({ + top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0, x: 0, y: 0, + toJSON: () => ({}), + ...rect, + } as DOMRect); + }; + + beforeEach(() => { + draggable = new DDDraggable(element); + helper = document.createElement('div'); + scrollEl = document.createElement('div'); + document.body.appendChild(helper); + document.body.appendChild(scrollEl); + }); + + afterEach(() => { + draggable.destroy(); + helper.remove(); + scrollEl.remove(); + }); + + it('returns 0 when helper is fully inside scroll container', () => { + mockRect(helper, { top: 100, bottom: 200 }); + mockRect(scrollEl, { top: 0, bottom: 600 }); + expect((draggable as any)._getClipping(helper, scrollEl)).toBe(0); + }); + + it('returns negative value when helper is clipped above scroll container', () => { + mockRect(helper, { top: -50, bottom: 50 }); + mockRect(scrollEl, { top: 0, bottom: 600 }); + const clip = (draggable as any)._getClipping(helper, scrollEl); + expect(clip).toBeLessThan(0); + }); + + it('returns positive value when helper is clipped below scroll container', () => { + mockRect(helper, { top: 550, bottom: 650 }); + mockRect(scrollEl, { top: 0, bottom: 600 }); + const clip = (draggable as any)._getClipping(helper, scrollEl); + expect(clip).toBeGreaterThan(0); + }); + + it('returns 0 when helper is fully outside a nested (non-root) scroll container — stops nested scroll', () => { + // helper is entirely above the nested container + mockRect(helper, { top: -200, bottom: -50 }); + mockRect(scrollEl, { top: 0, bottom: 600 }); + // scrollEl is NOT the document root, so fully-outside should stop scrolling + expect((draggable as any)._getClipping(helper, scrollEl)).toBe(0); + }); + + it('continues scrolling when helper is fully above the root scroll container', () => { + const rootEl = document.scrollingElement as HTMLElement || document.documentElement; + mockRect(helper, { top: -200, bottom: -50 }); + mockRect(rootEl, { top: 0, bottom: 600 }); + // For the root container, fully-outside-above should still return a negative clipping value + const clip = (draggable as any)._getClipping(helper, rootEl); + expect(clip).toBeLessThan(0); + }); + + it('continues scrolling when helper is fully below the root scroll container', () => { + const rootEl = document.scrollingElement as HTMLElement || document.documentElement; + mockRect(helper, { top: 700, bottom: 850 }); + mockRect(rootEl, { top: 0, bottom: 600 }); + const clip = (draggable as any)._getClipping(helper, rootEl); + expect(clip).toBeGreaterThan(0); + }); + }); }); diff --git a/src/dd-draggable.ts b/src/dd-draggable.ts index a52ecb52f..fbba3779e 100644 --- a/src/dd-draggable.ts +++ b/src/dd-draggable.ts @@ -476,7 +476,8 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt const elRect = el.getBoundingClientRect(); const scrollRect = scrollEl.getBoundingClientRect(); const viewportH = window.innerHeight || document.documentElement.clientHeight; - if (elRect.bottom < scrollRect.top || elRect.top > scrollRect.bottom) return 0; // fully outside + const isRoot = scrollEl === (document.scrollingElement || document.documentElement); + if (!isRoot && (elRect.bottom < scrollRect.top || elRect.top > scrollRect.bottom)) return 0; // fully outside nested container const clippedBelow = elRect.bottom - Math.min(scrollRect.bottom, viewportH); const clippedAbove = elRect.top - Math.max(scrollRect.top, 0); if (clippedAbove < 0) return clippedAbove;