Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-safari-scroll-bottom-drag-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@dnd-kit/dom": patch
---

Fix dragged element jumping by one row in Safari when a scroll container (the page or a nested scrollable element) is scrolled to (or near) its end. Taking the source element out of the document flow shrinks the scrollable content, which clamps the scroll offset at the scroll extreme. The `Feedback` plugin now captures the scroll offsets of every scrollable ancestor before promoting the source element and restores them once the placeholder has restored the content size, keeping the drag feedback aligned with its origin.
31 changes: 30 additions & 1 deletion packages/dom/src/core/plugins/feedback/Feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getFixedPositionOffset,
getFrameTransform,
getRoot,
getScrollableAncestors,
getWindow,
isHTMLElement,
prefersReducedMotion,
Expand Down Expand Up @@ -316,6 +317,24 @@ export class Feedback extends Plugin<DragDropManager, FeedbackOptions> {

/* ---- Apply initial feedback styles ---- */

const feedbackWindow = getWindow(feedbackElement);

// Taking the source out of flow shrinks the scrollable content, so a
// container scrolled to its end has its scrollTop clamped by the browser
// (notably Safari), which leaves the fixed feedback element a row off.
// Capture each ancestor's offset so it can be restored once the placeholder
// restores the content size.
const initialScrollPositions = new Map<
Element,
{top: number; left: number}
>();
for (const ancestor of getScrollableAncestors(element)) {
initialScrollPositions.set(ancestor, {
top: ancestor.scrollTop,
left: ancestor.scrollLeft,
});
}

feedbackElement.setAttribute(ATTRIBUTE, 'true');

const transform = untracked(() => dragOperation.transform);
Expand Down Expand Up @@ -357,6 +376,17 @@ export class Feedback extends Plugin<DragDropManager, FeedbackOptions> {
}
}

// Restore the scroll offsets now that the placeholder has restored the size.
for (const [ancestor, position] of initialScrollPositions) {
if (ancestor.scrollTop !== position.top) {
ancestor.scrollTop = position.top;
}

if (ancestor.scrollLeft !== position.left) {
ancestor.scrollLeft = position.left;
}
}

/* ---- Popover promotion ---- */

if (supportsPopover(feedbackElement)) {
Expand Down Expand Up @@ -399,7 +429,6 @@ export class Feedback extends Plugin<DragDropManager, FeedbackOptions> {
const initialShape = new DOMRectangle(feedbackElement);
untracked(() => (dragOperation.shape = initialShape));

const feedbackWindow = getWindow(feedbackElement);
const handleWindowResize = (event: Event) => {
this.manager.actions.stop({event});
};
Expand Down
Loading