Skip to content

Release 0.6.0 - #199

Merged
justin-schroeder merged 38 commits into
mainfrom
release/0.6.0
Jun 11, 2026
Merged

justin-schroeder merged 38 commits into
mainfrom
release/0.6.0

Conversation

@justin-schroeder

@justin-schroeder justin-schroeder commented Jun 11, 2026 •

Copy link
Copy Markdown
Member

In this PR: the 0.6.0 release. Everything below landed through individually reviewed PRs on release/0.6.0, and CI is fully green (all four shards) — Chrome, Firefox, real WebKit, and mobile Chromium, 150 Playwright tests.

Core fixes (each with red→green regression coverage)

PR Fixes What
#175 — validateTransfer crash guard on undefined parents (thanks @stanislav-janu)
#178 #154's bug z-index no longer stuck at 9999 after drag (thanks @DanielASMPT)
#182 #149 #151 insert plugin respects group/accepts (built on @vexcited's #152)
#183 #170 drag handles inside shadow DOM (built on @driskull's #171/#172)
#184 #173 long-press timer no longer armed when longPress is off (mobile crash)
#185 #157 #148 dropOrSwap respects group/accepts, and accepts() fires during hover
#186 #162 event handler types carry the config generic — see breaking note
#187 #166 pointerdown no longer swallowed from React's delegated listeners
#188 #139 #96 draggable attribute synced with handles/callbacks; text selection works with drag handles
#189 #145 teardown actually runs on unmount (Vue onBeforeUnmount, React ref capture) + mid-interaction cleanup
#190 #70 animations({ easing })
#191 #169 identity-preserving remap during drags — fixes React fast-drag scrambling

Vue/types/docs/meta

