From ec73f1e1e7a09c3a5abac4f2c53b00b5d5577e72 Mon Sep 17 00:00:00 2001 From: Bryan Doss Date: Thu, 9 Jul 2026 21:16:06 -0400 Subject: [PATCH] fix: smooth drag-and-drop reordering of relation chips Migrate the relation-chip reorder component to the next-gen dnd-kit (@dnd-kit/react + @dnd-kit/helpers). The classic sortable strategies compute transforms assuming uniform item sizes, which stretched, mis-spaced, or snapped variable-width chips in wrapped multi-row lists. The new library measures the real DOM and FLIP-animates optimistic reordering, so wrapped chip rows reflow smoothly and land correctly. Positions are committed on drop, reusing the existing position set. - reorderable-relation-chips-dnd.tsx: rewritten on DragDropProvider/ useSortable; adds afterChips slot so the add-entity button lives inside the flex-wrapped list - chip.tsx: LinkableRelationChip accepts sortableDragHandleRef (the new API marks handles via callback ref); dots button unchanged - editable-entity-page.tsx: renders the add button via afterChips; IMAGE/VIDEO branching normalized to renderableTypeStrict - vite.config.js + vitest.setup.ts: stub ResizeObserver in the test environment; @dnd-kit/dom references it at module scope and jsdom does not implement it Co-authored-by: Juan Mardikian --- apps/web/design-system/chip.tsx | 17 +- .../reorderable-relation-chips-dnd.tsx | 236 ++++++------------ apps/web/package.json | 2 + .../entity-page/editable-entity-page.tsx | 225 +++++++++-------- apps/web/vite.config.js | 1 + apps/web/vitest.setup.ts | 12 + bun.lock | 18 ++ 7 files changed, 242 insertions(+), 269 deletions(-) create mode 100644 apps/web/vitest.setup.ts diff --git a/apps/web/design-system/chip.tsx b/apps/web/design-system/chip.tsx index 7e8675c693..1a5f376389 100644 --- a/apps/web/design-system/chip.tsx +++ b/apps/web/design-system/chip.tsx @@ -86,6 +86,8 @@ type LinkableRelationChipProps = { className?: string; disableLink?: boolean; sortableDragHandleListeners?: DraggableSyntheticListeners; + /** Callback ref marking the dots button as the drag handle (next-gen @dnd-kit/react). */ + sortableDragHandleRef?: (element: HTMLButtonElement | null) => void; truncateLabel?: boolean; children: React.ReactNode; @@ -198,6 +200,7 @@ export function LinkableRelationChip({ className = '', disableLink = false, sortableDragHandleListeners, + sortableDragHandleRef, truncateLabel = false, children, }: LinkableRelationChipProps) { @@ -216,6 +219,16 @@ export function LinkableRelationChip({ const triggerRef = React.useRef(null); const contentRef = React.useRef(null); + // Stable merged ref: an inline callback would re-run on every render (null then node), + // detaching/re-attaching the drag handle during hover/popover state updates. + const dotsButtonRef = React.useCallback( + (node: HTMLButtonElement | null) => { + triggerRef.current = node; + sortableDragHandleRef?.(node); + }, + [sortableDragHandleRef] + ); + const shouldClamp = !truncateLabel && typeof children === 'string' && children.length >= 42; const labelInner = truncateLabel ? {children} : children; @@ -305,7 +318,7 @@ export function LinkableRelationChip({