diff --git a/packages/dragdrop/src/index.ts b/packages/dragdrop/src/index.ts index fb4349891..78ffed014 100644 --- a/packages/dragdrop/src/index.ts +++ b/packages/dragdrop/src/index.ts @@ -374,7 +374,7 @@ export class Drag implements IDisposable { let prevElem = this._currentElement; // Find the current indicated element at the given position. - let currElem = Private.findElementBehidBackdrop(event, this.document); + let currElem = Private.findElementBehindBackdrop(event, this.document); // Update the current element reference. this._currentElement = currElem; @@ -837,30 +837,51 @@ namespace Private { } /** - * Find the event target using pointer position. + * Find the event target using pointer position if given, or otherwise + * the central position of the backdrop. */ - export function findElementBehidBackdrop( - event: PointerEvent, + export function findElementBehindBackdrop( + event?: PointerEvent, root: Document | ShadowRoot = document ) { - // Check if we already cached element for this event. - if (lastElementSearch && event == lastElementSearch.event) { - return lastElementSearch.element; + if (event) { + // Check if we already cached element for this event. + if (lastElementEventSearch && event == lastElementEventSearch.event) { + return lastElementEventSearch.element; + } + Private.cursorBackdrop.style.zIndex = '-1000'; + const element: Element | null = root.elementFromPoint( + event.clientX, + event.clientY + ); + Private.cursorBackdrop.style.zIndex = ''; + lastElementEventSearch = { event, element }; + return element; + } else { + const transform = cursorBackdrop.style.transform; + if (lastElementSearch && transform === lastElementSearch.transform) { + return lastElementSearch.element; + } + const bbox = Private.cursorBackdrop.getBoundingClientRect(); + Private.cursorBackdrop.style.zIndex = '-1000'; + const element = root.elementFromPoint( + bbox.left + bbox.width / 2, + bbox.top + bbox.height / 2 + ); + Private.cursorBackdrop.style.zIndex = ''; + lastElementSearch = { transform, element }; + return element; } - Private.cursorBackdrop.style.zIndex = '-1000'; - const element: Element | null = root.elementFromPoint( - event.clientX, - event.clientY - ); - Private.cursorBackdrop.style.zIndex = ''; - lastElementSearch = { event, element }; - return element; } - let lastElementSearch: { + let lastElementEventSearch: { event: PointerEvent; element: Element | null; } | null = null; + let lastElementSearch: { + transform: string; + element: Element | null; + } | null = null; /** * Find the drag scroll target under the mouse, if any. @@ -871,7 +892,7 @@ namespace Private { let y = event.clientY; // Get the element under the mouse. - let element: Element | null = findElementBehidBackdrop(event); + let element: Element | null = findElementBehindBackdrop(event); // Search for a scrollable target based on the mouse position. // The null assert in third clause of for-loop is required due to: @@ -1240,15 +1261,25 @@ namespace Private { // native double click detection, used in e.g. datagrid editing. cursorBackdrop.style.transform = 'scale(0)'; body.appendChild(cursorBackdrop); + resetBackdropScroll(); document.addEventListener('pointermove', alignBackdrop, { capture: true, passive: true }); + cursorBackdrop.addEventListener('scroll', propagateBackdropScroll, { + capture: true, + passive: true + }); } cursorBackdrop.style.cursor = cursor; return new DisposableDelegate(() => { if (id === overrideCursorID && cursorBackdrop.isConnected) { document.removeEventListener('pointermove', alignBackdrop, true); + cursorBackdrop.removeEventListener( + 'scroll', + propagateBackdropScroll, + true + ); body.removeChild(cursorBackdrop); } }); @@ -1264,6 +1295,46 @@ namespace Private { cursorBackdrop.style.transform = `translate(${event.clientX}px, ${event.clientY}px)`; } + /** + * Propagate the scroll event from the backdrop element to the scroll target. + * The scroll target is defined by presence of `data-lm-dragscroll` attribute. + */ + function propagateBackdropScroll(_event: Event) { + if (!cursorBackdrop) { + return; + } + // Get the element under behind the centre of the cursor backdrop + // (essentially behind the cursor, but possibly a few pixels off). + let element: Element | null = findElementBehindBackdrop(); + if (!element) { + return; + } + // Find scroll target. + const scrollTarget = element.closest('[data-lm-dragscroll]'); + if (!scrollTarget) { + return; + } + // Apply the scroll delta to the correct target. + scrollTarget.scrollTop += cursorBackdrop.scrollTop - backdropScrollOrigin; + scrollTarget.scrollLeft += cursorBackdrop.scrollLeft - backdropScrollOrigin; + + // Center the scroll position. + resetBackdropScroll(); + } + + /** + * Reset the backdrop scroll to allow further scrolling. + */ + function resetBackdropScroll() { + cursorBackdrop.scrollTop = backdropScrollOrigin; + cursorBackdrop.scrollLeft = backdropScrollOrigin; + } + + /** + * The center of the backdrop node scroll area. + */ + const backdropScrollOrigin = 500; + /** * Create cursor backdrop node. */ diff --git a/packages/dragdrop/style/index.css b/packages/dragdrop/style/index.css index ff39cb920..3ff5f28a4 100644 --- a/packages/dragdrop/style/index.css +++ b/packages/dragdrop/style/index.css @@ -13,6 +13,8 @@ |----------------------------------------------------------------------------*/ .lm-cursor-backdrop { + top: 0px; + left: 0px; position: fixed; width: 200px; height: 200px; @@ -20,6 +22,20 @@ margin-left: -100px; will-change: transform; z-index: 100; + scrollbar-width: none; + -ms-overflow-style: none; + overflow: scroll; +} + +.lm-cursor-backdrop::after { + content: ''; + height: 1200px; + width: 1200px; + display: block; +} + +.lm-cursor-backdrop::-webkit-scrollbar { + display: none; } .lm-mod-drag-image { diff --git a/packages/dragdrop/tests/src/index.spec.ts b/packages/dragdrop/tests/src/index.spec.ts index 61ec4b1c3..50e17d9dd 100644 --- a/packages/dragdrop/tests/src/index.spec.ts +++ b/packages/dragdrop/tests/src/index.spec.ts @@ -611,6 +611,38 @@ describe('@lumino/dragdrop', () => { expect(backdrop.style.transform).to.equal('translate(100px, 500px)'); override.dispose(); }); + + it('should propagate scroll to underlying target', () => { + let override = Drag.overrideCursor('wait'); + const backdrop = document.querySelector( + '.lm-cursor-backdrop' + ) as HTMLElement; + + const wrapper = document.createElement('div'); + const content = document.createElement('div'); + document.elementFromPoint = (_x, _y) => { + return wrapper; + }; + document.body.appendChild(wrapper); + wrapper.appendChild(content); + wrapper.setAttribute('data-lm-dragscroll', 'true'); + wrapper.style.overflow = 'scroll'; + wrapper.style.height = '100px'; + wrapper.style.width = '100px'; + content.style.height = '2000px'; + content.style.width = '2000px'; + + backdrop.scrollTop += 400; + backdrop.dispatchEvent(new Event('scroll')); + expect(wrapper.scrollTop).to.equal(400); + + backdrop.scrollTop += 400; + backdrop.dispatchEvent(new Event('scroll')); + expect(wrapper.scrollTop).to.equal(800); + + override.dispose(); + document.body.removeChild(wrapper); + }); }); }); });