Release 0.6.0 — production sortable: drag defaults, live offsets, cross-container boards#11
Merged
Conversation
The runtime no longer passes the active item's own droppable to the collision detector, so overId can never equal activeId and drops near the source slot commit instead of silently resolving to a null move. BREAKING: custom detectors no longer receive the active id in DndCollisionInput.droppableRects.
beginDrag invalidates all cached measurements so the first refresh uses post-scroll positions instead of rects captured at registration. DndAutoScrollController gains an onScrolled callback fired after each tick; DndAutoScroll wires it to invalidate measurements and re-run collision at the current pointer, keeping overId correct while content scrolls under a stationary pointer.
The new strategy commits the move at the drop-over target, so the drop lands where the isOver highlight is instead of where the active rect center falls. Documents that verticalList, horizontalList and grid resolve from the rect center and can disagree with the highlight, and that isOver on droppable and sortable-item details reflects collision, not the committed move.
DndRuntime exposes initialActiveRect, the active rectangle measured at drag start, which stays constant for the session. The Flutter and Jaspr overlays lay out from it instead of the live activeRect, so collapsing the source slot to open a placeholder gap no longer clips the preview to zero. DndDragOverlayDetails carries both rects: activeRect stays live for collision-aware builders, initialActiveRect is the layout size.
Records the drag-default changes in ADR 0024 and the package changelogs: active item excluded from collision candidates, drag-start and auto-scroll re-measurement, SortableStrategies.dropOnOver, and drag-start overlay sizing. BREAKING: overId can no longer be the active id, overlays no longer track a live source resize, and DndDragOverlayDetails requires initialActiveRect.
Covers sortables inside a scroll view, dropping where the highlight is, building a placeholder gap, and running nested or parallel sortables on one controller. Notes on the collision page that the active item is never a candidate, and on the sortable page that dropOnOver exists for highlight-driven UIs.
Records the validation results and the remaining follow-up: the animated placeholder API, a programmatic drag API for tests, and scroll-listener re-measurement.
Sortable resolution no longer requires a drag end event. SortableDragContext carries the session, the drag phase, overId, and the end event when there is one, so the same strategy runs while a drag moves (preview) and when it ends (commit) through one code path. SortableStrategyInput and SortableMultiMoveInput now take context; their event field becomes a deprecated getter that is null during preview. Both adapters gain resolveDetails(SortableDragContext) beside moveDetailsFor, which now delegates to it.
SortablePreview reports where the active item would land if released now, resolved through the same strategy the drop will run so the preview and the committed move cannot disagree. Resolution is lazy and cached per move, so reading it from every item in a list costs one resolution. SortableScope exposes it as SortableScopeData.preview, and sortable items read it through SortableItemDetails.previewIndex and previewContainerId. Multi-container scopes do not publish a preview yet.
SortableMultiScope now resolves the same preview single-container scopes publish, using the reorder strategy of the container the dragged item came from. Container areas register their strategy with the scope through an owner-aware registry, because the strategy is configured per area while the resolution happens at the scope. SortableMultiContainerArea becomes stateful to manage that registration; its public constructor is unchanged.
SortableOffsets.verticalList and horizontalList report how far each item a move displaces should shift, so an application can open a placeholder gap without recomputing collision, direction, or measurement itself. Offsets are output only; SortableOffsets.none is the default and moves nothing. Displaced items shift by the dragged item's extent plus the list gap, which reproduces the post-move layout for variable-height lists. A displaced item needs no measurement of its own, so lazy lists stay correct; a missing rect for the dragged item yields no offsets rather than a guess.
SortableScope takes an offsetResolver, defaulting to SortableOffsets.none, and SortableItemDetails.offset reports how far each item should move to make room for the previewed drop. Offsets share the preview's cache, so a list resolves them once per move however many items read them. Applying the offset inside the item builder keeps it below the measured node on both adapters, so it cannot change a measured rectangle; a Flutter widget test and a Jaspr browser test pin that the rects stay put while offsets are applied and overId does not oscillate.
Adds ADR 0025, extends the 0.6.0 changelogs, documents the offset resolver in the Flutter README, and rewrites the website placeholder recipe around the published offsets instead of hand-built gap logic. Adds a performance smoke test for the per-move cost of live offsets, and a roadmap Phase 34 entry, correcting the stale Current State section.
The sortable catalog demo now opens a placeholder gap while dragging, using SortableOffsets.verticalList with dropOnOver so the gap and the committed move agree. Flutter adds a toggle so both modes are visible side by side; Jaspr animates the offset with a CSS transition. A gallery widget test drives a real drag and asserts the displaced row shifts up and that dropping commits the move the gap previewed.
The demo had no DndDragOverlay, so the dragged row stayed frozen in its slot while the pointer moved — it was the only Flutter demo missing one. The live gap made it obvious: neighbours slid aside while the dragged row did not move. The demo now owns a controller, passes it to the scope, and renders the dragged row in an overlay. The gallery test asserts the overlay tracks the pointer instead of resting at the source slot.
The sortable demo dimmed the dragged row to partial opacity while its neighbours slid over its slot, so the source appeared to sit under them. The floating copy already lives in the overlay, so the in-list row is now hidden while keeping its slot: the neighbours slide over one clean gap that follows the pointer. Corrects the recipe to hide the source rather than collapse it — the offsets already reclaim the slot, so collapsing would reclaim it twice. The gallery test asserts exactly one row is hidden while dragging.
The live gap already shows where the row lands, so lighting up each hovered neighbour's border was redundant noise. The border is now constant, which also avoids a width change that perturbs the item's measured size mid-drag.
…le demo Removes the soft background tint hovered neighbours got while dragging, so the live gap is the only landing cue. Hovered rows now render exactly like resting ones.
The overlay builder wrapped _TrackContent in a div that repeated the 14/18 padding _TrackContent already has and used a 2px border, so the floating copy rendered a padding layer and a pixel larger than the source and overflowed the correctly-sized overlay box. It now mirrors the in-list row's box — no extra padding, 1px border — so the copy matches the source size; the accent border and shadow still lift it.
Adds live sortable feedback to the 0.6.0 line: phase-aware resolution (SortableDragContext), a live preview for single- and multi-container scopes, and an opt-in SortableOffsetResolver for single-container gaps. See docs/decisions/0025-sortable-live-preview-and-offsets.md.
SortableMultiOffsets.verticalLists / horizontalLists report per-item shifts for a board move: a same-container move delegates to the single-list logic, and a cross-container move closes the slot the dragged item leaves in its source column and opens one at the landing index in the target column. Adds SortableMultiOffsetResolver and SortableMultiOffsetInput, mirroring the existing SortableMultiMoveResolver shape.
SortableMultiScope takes an offsetResolver (default SortableMultiOffsets.none) and feeds the resolved preview move into it, so SortableItemDetails.offset reports the shift on both same-column and cross-column drags. Offsets share the preview cache and invalidate on the same signals.
Both board demos set SortableMultiOffsets.verticalLists and animate the per-card offset, hiding the dragged card so the gap reads cleanly within and across columns. Records the contract in ADR 0026, extends the 0.6.0 changelogs and the website recipe, and adds a gallery test that drives a drag and asserts a displaced card carries an offset transform.
Adds cross-container sortable offsets: SortableMultiScope.offsetResolver and the SortableMultiOffsets built-ins open a live gap within and across columns. See docs/decisions/0026-cross-container-sortable-offsets.md.
Adaptive cross-container insertion decided before/after from the dragged card's translated centre, which depends on where the card was grabbed and which column it came from. Near the over item's centre it flickered, and the gap did not track the pointer — dropping over the lower half of a card, or over the second card, could show no gap at all. It now compares the pointer against the hovered card's midpoint: upper half inserts before, lower half after. The pointer already chose the hovered card (pointerWithin), so the whole decision runs on one stable signal. Fixes both adapters, which share this core logic.
Hovering the gap between two cards, or the trailing space in a populated column, resolved to the column container and appended at the end, so the placeholder gap vanished. The multi-container collision detector now resolves such a pointer to the nearest card, so the gap opens between the adjacent cards; an empty column still resolves to the container.
…tector SortableMultiScope only wires its multi-container collision detector into a controller it creates itself. The Jaspr board demo passed its own plain DndController, so the pointer resolved to the whole column in the gaps between cards and drops appended at the end — the gap fix never ran because the wrong detector was in use. The demo's controller now carries SortableMultiContainer.collisionDetector. Documents the requirement on SortableMultiScope.controller for both adapters, and adds a widget test that drops into the gap between two cards and asserts a placeholder gap opens.
…oller DndRuntime.collisionDetector is now settable, and SortableMultiScope installs its effective detector on whatever controller it uses — internal or app-provided. An app can pass a plain DndController() to SortableMultiScope without wiring SortableMultiContainer.collisionDetector itself; the scope's collisionDetector parameter remains the customization point. Simplifies the Jaspr board demo back to a plain controller and adds a widget test that passes a plain external controller, drops into the gap between two cards, and asserts a placeholder gap opens.
…spr columns Flutter board demo drops the custom dark gradient theme for a standard Scaffold + AppBar and Material-themed columns and cards, matching the other demos, and replaces the stale "Experimental ... (Web Only)" header with an accurate intro line. Jaspr board columns now have a fixed height and scroll their own cards, like the Flutter columns.
Adds widget tests for the layout that motivated the drag-start re-measure fix but had no coverage: a single-container SortableScope in a SliverList, and a multi-container SortableMultiScope with vertical sections in a CustomScrollView (the day-list shape from the integration report). Each scrolls the viewport, then drags and asserts the drop resolves to the post-scroll target — which fails if measurements are not refreshed at drag start.
A CustomScrollView with sticky day headers where sections reorder within a day and items drag across sections. Two sortable surfaces share one controller — a per-day SortableScope for sections and one board-wide SortableMultiScope for items — with a kind-scoped collision detector routing section vs item drags. Items carry an editable note that survives moves. Registered as a Flutter-only advanced demo outside the parity catalog (slivers have no Jaspr peer); documented in examples-standard. Widget tests cover section reorder, cross-section item moves, and note persistence.
Section cards size to their content, but the live gap is a paint-only transform, so shifting items down to open room let the last item spill past the card border. Reserve one dragged-item's height of real layout at the section tail while a foreign item previews a drop there, and drop it again otherwise. Same-section reorders stay net-zero and reserve nothing.
…slot The active item is excluded from the droppable set, so a pointer still inside its own (vacated) slot resolved over the nearest neighbour: neighbours shifted the instant an item was picked up, and a press-hold-then-release with no movement committed a swap. Resolve to no move whenever the pointer is within the active item's original rect, which stays put for the whole drag. Covers both the live preview and the commit.
Adds the late fixes to the 0.6.0 notes across all three packages: settable collision detector, SortableMultiScope wiring its detector onto any controller, pointer-based cross-container insertion, gap-to-nearest-card resolution, and the pick-up / drop-in-place no-op.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bundles the whole 0.6.0 line for
dnd_kit,dnd_kit_flutter, anddnd_kit_jaspr: the integration-feedback drag-default fixes, a live sortable preview + offsets plug-in, cross-container board offsets, and the multi-container refinements found during live testing.Drag defaults that match what users see (ADR 0024)
DndCollisionInput.droppableRectsexcludes the active id, sooverIdcan never equalactiveIdand drops near the source slot commit.beginDragmarks all measurements dirty, so a drag started in a scrolled viewport measures targets at their current positions.SortableStrategies.dropOnOver— the move commits where theisOverhighlight is.DndRuntime.initialActiveRect(fixed for the session), so previews survive a collapsed source slot.DndDragOverlaysizes from it.DndAutoScrollre-resolves collision on every tick, sooverIdfollows content moving under a stationary pointer.Live sortable preview + offsets, as an opt-in plug-in (ADR 0025)
SortableDragContext/SortableResolutionPhase: one code path for preview and commit, so the gap a UI shows equals the move it reports.SortablePreview(lazy, cached per move) reports where the active item would land.SortableOffsetResolver+SortableOffsets.verticalList / horizontalList / none(defaultnone): the library reports geometry, the app decides whether to animate. Applydetails.offsetinside the item builder.SortableStrategyInput.eventandSortableMultiMoveInput.event(now null during preview) — usecontext.Cross-container board offsets + refinements (ADR 0026)
SortableMultiOffsetResolver+SortableMultiOffsets.verticalLists / horizontalLists / none: a cross-container move closes the source column and opens the target column.SortableMultiScopetakesoffsetResolverandcollisionDetector, and installs its effective detector on whatever controller it is given — including one the app created — so an external controller ranks collisions with no manual wiring.DndController/DndRuntime.collisionDetectorare settable.Breaking behavior
beginDragre-measures all droppables.DndDragOverlaysizes from the drag-start rect;DndDragOverlayDetailsgains a requiredinitialActiveRect.DndAutoScrollinvalidates measurements each tick.Examples & docs
CustomScrollViewwith sticky day headers, sections reordering within a day and items dragging across sections.docs/plans/completed/.Testing
dart run melos run validategreen across all six packages.insertion, the pick-up / drop-in-place no-op, and sortables inside slivers.