feat(useDragDrop): add headless drag-and-drop primitive#225
Merged
Conversation
|
commit: |
744c7a3 to
9602b21
Compare
9602b21 to
b2ebe01
Compare
New composable in `packages/0/src/composables/createDragDrop` providing the foundation for drag-and-drop interactions across v0. Owns two registries (draggables and zones) plus active-drag state, with pluggable pointer and keyboard transports as defaults. Lifecycle hooks (onBeforeStart, onMove, onBeforeDrop, onDrop, onCancel) and a plugin array provide three independent extension points without forking. - Two internal `createRegistry` instances with `events: true` for late-mount reactive sync; tickets carry attrs / isDragging / isOver / willAccept / indicator. - Default `pointerTransport` (mouse / touch / pen via Pointer Events) and `keyboardTransport` (Space / Enter / arrows / Escape) run in parallel. - Hit-testing via `document.elementFromPoint` + ancestor walk; optional list orientation opts into automatic index/indicator computation. - Deprecated ARIA (`aria-grabbed`, `aria-dropeffect`) intentionally omitted; consumers wire an `aria-live` region for announcements. - Maturity: draft, since: null, category: system. Promotion to preview blocks on the first downstream consumer.
…export The factory now calls the underlying createContext provide internally when invoked inside a setup() scope. Public surface shrinks to createDragDrop + useDragDrop, matching v0's compound-component convention where the provide call is internal (TabsRoot precedent). Also polish the basic docs example with hover affordances: - cursor-grab on items, cursor-grabbing while dragging - wrapper applies cursor-grabbing during drag so destination zones inherit the cursor instead of falling back to the default arrow - destination zone shows ring + tinted bg when accepting an active drag
…rdening Inspect-fix loop landed nine findings across the public-API typing, lifecycle-chain coverage, and v0-convention compliance: - Distribute ActiveDrag, DraggableTicketInput, DraggableTicket over their K parameter so a discriminated-union K narrows drag.value when the consumer narrows drag.type. - Make useDragDrop generic so sub-components can recover K at the inject boundary instead of receiving DragDropContext<DragType>. - Drop with no over-zone now routes through the cancel chain (zone.onLeave → draggable.onCancel → global.onCancel) instead of silently clearing active. - indicator getter switched from toRef to computed so reactive reads are cached — getBoundingClientRect on every child of every zone is too expensive to recompute per access. - Import section order corrected in the entry and adapter files (Globals → Composables → Adapters → Utilities → Types → Re-exports). - Replaced raw Array.isArray and typeof === 'function' with isArray and isFunction from #v0/utilities. Tests: cover zone-change onLeave/onEnter, drop-no-zone fires onCancel, and `transports: []` actually disables both default transports. 19 tests passing, typecheck clean, lint clean.
…tion The composable example was in one file with DragItem and DropList inlined as defineComponent + h() calls. That hides the API surface the docs are meant to demonstrate and breaks the multi-file convention in .claude/rules/docs.md (Composable Page Structure: ::: example with 2+ files; supporting Vue files in PascalCase). Split into: - DragItem.vue — registers itself as a draggable, binds attrs - DropList.vue — registers itself as a zone, emits move events - basic.vue — entry: owns data, calls createDragDrop, wires moves Docs page rewrites the ## Examples block with multi-paragraph prose explaining what the example demonstrates, when to reach for it, and how to extend it, followed by the file-role table.
Per .claude/rules examples-prefer-data-attr-selectors and the use-virtual-focus / Checkbox precedents, drive state styling off the data-attrs the composable already exposes via attrs.value rather than JS-conditional :class bindings. - DragItem: cursor-grab base; data-[dragging]:cursor-grabbing + data-[dragging]:opacity-50 for the lift-up feedback - DropList: surface base; data-[accepts]:* for the primary-tinted border / bg / ring (data-accepts is set only when isOver && willAccept) - basic.vue wrapper: :data-dragging on the wrapper plus data-[dragging]:cursor-grabbing so the cursor stays consistent across zones during a drag The conditional :class branches are gone — every state read flows through the composable's attrs spread (v-bind="zone.attrs.value" / "ticket.attrs.value") and the static class string.
position.index is undefined when the zone has no children for resolveDropPosition to bin against — empty-list drops were silently returning without emitting. Falls back to items.length so the dragged item lands at the end.
…ones When a zone declares orientation but has no children to bin against, resolveDropPosition returns null. Previously this left position.index undefined and pushed the empty-zone fallback onto every consumer. Default to 0 — the only sensible drop position when there's nothing to splice between — so consumer onDrop callbacks can splice without their own fallback. Indicator stays null (nothing to point at). Drops the parallel ?? items.length workaround in the docs example.
Add flex-wrap to the basic example wrapper and switch each DropList from flex-1 (basis 0) to basis-48 grow so the two lists sit side-by-side at wide widths and stack when the example container narrows below ~25rem.
…fence Per .claude/rules/docs.md composable section structure, Usage is a short prose sentence followed by a collapsible code fence — not a multi-paragraph explanation. The expanded prose belongs in the Examples block (where it already lives).
… sections Per .claude/rules/new-feature-checklist.md the `system` category is exclusively for `use*` browser-API wrappers. Stateful UI-pattern primitives (createBreadcrumbs, createOverflow) live in `utilities`. Moving createDragDrop to match. Doc expansion to match the composable-page structure rule: - Architecture mermaid showing the factory's three pieces of state, pluggable transports, hook chain, and child registration paths - Transports section with the built-ins table, default-replacement semantics, and the custom-transport interface contract - Reactivity table covering every consumer-facing ref / shallow-ref - Recipes block (multiple drag types, vetoing drops, custom transports) - Accessibility section (list-of-lists ARIA convention, deprecated-attr rationale, roving-tabindex pointer) - FAQ block: native HTML5 DnD tradeoff, position.index semantics, K parameter narrowing, dual-role elements, plugin extension points
…ention The v0 ecosystem already has the adapter pattern documented in composables.md and implementation.md, used by useDate, useLocale, useLogger, useStorage, useNotifications, and createDataTable (ClientAdapter, ServerAdapter, VirtualAdapter). Inventing 'transport' just for createDragDrop fragments the vocabulary for no semantic gain. Renames: - DragDropTransport → DragDropAdapter - TransportEmit → DragDropAdapterEmit - pointerTransport → pointerAdapter - keyboardTransport → keyboardAdapter - PointerTransportOptions → PointerAdapterOptions - KeyboardTransportOptions → KeyboardAdapterOptions - options.transports → options.adapters - ## Transports → ## Adapters (docs section) The 'via' discriminator on ActiveDrag stays as 'pointer' | 'keyboard' strings — those describe the input mechanism, not the implementation type. Custom adapters declare their own 'via' value via Extensible.
… system Drops the auto-provide DI plumbing (createContext tuple, useDragDrop inject wrapper, hasInjectionContext branch) — the composable is now a plain DOM-coupled hook in the `system` category alongside usePopover, useRovingFocus, etc. Examples thread the context to children via an explicit `:dnd` prop. If subtree DI is needed later (e.g. for a future DropZone component), it can be added additively as a separate `createDragDropContext()` companion mirroring `createSelectionContext`.
Spec required a forward-link to consumers who relied on the auto-provide pattern; explains that wrapping useDragDrop() with their own provide/inject pair is the manual workaround until a createDragDropContext() trinity factory lands.
…ness fixes
Restructure pointer/keyboard adapters as PascalCase classes extending
an abstract DragDropAdapter base, mirroring createDataTable. Fold emit
into DragDropAdapterContext; rename install/uninstall to setup/dispose
to match v0's other adapters. Replace raw document.addEventListener
with useEventListener.
Naming: handle* -> on*, ctx -> context, *Ref suffix dropped from
ticket-locals, inline buildDraggableAttrs/buildZoneAttrs into toRef
getters, drop willZoneAcceptById and fireZoneHook helpers. Type-guard
hygiene: === null -> isNull, accept(drag) === true -> Boolean(...).
Correctness: cancel chain accepts a reason ('cancel' | 'reject') so
drop rejection is distinguishable from user cancel; in onMove, fire
onLeave/onEnter before the per-callback onMove so consumers see a
coherent over-state; dispose both registries on scope teardown.
Tickets now expose state and methods only; consumers wire DOM attributes themselves. data-draggable / aria-roledescription / data-dragging / data-dropzone / data-over / data-accepts / touch-action all move from the composable's toRef builders to the example templates. Headless-first — the composable shouldn't pre-decide attribute keys that the design-system layer may want to control. Docs accuracy pass: rename remaining lowercase pointerAdapter / keyboardAdapter references in the architecture mermaid, FAQ, and accessibility section to PascalCase classes; document the new onCancel(drag, reason: 'cancel' | 'reject') signature in the vetoing recipe; add Methods sub-section for dnd.cancel(); add DOM attributes sub-section listing the canonical wiring (since the composable no longer ships it); fix the architecture intro state count; replace "arrow keys to navigate" with the accurate "nudge by step px"; show position arg in the recipe onDrop signature.
Trim Usage prose to one sentence and remove the in-fence surface listings that duplicated the Reactivity table. Multi-line the ticket register for symmetry with the zone register. Add `collapse` to the ::: example block so the file list folds by default. Rename multi-word identifiers to single-word per style.md: moveCard (dangling reference) replaced with an inline comment showing the onDrop arg shape; showRejectionToast → notify; MyWebXrAdapter / MyCustomAdapter / MyAdapter → TouchAdapter (consistent canonical example); autoScroll / flipAnimations → scroll / flip in the FAQ.
Match sibling pages: 'ts collapse no-filename' directive, declare
useTemplateRef('el') and primitive value, console.log the expected
output. The dangling 'Card' interface and 'card' identifier are gone;
the fence now compiles and runs standalone.
…rule Switch the Usage code fence from a synthetic ts snippet to a Vue SFC matching usePopover's convention — separate template refs for draggable and dropzone, real DOM bindings showing where ref="..." goes, and the canonical data-draggable / aria-roledescription / data-dropzone attributes consumers must wire. Add the playground directive so readers can edit it in-browser. Frame the multi-type recipe with a "default to a single type per scope; widen K only when types interact" overview before the union example, since the existing prose jumped straight to the union case.
…icator Sweep multi-word identifiers in index.ts: cancelDrag → bail, dropPositionFor → position, findZone → at, getDraggable / getZone inlined at call sites, willZoneAccept → accepts, overZoneId → over, baseDraggables / baseZones → dragRegistry / zoneRegistry, previousOver → previous. Replace registerDraggable / registerZone outer functions with method shorthand on the spread context objects so no multi-word function names are exposed. Adapter base method findTicket → locate (single-word verb matching the v0 adapter vocabulary — date, t, can, generate, filter, etc.). Arrow handlers in pointer / keyboard adapters stay arrows to avoid this-aliasing eslint conflicts; the find → locate rename was the only path that compiled clean without disables. Merge indicator.ts into index.ts as a private resolveDropPosition helper. ResolvedPosition becomes private; the 6 unit tests that directly exercised resolveDropPosition are dropped — coverage rolls up to the existing onDrop integration tests that flow position.index through the cancel/drop chain. Sibling DragItem.vue / DropList.vue: swap import order so vue imports precede @vuetify/v0 imports per project convention.
Architectural — eliminate cast sprawl: Widen registry generic to <Input, Input & Ticket> so .get() returns the intersection directly. Removes 8 (registry.get(id) as Input & Output) casts at lifecycle call sites. Drop distributivity from DraggableTicket (not load-bearing for narrowing); keep it on ActiveDrag and DraggableTicketInput where correlated narrowing is the headline feature. Behavioral correctness: - Implement `disabled` on draggables (skip start) and zones (skip in hit-test); previously declared but never honored. - Wrap consumer hooks in safeCall helper; ensure active.value clears in finally so a throwing hook can't wedge the drag forever. - Guard adapter setup() against re-entry — replace cleanup, log warn. - Fire cancel chain in onScopeDispose if drag is active so consumer rollback runs before teardown. Wrap each disposer call so one throw doesn't skip registry dispose. - Listen to unregister:ticket on both registries; cancel orphaned drag if source unregisters mid-drag, clear over field if zone disappears. - KeyboardAdapter checks ticket.el.value for null with a useful warn, no more cryptic getBoundingClientRect on null. Naming + JSDoc: - dragRegistry/zoneRegistry -> _draggables/_zones (underscore-mirror). - register(input) -> register(registration) (createSelection precedent). - Add @module JSDoc to all 4 adapter files; @example blocks on the useDragDrop factory and on PointerAdapter / KeyboardAdapter classes. - Use the ./adapters barrel from the parent index.ts. - DragDropAdapterEmit type -> interface for object-contract consistency. - childRects -> rects; i,r -> index,rect inside resolveDropPosition. Tests +5: drop happy-path, empty oriented zone returns index:0, disabled zone is skipped in hit-test, onLeave/onEnter fires before per-callback onMove, adapter dispose runs on scope teardown.
Per feedback_name_composable_instance: don't chain method calls on use*() invocations. Hoist `const logger = useLogger()` to the top of the useDragDrop factory and to the top of each adapter setup() so the 10 scattered useLogger().warn / useLogger().error calls become single references. Also multi-line the keyboard adapter's emit.start call for readability.
Every type-level @example now starts with `import { useDragDrop } from '@vuetify/v0'` plus a factory instantiation so the snippet renders as runnable in the docs API page. Previously the bodies referenced an undefined `dnd`. Affects 10 types: DropIndicator, DropPosition, ActiveDrag, DraggableTicketInput, DraggableTicket, DropZoneTicketInput, DropZoneTicket, DragDropPlugin, DragDropOptions, DragDropContext.
…vers The indicator computed previously called getBoundingClientRect on every child of the over-zone every time active.value changed (~60 Hz during a pointer drag). The old comment claimed caching but active.value is a tracked dep, so the cache invalidated every move. Replace with a real cache: a per-zone shallowRef<DOMRect[]> refreshed only when useResizeObserver fires (zone resized) or useMutationObserver fires with childList changes (children added / removed). Pointer moves now do an O(N) scan against cached rects instead of O(N) layout queries. position() drops its own duplicate rect collection — reads the zone's cached indicator directly. Empty-oriented-zone fallback (index 0) is preserved. For sortable lists with many items the win is meaningful; for small lists it's the same shape but no longer accidentally O(N) layout.
Adapters section restructured: PointerAdapter and KeyboardAdapter each get their own H3 with an option table (threshold for pointer; activate + step for keyboard). Replacing-the-defaults and Custom-adapters move into subsections so the abstract-class extension example is part of the adapter story rather than orphaned. The Recipes "Custom adapters?" recipe is now redundant — the new Adapters subsections cover both how to define a custom adapter (via the abstract base) and how to use it (via adapters: [new MyAdapter()]). Drop the recipe entirely along with its trailing question mark. Reactivity: add the success-check icon to the "every state field is reactive" header line so the visual cue matches sibling pages (createSelection, createStep). Correct the stale claim that getBoundingClientRect runs per active.value change — after the zone-rects cache landed, it runs only on resize / child mutation.
H1: per-zone useResizeObserver / useMutationObserver bound to the
calling component's scope, not the zone registration's lifecycle. If
a consumer called zone.unregister() while the component stayed mounted
(realistic in dynamic kanban use cases), the observers leaked. Wrap
per-zone observer setup in a dedicated effectScope() and stop it on
the matching _zones unregister:ticket event.
M1: useMutationObserver(childList: true) doesn't fire on initial
attach (spec); useResizeObserver fires async after attach. Populated
oriented zones could see rects.value === [] on the first pointer move
inside the same microtask. Prime rects synchronously via
watch(el, refresh, { immediate: true, flush: 'post' }) so the first
move sees a populated cache.
L1: drop `=== true` from the disabled gate in onStart() — every
sibling composable uses bare-truthy `if (toValue(disabled))`.
L2: add @example blocks to DraggablesContext and ZonesContext that
slipped through the recent self-contained-example pass.
M2: comment the invariant that registry dispose() clears listeners
before clear:registry fires, so the unregister:ticket handlers are
silent during factory teardown — the hot-drag teardown relies on
cancel() running first.
…ecks Pressing Space or Enter anywhere on the page was silently calling preventDefault even when no draggable was focused and no drag was active, blocking page scroll, button activation, and form submit. Move the preventDefault inside the isActive and ticket branches so default behavior only suppressed when the keypress is genuinely consumed by a drag.
…cleanup - Wrap exposed `active` ref in shallowReadonly so the Readonly<ShallowRef> type contract is enforced at runtime, not just at the type level. - Track per-zone observer scopes in a `book` Map and stop them in the factory's onScopeDispose, plugging the listener-clear race in _zones.dispose() that previously orphaned observer scopes on registry mass-clear. - Replace the O(zones * depth) zone walk inside at() with a `nodes` Map keyed on element, kept in sync via per-zone watchers and onWatcherCleanup. Pointer moves now do O(depth) lookups against the cache. - Replace `(accept as string[]).includes(...)` with a typed `includes` through K['type']. - Improve DropIndicator @example to demonstrate consuming index/edge/rect rather than just restating the access path. - Rewrite test mutations through a CaptureAdapter pattern instead of `(dnd.active as any).value = ...`, plus add coverage for cancel chain, accepts predicate false path, oriented indicator math, pointer threshold, custom keyboard step/activate keys, double-setup warning, and throwing-hook recovery. - Promote maturity to preview now that implementation, tests, and docs have all landed.
The exposed `query` ref was typed Readonly<ShallowRef<string>> but only through a cast — the underlying ref was still writable at runtime, and TypeScript's Readonly contract was a lie. Wrap with shallowReadonly so the runtime enforces the same contract the type advertises. Sibling fix to the same anti-pattern caught in useDragDrop's `active` ref.
Add a built-in adapters overview table at the top of the Adapters section (PointerAdapter, KeyboardAdapter, DragDropAdapter base) per the adapter-section docs convention. Wrap the existing reactive-fields table under a Reactive fields subheading so the Reactivity section can grow additional subsections later.
Add a third adapter-naming pattern alongside the Vuetify0-prefix default and the third-party-branding rule: input-source adapters (useDragDrop's PointerAdapter and KeyboardAdapter) ship as defaults but take their name from the input modality, not from a brand, because multiple are installed simultaneously rather than swapped. Keeps the existing useDragDrop names in line with the rule.
- safeCall reads as a defensive call; next reads as "advance the lifecycle chain" which is what each invocation actually does. Rename the local ActiveDrag draft in onMove from `next` to `draft` to free the name and match the existing convention in onStart. - Narrow the Partial<Input & Output> register casts to Partial<Input & RegistryTicket> — matches createRegistry's actual parameter shape and drops a redundant intersection.
- Rename the discriminated-type generic K to Z across the composable and
adapters to match the convention used by every other registry-based
composable in v0 (createModel, createSelection, createForm, …).
- Convert DraggablesContext and ZonesContext from `type X = Omit<...> & { ... }`
to `interface X extends Omit<...> { ... }` to match the existing pattern
in createModel, createSelection, createNested, useFeatures, useTheme,
useStack, etc.
- Rename `decorated` to `input` at both register sites; the local IS the
input being passed to the registry.
- Pass `true` as onScopeDispose's second argument so the factory's DOM /
observer teardown is deferred (matches createObserver, useHotkey,
useMediaQuery, useToggleScope, useLazy).
- Strip inline comments that restated the code; keep only the two that
document non-obvious design choices (the empty-zone index-0 default and
the "keep drag alive when zone unmounts" decision).
…nregister - onMove computes willAccept against the new draft, not the stale active.value - PointerAdapter tracks downId to ignore secondary pointers (multi-touch overwrote downSource) - KeyboardAdapter preventDefault only when an Arrow branch actually matched - Zone unregister fires the departing zone's onLeave before clearing over/willAccept - locate() narrows EventTarget via instanceof Element instead of an unsound cast
- DragItem: tabindex="0", role="listitem", onBeforeUnmount(() => ticket.unregister()) - DropList: role="list", onBeforeUnmount(() => zone.unregister()) - basic: live region (role="status" aria-live="polite") that announces moves to screen readers The previous example shipped non-focusable draggables (keyboard adapter could not pick them up) and never deregistered on unmount, leaking registry tickets. The list-of-lists ARIA semantics now match what the page's Accessibility section calls for.
Source was renamed K→Z in 1368086; the docs page lagged. KanbanTypes (a user type literal in the recipe) is left untouched.
The Cleanup table previously labeled onScopeDispose(cleanup, true) as "deferred"; per Vue 3.5+ the second arg is failSilently and only suppresses the no-active-scope dev warning. Updates the row to reflect the actual semantics so callsites that pass true (useTheme, useLazy, useHotkey, useToggleScope, etc.) read correctly.
b395a60 to
c826ed3
Compare
`resolveDropPosition` now resolves coords landing in margin/padding/gap between children to the nearest boundary, instead of falling through to "after last" — the latter mis-positioned drops in any layout with `gap`, `margin`, or padding between items. `KeyboardAdapter` no longer hijacks Space/Enter inside `<input>`, `<textarea>`, `<select>`, `[contenteditable]`, or ARIA textbox/searchbox/ combobox/spinbutton, and ignores activation keys held with any modifier (Ctrl/Shift/Alt/Meta) — both regressions for draggables containing editable content or apps with their own keyboard shortcuts.
…Via, rename scopes - Drop `DragDropAdapterInterface` per the rule that every adapter is an abstract class only (no parallel `XxxAdapterInterface`); `DragDropAdapter` is now the sole contract. `DragDropOptions.adapters` types as `DragDropAdapter<Z>[]`. - Drop internal `ResolvedPosition` (structural duplicate of public `DropIndicator`). - Extract `DragVia = Extensible<'pointer' | 'keyboard'>` type alias; reuse it across `ActiveDrag.via`, `onStart`'s parameter, and `DragDropAdapterEmit.start`. - Rename `book` → `scopes` (Map<ID, EffectScope>); wrap dispose loop in try/catch + logger.error to match the surrounding `_draggables.dispose()` / `_zones.dispose()` defensive pattern. - Use `isElement` from `#v0/utilities` instead of raw `instanceof Element`. - `vetoed` now identifies which hook threw (zone vs options) in the log message instead of hardcoding 'onBeforeDrop'. - Replace `context.active.value!` non-null assertion with explicit guard. - Add inline comment on each `this`-using arrow handler in pointer/keyboard adapters explaining why arrow form is required (lexical `this`). - Reorder import sections in adapter files per perfectionist/sort-imports. - Test: migrate `CaptureAdapter` to `extends DragDropAdapter`; spy on `console.error` in throwing-hook test to satisfy the zero-stderr policy.
Picks up DocsApiIndex / DocsApiSearch in the components manifest, and drops the now-unemitted `_ExtractParamParserType` import from the typed router declaration.
`bail()` and `onDrop` now clear `active.value` BEFORE firing hooks. The prior `try/finally` left the drag live during hook invocation, so a hook calling `dnd.cancel()` or `ticket.unregister()` (which triggers the unregister-handler's own `cancel()` call) re-entered `bail()` with the same drag and double-fired the cancel chain. `safeAccept` now detects when an `accept` function returns a Promise and rejects with a logger.warn. `Boolean(Promise)` is always `true`, so an `async` accept predicate would silently approve every drop regardless of the eventual resolved value. Inlining the prior module-level `accepts` helper into `safeAccept` puts the predicate call inside the logger's scope so the warning can fire from the right place. Hooks fired by the cancel/drop chains now read `drag` (the captured snapshot) as the source of truth; `context.active.value` is already cleared at hook-fire time.
…DragType example
- `DraggableTicket<Z>` is now a distributive conditional (`Z extends
DragType ? RegistryTicket & {...} : never`), matching the existing
shape of `DraggableTicketInput<Z>`. Previously a plain interface — for
union `Z`, `type` and `value` widened independently to the cross-product
rather than carrying per-member correlation. Now `if (ticket.type ===
'card') ticket.value` narrows to the matching member's `value`.
- Disposer teardown switches to LIFO via `disposers.toReversed()`. Plugins
installed on top of adapters tear down before the adapters they hooked.
- `DragType` interface now has an `@example` showing the discriminated-
union extension pattern, per the per-symbol `@example` rule.
…ivity row for active.via - Accessibility section gains a "Post-drop focus" recipe explaining the `nextTick` + refocus-by-id pattern, branched on `active.value.via === 'keyboard'` so pointer drags are unaffected. - Adapters section names `DragVia` directly in prose instead of paraphrasing the underlying `Extensible<...>` shape. - Reactivity table gains a `dnd.active.value.via` row pointing readers at the discriminator they need for keyboard-only branching.
`safeAccept`'s `instanceof Promise` check missed thenables from polyfilled or third-party promise libraries (q, bluebird, et al.) — those fell through to `Boolean(result)` and silently approved the drop. Switched to a duck-typed `.then` check that catches any thenable. Also documented the synchronous-only contract on `accept`'s field JSDoc.
…ntract The post-drop focus recipe read `active.value.via` — but `active.value` is `null` inside `onDrop` after the cleared-before-notify fix, so the recipe would have thrown `Cannot read 'via' of null`. Switched to `drag.via` (the first argument to `onDrop`). Added a TIP to the Vetoing drops section making the hook contract explicit: hooks fire after `active.value` is cleared, so consumers must read the `drag` argument rather than re-reading the reactive ref. Also noted the synchronous-only restriction on `accept` predicates.
Replaces raw `result !== null && typeof result === 'object'` with the
`isObject` utility per the project's hard rule on type guards. Narrowing
via `isObject` also drops the `as { then?: unknown }` cast.
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.
Summary
useDragDropinpackages/0/src/composables/useDragDrop/— a headless drag-and-drop primitive that owns two registries (draggables, zones) plus active-drag state, with pluggable pointer + keyboard adapters.onBeforeStart,onMove,onBeforeDrop,onDrop,onCancel) and a plugin array provide three independent extension points without forking.document.elementFromPoint+ ancestor walk; opt-in list orientation auto-computes dropindexandindicator.aria-liveannouncement guidance; consumers wire roving tabindex per zone viauseRovingFocus.draft,since: null,category: system. Promotion topreviewblocks on the first downstream consumer.use*factory: no auto-provide, caller owns the instance and threads it to children explicitly. If subtree DI is needed later (e.g. for a future<DropZone>component), it can be added additively as acreateDragDropContext()companion mirroringcreateSelectionContext.Files
packages/0/src/composables/useDragDrop/— factory, types, two registry wrappers, indicator math, pointer + keyboard adaptersapps/docs/src/pages/composables/system/use-drag-drop.md+apps/docs/src/examples/composables/use-drag-drop/{basic,DragItem,DropList}.vue— docs page + working two-list examplepackages/0/README.md,packages/0/src/maturity.json,packages/0/src/composables/index.ts,apps/docs/src/pages/composables/index.md— feature-checklist updates