CI overhaul (#193 #195 #196 #197 #198)

Every recent "cancelled" run was the same failure: Playwright's browser download hangs at 100% on Node 24.16.0, which check-latest silently pulled in (the last green run used 24.15.0). In this PR the workflow pins Node to the verified-good version, adds a browser cache plus bounded install retries, and shards the suite per browser project (~3 min each). Along the way I found that devices["Desktop Webkit"] was never a real Playwright device descriptor — that project had silently been running on Chromium since it was added. It now runs on actual WebKit, and the full suite passes on it.

⚠️ For the release notes

SortEvent/TransferEvent/DragstartEvent/DragendEvent now take a type argument (#186). Handlers written inline are inferred and unaffected; explicit annotations need SortEvent<MyItem>.

Issue tracker

51 of the 59 open issues are resolved in this release cycle (fixed, answered, or closed with an explanation). Intentionally still open: #174/#156/#131 (docs fixes merged here, awaiting deploy verification) and the roadmap trackers #8 (cloning), #30 (tree view), #25 (drop animation), #17 (Svelte), #150 (selection API).

defunkt-dev and others added 30 commits April 10, 2026 03:24
# Conflicts:
#	dist/README.md
#	dist/index.cjs
#	dist/index.cjs.map
#	dist/index.mjs
#	dist/index.mjs.map
#	dist/package.json
#	pnpm-lock.yaml
Vue 3.5's useTemplateRef() returns Ref<HTMLElement | null>, which the
previous VueElement type rejected. Widen the ref value to include null
(the runtime already waits for a non-null element before initializing)
and reuse VueElement for the parent field. Convert the Vue Test1
component to useTemplateRef so the framework e2e suite exercises it.

Co-authored-by: jf908 <2487520+jf908@users.noreply.github.com>
initDrag captured originalZIndex after already setting the element to
9999, so handleEnd wrote 9999 back and the elevated z-index persisted
on the dropped item forever. Capture the user's inline value first.

Adds a regression test asserting the inline z-index is empty again
after a completed drag.

Co-authored-by: DanielASMPT <63778582+DanielASMPT@users.noreply.github.com>
The 'number of draggable items does not match the number of values'
warning now reports both counts so the mismatch is debuggable at a
glance. Also hoists getValues so it is only called once per remap.

Co-authored-by: Carl Olsen <913047+unstoppablecarl@users.noreply.github.com>
npm, Renovate, and other tooling can now link back to the docs site
and repo from the published package (mirrors formkit/tempo#61).

Co-authored-by: Ghislain B <643976+ghiscoding@users.noreply.github.com>
The React and Vue dragAndDrop examples showed a nested config property,
but those adapters extend ParentConfig directly — config options must be
spread at the top level. Only the vanilla core API takes a nested
config object. Also removes a duplicated 'a' in the useDragAndDrop
description.

Co-authored-by: RHellenes <16990441+RHellenes@users.noreply.github.com>
…point (#182)

The insert plugin bypassed transfer validation entirely:

- processParentDragEvent adopted any hovered parent as state.currentParent
  even when moveOutside's group/accepts checks rejected it, so handleEnd
  would transfer values into lists that should never receive them.
- checkPosition treated *any* registered parent under the cursor as a
  valid hover target, keeping the insert point visible over foreign
  lists.
- moveOutside returned early when accepts() passed, skipping the insert
  point UI until a second dragover.

Extract an isValidDropTarget() helper mirroring core validateTransfer
semantics (initial parent always valid; otherwise dropZone, accepts, or
truthy group equality) and use it in both paths. The checkPosition
ancestor walk now continues past invalid parents so nested lists with
different groups don't mask a valid ancestor.

The drag() Playwright helper now honors the position field (top/bottom/
left/right/center) the same way syntheticDrag already did, which new
tests use to target insert ranges deterministically.

Fixes #149, fixes #151.

Co-authored-by: Mikkel Almonte--Ringaud <59152884+Vexcited@users.noreply.github.com>
validateDragHandle relied on querySelectorAll + elementFromPoint, which
both stop at shadow boundaries — a handle rendered inside a web
component's shadow root could never validate (#170).

validateDragHandle now takes the event and first scans
e.composedPath() for a handle match, bounded to elements inside the
draggable node, falling back to the previous coordinate hit-test.
Because native dragstart fires on the draggable element itself (its
composed path never includes the handle), handleDragstart now reuses
the validation recorded on pointerdown for the same node, only
re-validating from the event when no pointerdown was seen (Safari).

The synthetic/touch drag path already keys off pointerDown.validated,
so it picks up shadow DOM support from the same change.

Adds a shadow DOM test page and Playwright specs covering both the
positive (drag via shadow handle) and negative (no drag from item body)
paths using real event ordering: composed pointerdown on the handle,
dragstart on the draggable element.

Fixes #170.

Co-authored-by: Matt Driscoll <1231455+driskull@users.noreply.github.com>
…184)

handleLongPress armed its timeout unconditionally, so with the default
longPress: false a one second touch hold still fired the timer — setting
state.longPress, applying longPressClass, and preventDefault-ing the
stale pointerdown event. On touch devices this corrupted in-flight state
(reported as 'Cannot read properties of undefined (reading el)' on
Android Chrome) for a feature the consumer never enabled.

With longPress off, a press-and-hold is now simply a pointer that has
not moved yet.

New mobile Playwright specs: a 1.3s hold with longPress disabled stays a
no-op (no class, no errors, dragging still works afterwards) and
longPress: true continues to apply the class and drag after the hold.
The disabled spec fails red on the unfixed code (the class appears).

Fixes #173.
…op (#185)

The dropOrSwap plugin replaced the core transfer path but never called
validateTransfer, so any registered list could receive any drag: two
lists with different groups (or no groups at all) could exchange items,
and accepts() was never consulted while hovering — it only mattered
after release, when nothing could be styled.

The node/parent dragover and pointerover handlers now gate on a
canTransferTo() check (current parent and initial parent always allowed,
everything else goes through core validateTransfer). Hovering an invalid
parent clears targeting state so the subsequent drop is a no-op instead
of swapping against stale targets.

New test page and Playwright specs: same-group transfer, foreign-group
rejection (values and styling), no-group rejection, accepts() invoked
during hover and allowing transfer, and within-list sorting. Three of
the five fail red on the unfixed plugin.

Fixes #157, fixes #148.
…186)

SortEvent, TransferEvent, DragstartEvent, and DragendEvent each declared
their own generic (<T>(data) => void) instead of reusing ParentConfig's
T, so onSort/onTransfer/onDragstart/onDragend parameters were typed
against an unrelated T — accessing your item type's properties inside a
handler was a type error.

The handler types now take the config's generic (SortEvent<T>), and the
internal payload builders that leaned on the old universally-quantified
form (casting parents/state to unknown) are cast to T properly.

Adds src/types.test-d.ts, compile-time assertions checked by
tsc --noEmit: with the old declarations it fails with 8 errors; with the
fix it compiles.

Fixes #162.
handleNodePointerdown called stopPropagation as its first statement, so
pointerdown never reached document level. React (and other frameworks)
attach delegated listeners at the root, which means consumer
onPointerDown handlers inside draggable items never fired at all.

Replace stopPropagation with an event-claim: the innermost draggable
node marks the event in a WeakSet and ancestor node handlers plus the
root pointerdown handler (which clears selection state) skip claimed
events. Propagation continues to framework roots while preserving the
two behaviors stopPropagation provided — innermost-node-wins for nested
lists, and no self-deselection when pressing on a node.

Adds a React framework e2e: a real click on a button inside a draggable
item must invoke its onPointerDown (fails red with the old
stopPropagation). Multi-drag selection suites pass unchanged.

Fixes #166.
…188)

Two related desyncs between library state and the native draggable
attribute:

- With dragHandle configured, setupNode still set draggable=true on the
  whole item, so browsers disabled text selection everywhere inside it
  and items could be picked up natively from anywhere (#139). The item
  now stays draggable=false until a pointerdown on the handle validates,
  and is disarmed again on release (pointerup over the node, root
  pointerup, blur, and drag end all restore per config).

- Elements excluded by the draggable callback kept draggable=true on the
  MutationObserver remap path, allowing native drags the library would
  ignore (#96). remapNodes now clears the attribute for excluded
  elements.

The pointerup disarm is scoped to dragHandle configs and leaves
state.pointerDown set, since the focus/blur handlers rely on it to
manage draggable while editing inputs inside items (#142 — the Firefox
issue-142 spec caught a first attempt that cleared it).

New Playwright specs: handle-gated attribute arming over a real pointer
press/release cycle, real mouse text selection inside an item with drag
handles (both red on unfixed code), and draggable-callback toggling.

Fixes #139, fixes #96.
Three layers of the same lifecycle bug (#145):

- The Vue adapter registered tearDown in onUnmounted, but template refs
  are already null by then — tearDown never ran at all. Moved to
  onBeforeUnmount. The React adapter had the equivalent bug: it read
  parent.current inside the effect cleanup, after React nulls DOM refs
  on unmount. The element is now captured when the effect runs.

- tearDown itself only aborted the parent's listeners. The document
  level handlers and pending long press timer kept firing against the
  removed list: a held pointer that moved after unmount started a
  synthetic drag on a disconnected parent, where showPopover throws
  InvalidStateError ('Invalid on disconnected popover elements' — the
  exact error reported). tearDown now clears pointerDown state and its
  timer when they belong to the torn-down parent, and resets any
  in-flight drag that involves it.

- Both showPopover call sites are guarded with isConnected as a final
  backstop.

New e2e (tests/pages/teardown.vue + teardown.spec.ts): press an item,
unmount the list programmatically mid-press, move the held pointer —
must produce no errors — then remount and verify dragging works. Fails
red on unfixed code with the exact InvalidStateError from the issue.

Fixes #145.
The animations plugin hardcoded ease-in-out for every sort/transfer
animation. AnimationsConfig now accepts an easing string (any value the
Web Animations API accepts) threaded through all animate() calls,
defaulting to the previous ease-in-out.

Adds an e2e that reads the running animation's timing mid-drag and
asserts the configured cubic-bezier is applied.

Closes #70.
)

remapNodes pairs values to DOM children positionally. During a drag
that's only safe when the DOM has caught up with the values array —
React commits are asynchronous, so under fast drags with slow renders
the remap ran against stale DOM and reassigned node identities: elements
got values they don't render, state.draggedNode was re-pointed at a
different element, and subsequent sorts spliced at wrong indices,
compounding the corruption (#169).

While a drag is active, remapNodes now pairs each known node to its
existing value and derives the index from the current values array
(consumed-slot matching, so duplicate values stay 1:1). It falls back
to positional pairing whenever existing node values can't be matched
onto the current values — initial setup, transfers committing, and
wholesale value replacement keep their previous behavior. Outside of
drags nothing changes.

New React test component commits its state 50ms late (the adapter's
value store updates synchronously, the DOM lags — same shape as a slow
React render), and the new spec sorts twice inside that window. It
asserts the invariant that breaks userland: a registered node must
never carry a value other than the one it renders. On unfixed code the
probe catches an element rendering 'one' registered as 'two'; fixed,
identities hold mid-drag and the drag lands where the user hovered.

Fixes #169.
The non-React language panes are Nuxt server components mounted lazily
on tab click. On the statically deployed site there is no server to
answer the runtime island request, so the Vue/Solid/Native/Marko tabs
rendered empty (#174). All panes now render during prerender and toggle
with v-show — the highlighted HTML ships in the static page, so the
client-perf benefit of server components is preserved.

Prerendering all panes also surfaced a build-breaking error the lazy
islands had been hiding: docs/examples/config/config.ts mirrors
ParentConfig and still used the unparameterized event handler types
(TS2314 after the #186 fix) — twoslash rejected the sample and the
island 500'd. Updated to SortEvent<T> et al.

Verified: nuxt generate succeeds (71 island routes prerendered, zero
errors) and the generated index.html contains raw-code containers for
all five languages.

Fixes #174.
The suite grew past what a single serial job can finish inside the
30-minute timeout — every recent run was killed at 30m and reported as
cancelled. One matrix shard per Playwright project (Chrome, Firefox,
WebKit, mobile Chromium) keeps each job well under the limit, installs
only the browser it needs, and fails independently per engine.
Framework integration docs belong on the docs site; the README stays a
short introduction. Also drops the empty 'About this repository' stub
and points the npm version badge at drag-and-drop instead of tempo.
The browser download from the Playwright CDN intermittently stalls with
no output; without a step timeout it consumed the entire 30-minute job
(every recent run was killed there before tests even started — both the
single-job and the sharded layout). Each install attempt is now bounded
at 5 minutes with up to 3 retries, and the browser cache makes the step
a no-op on subsequent runs of the same Playwright version.
Node 24.16.0 (pulled in by check-latest) hangs Playwright's browser
download at 100% — the stream never finalizes, deterministically, for
every browser. The last green run (Apr 30) used 24.15.0 with the same
workflow. Pin it and bump deliberately with a verified run instead of
floating to whatever the runner cache serves.
The #169 regression probe imported the library module through Vite's
/@fs/ endpoint with a hardcoded absolute path from the machine it was
written on, failing instantly on CI runners. The path now resolves from
the repo root at runtime, and the probe runs as a string-evaluated
script so the dynamic import reaches the browser untransformed.
devices['Desktop Webkit'] is not a registered Playwright device
descriptor, so the spread was undefined and the project silently ran on
the default browser — chromium — since it was added. The CI shard that
installs only webkit exposed it (every launch failed wanting
chromium-headless-shell). Switch to devices['Desktop Safari'], the real
WebKit descriptor. Full suite passes on actual WebKit locally.
@vercel

vercel Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
drag-and-drop-docs Ready Ready Preview Jun 11, 2026 7:59pm

Request Review

)

Publishing is now a GitHub workflow instead of a local pnpm publish:

- Pushing a v* tag triggers publish.yml. The version comes from the tag
  (v0.6.0 -> 0.6.0, v0.6.0-dev.abc1234 -> 0.6.0-dev.abc1234) and the
  npm dist-tag is inferred from it: plain versions publish as latest
  (allowed only from main), -next.* as next, -dev.* as dev, with the
  short commit hash as the prerelease suffix.
- The full Playwright matrix runs first (ci.yml is now callable and
  gates the publish job).
- npm trusted publishing via OIDC: id-token permission + npm@latest,
  no tokens anywhere.
- Build is validated with publint and published from dist/.
- changelogithub generates GitHub release notes for latest releases.

Replaces release.yml; ci.yml no longer fires directly on tag pushes
since publish.yml invokes it.
@justin-schroeder
justin-schroeder merged commit 2c54088 into main Jun 11, 2026
10 checks passed

This branch was successfully deployed

1 active deployment
Preview — 6530a010 Deployed Jun 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants