From 8a053bbe07e2dab2739def5c98a53be64d8da5bd Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Thu, 26 Feb 2026 20:48:48 +0100 Subject: [PATCH] fix(dataset-modal): fix drag overlay shift caused by modal transform containing block (#38274) Co-authored-by: Claude Opus 4.6 --- .../hooks/useContainingBlockModifier.ts | 78 +++++++++++++++++++ .../Datasource/FoldersEditor/index.tsx | 7 +- 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 superset-frontend/src/components/Datasource/FoldersEditor/hooks/useContainingBlockModifier.ts diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useContainingBlockModifier.ts b/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useContainingBlockModifier.ts new file mode 100644 index 00000000000..2825f2dbd65 --- /dev/null +++ b/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useContainingBlockModifier.ts @@ -0,0 +1,78 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import type { Modifier } from '@dnd-kit/core'; + +/** + * Finds the nearest ancestor element that creates a containing block for + * position:fixed elements. Per CSS spec, an ancestor with transform, + * will-change:transform, filter, perspective, or contain:paint establishes + * a new containing block for fixed-position descendants. + */ +function findContainingBlockAncestor(element: HTMLElement): HTMLElement | null { + let ancestor = element.parentElement; + while (ancestor && ancestor !== document.documentElement) { + const style = window.getComputedStyle(ancestor); + if ( + (style.transform && style.transform !== 'none') || + style.willChange === 'transform' || + (style.filter && style.filter !== 'none') || + (style.perspective && style.perspective !== 'none') + ) { + return ancestor; + } + ancestor = ancestor.parentElement; + } + return null; +} + +/** + * Hook that returns a DragOverlay modifier to compensate for CSS transform + * on ancestor elements. When the FoldersEditor is rendered inside a draggable + * modal (react-draggable), the modal's transform: translate() creates a new + * containing block for position:fixed elements. dnd-kit's DragOverlay uses + * position:fixed with viewport-relative coordinates, causing it to be offset + * by the ancestor's position. This modifier subtracts that offset. + */ +export function useContainingBlockModifier( + containerRef: React.RefObject, +): Modifier[] { + const containingBlockRef = useRef(null); + + useEffect(() => { + if (containerRef.current) { + containingBlockRef.current = findContainingBlockAncestor( + containerRef.current, + ); + } + }, [containerRef]); + + const modifier: Modifier = useCallback(({ transform }) => { + if (!containingBlockRef.current) return transform; + const rect = containingBlockRef.current.getBoundingClientRect(); + return { + ...transform, + x: transform.x - rect.left, + y: transform.y - rect.top, + }; + }, []); + + return useMemo(() => [modifier], [modifier]); +} diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx b/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx index 18b5ea358b8..688adb3faee 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx +++ b/superset-frontend/src/components/Datasource/FoldersEditor/index.tsx @@ -55,6 +55,7 @@ import { FoldersContainer, FoldersContent } from './styles'; import { FoldersEditorProps } from './types'; import { useToasts } from 'src/components/MessageToasts/withToasts'; import { useDragHandlers } from './hooks/useDragHandlers'; +import { useContainingBlockModifier } from './hooks/useContainingBlockModifier'; import { useItemHeights } from './hooks/useItemHeights'; import { useHeightCache } from './hooks/useHeightCache'; import { @@ -89,6 +90,8 @@ export default function FoldersEditor({ const [showResetConfirm, setShowResetConfirm] = useState(false); const sensors = useSensors(useSensor(PointerSensor, pointerSensorOptions)); + const contentRef = useRef(null); + const dragOverlayModifiers = useContainingBlockModifier(contentRef); const fullFlattenedItems = useMemo(() => flattenTree(items), [items]); @@ -408,7 +411,7 @@ export default function FoldersEditor({ onResetToDefault={handleResetToDefault} allVisibleSelected={allVisibleSelected} /> - + - +