From dc66493caa6ffa72da3f7fd093f4eedb82d0b08d Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 11 Aug 2026 11:27:50 +0200 Subject: [PATCH] perf: isolate board grid previews --- .../board/sections/grid/grid-editor.css | 12 +- .../board/sections/grid/grid-editor.tsx | 228 +++++++++++------- .../board/sections/grid/grid-entry-memo.ts | 4 +- .../sections/grid/grid-preview-geometry.ts | 101 ++++++++ .../sections/grid/grid-resize-outline.ts | 72 ++++++ .../grid/test/grid-entry-memo.spec.tsx | 65 ++++- .../grid/test/grid-preview-geometry.spec.ts | 86 +++++++ .../grid/test/grid-resize-outline.spec.ts | 49 ++++ 8 files changed, 517 insertions(+), 100 deletions(-) create mode 100644 apps/nextjs/src/components/board/sections/grid/grid-preview-geometry.ts create mode 100644 apps/nextjs/src/components/board/sections/grid/grid-resize-outline.ts create mode 100644 apps/nextjs/src/components/board/sections/grid/test/grid-preview-geometry.spec.ts create mode 100644 apps/nextjs/src/components/board/sections/grid/test/grid-resize-outline.spec.ts diff --git a/apps/nextjs/src/components/board/sections/grid/grid-editor.css b/apps/nextjs/src/components/board/sections/grid/grid-editor.css index 8eeb59007..89635831d 100644 --- a/apps/nextjs/src/components/board/sections/grid/grid-editor.css +++ b/apps/nextjs/src/components/board/sections/grid/grid-editor.css @@ -8,11 +8,13 @@ .board-grid-entry { z-index: 1; overflow: visible; - transition: - left 100ms ease, - top 100ms ease, - width 100ms ease, - height 100ms ease; +} + +.board-grid-entry[data-grid-preview="true"] { + width: var(--board-grid-preview-width) !important; + height: var(--board-grid-preview-height) !important; + translate: var(--board-grid-preview-x) var(--board-grid-preview-y); + will-change: translate; } .board-grid-entry[data-dnd-active="true"], diff --git a/apps/nextjs/src/components/board/sections/grid/grid-editor.tsx b/apps/nextjs/src/components/board/sections/grid/grid-editor.tsx index 6aefc96c2..8167f2946 100644 --- a/apps/nextjs/src/components/board/sections/grid/grid-editor.tsx +++ b/apps/nextjs/src/components/board/sections/grid/grid-editor.tsx @@ -1,6 +1,6 @@ "use client"; -import type { CSSProperties, PointerEvent as ReactPointerEvent, PropsWithChildren } from "react"; +import type { CSSProperties, PointerEvent as ReactPointerEvent, PropsWithChildren, RefObject } from "react"; import { createContext, memo, @@ -63,6 +63,8 @@ import { areDndGridEntryPropsEqual } from "./grid-entry-memo"; import { getGridDepth } from "./grid-depth"; import { createGridInteractionStore } from "./grid-interaction-store"; import { useBoardGridPortalHost } from "./grid-portal-host"; +import { clearGridPreviewGeometry, createGridPreviewDomState, syncGridPreviewGeometry } from "./grid-preview-geometry"; +import { createGridResizeOutlineController } from "./grid-resize-outline"; import type { CommitSectionGridInput, SectionGridPlacement } from "./use-grid-layout-actions"; import { useGridLayoutActions } from "./use-grid-layout-actions"; @@ -772,27 +774,12 @@ export default function GridEditor({ }: GridEditorProps) { const t = useI18n(); const { section, items, innerSections } = useSectionContext(); - const interaction = useBoardGridEditorGridInteraction(sectionId); const { registerGrid, getDepth, isDropTargetEligible } = useBoardGridEditorActions(); const gridRef = useRef(null); + const entryElementsRef = useRef(new Map()); const parentGridId = "parentSectionId" in section ? section.parentSectionId : null; const ownerPlacementId = "parentSectionId" in section ? section.id : null; const depth = getDepth(sectionId); - const previewGrid = interaction?.state.grids.find((grid) => grid.id === sectionId); - const controlledById = useMemo(() => new Map(placements.map((placement) => [placement.id, placement])), [placements]); - const renderPlacements = useMemo(() => { - if (!previewGrid || !interaction) return placements; - - if (interaction.sourceGridId === sectionId) { - if (interaction.mode === "resize") return previewGrid.placements; - const original = controlledById.get(interaction.activeId); - if (!original) return previewGrid.placements; - return previewGrid.placements.some((placement) => placement.id === interaction.activeId) - ? previewGrid.placements.map((placement) => (placement.id === interaction.activeId ? original : placement)) - : [...previewGrid.placements, original]; - } - return previewGrid.placements.filter((placement) => placement.id !== interaction.activeId); - }, [controlledById, interaction, placements, previewGrid, sectionId]); const labelById = useMemo( () => new Map( @@ -805,21 +792,7 @@ export default function GridEditor({ ), [innerSections, items, t], ); - const targetPlacement = - interaction?.valid && interaction.targetGridId === sectionId ? interaction.targetPlacement : null; - const growsDuringDrag = - interaction?.mode === "drag" && - (interaction.targetGridId === sectionId || - (interaction.targetGridId === null && interaction.sourceGridId === sectionId)); - const participatesInInteraction = - interaction && (interaction.sourceGridId === sectionId || interaction.targetGridId === sectionId); - const previewRowCount = getLayoutRowCount(previewGrid?.placements ?? renderPlacements); - const renderedRowCount = - maxRowCount === null && participatesInInteraction - ? Math.max(rowCount, previewRowCount) + (growsDuringDrag ? 1 : 0) - : rowCount; - - const { ref: droppableRef, isDropTarget } = useDroppable({ + const { ref: droppableRef } = useDroppable({ id: `board-grid:${sectionId}`, type: GRID_TARGET_TYPE, data: { kind: GRID_TARGET_TYPE, sectionId }, @@ -830,7 +803,6 @@ export default function GridEditor({ // through to an accepting parent grid underneath the pointer. accept: (source) => isDropTargetEligible(source, sectionId), }); - const isIntentTarget = interaction?.mode === "drag" ? interaction.targetGridId === sectionId : isDropTarget; const setGridRef = useCallback( (element: HTMLDivElement | null) => { @@ -839,6 +811,10 @@ export default function GridEditor({ }, [droppableRef], ); + const registerEntryElement = useCallback((id: string, element: HTMLElement | null) => { + if (element) entryElementsRef.current.set(id, element); + else entryElementsRef.current.delete(id); + }, []); useLayoutEffect(() => { const element = gridRef.current; @@ -855,33 +831,21 @@ export default function GridEditor({ }); }, [columnCount, depth, maxRowCount, ownerPlacementId, parentGridId, placements, registerGrid, sectionId]); - useLayoutEffect(() => { - const viewport = gridRef.current?.parentElement; - if (!viewport?.hasAttribute("data-section-id")) return; - viewport.style.setProperty("--board-grid-drag-height", `${getLogicalTrackSize(renderedRowCount)}px`); - return () => { - viewport.style.removeProperty("--board-grid-drag-height"); - }; - }, [renderedRowCount]); - return (
- {renderPlacements.map((placement) => { + {placements.map((placement) => { return ( ); })} - {targetPlacement && ( -