You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/soma/components/drag-drop
dev 2255d9f706
Add drag drop provider coverage
5 months ago
..
components remove backward-compat re-export shims 5 months ago
README.md Move form drag translations to morfo 5 months ago
drag-drop-provider.svelte.test.ts Add drag drop provider coverage 5 months ago
drag-drop-provider.svelte.ts Route Soma DOM actions through ActiveDom 5 months ago
exports.ts morfo: cross-layer contract + 14 new WAI-ARIA components + Dialog wired 6 months ago
index.ts morfo: cross-layer contract + 14 new WAI-ARIA components + Dialog wired 6 months ago
langs.ts morfo: cross-layer contract + 14 new WAI-ARIA components + Dialog wired 6 months ago
types.ts morfo: cross-layer contract + 14 new WAI-ARIA components + Dialog wired 6 months ago

README.md

DragDrop

A headless drag-and-drop system — pointer + keyboard + screen reader. Coordinates draggable sources and drop targets within a Provider boundary, fires a single onDrop event with a typed payload, and announces each drag step through the global Announce live region.

Soma chooses this system over HTML5 native draggable because HTML5 DnD has poor mobile support, incompatible cross-browser dataTransfer semantics, and no built-in keyboard model. This primitive works everywhere pointer events work (mouse, touch, pen) plus a full WAI-ARIA-aligned keyboard contract.

Anatomy

<DragDrop.Provider onDrop={(e) => moveItem(e.value, e.target)}>
	<DragDrop.Draggable value="task-1" data={{ kind: 'task' }}>
		Drag me
	</DragDrop.Draggable>

	<DragDrop.Droppable accept={(data) => data.kind === 'task'} textValue="Done">
		Drop here
	</DragDrop.Droppable>

	<DragDrop.Preview>
		{#snippet children({ active })}
			<div class="ghost">{active.label}</div>
		{/snippet}
	</DragDrop.Preview>
</DragDrop.Provider>

Parts

Part Element Description
Provider <div> Coordinates the drag gesture across descendants. Announces ARIA.
Draggable <div> An item that can be picked up. Pointer + keyboard gesture host.
Droppable <div> A target that accepts drops. Filters via accept(data, value).
Preview <div> Optional floating ghost element that follows the pointer during drag.

Props

Provider

Prop Type Default Description
id string auto DOM id.
onDragStart (e: { value, data, preventDefault }) => void — Fires before drag starts. Call preventDefault to cancel.
onDragEnd (e: DragEndEvent) => void — Fires on drop or cancel.
onDrop (e: DropEvent) => void — Fires on successful drop.
announceEnabled boolean true Emit live-region announcements via Announce global API.

Snippet props: { active, cancel }.

Draggable

Prop Type Default Description
id string auto DOM id.
value string — Required. Unique identifier; returned in onDrop.value.
data Record<string, any> {} Payload; returned in onDrop.data.
disabled boolean false Disables dragging.
moveBuffer number 5 Pixels the pointer must move before a drag is recognised.
textValue string — Label for announcements. Falls back to aria-label → textContent.

Snippet props: { dragging, handleProps }.

Droppable

Prop Type Default Description
id string auto DOM id.
accept (data, value) => boolean () => true Filter — returning false removes this target from keyboard nav.
disabled boolean false Disables all drops on this element.
textValue string — Label for announcements.

Snippet props: { over, accepting }.

ARIA

Part Attribute Value
Draggable role button
Draggable aria-roledescription Translated 'draggable'
Draggable aria-grabbed true during active drag (legacy but widely supported)
Draggable tabindex 0 (disabled: -1)
Droppable role region
Droppable aria-roledescription Translated 'drop zone'
Droppable aria-dropeffect 'move' while accepting, 'none' otherwise
Droppable tabindex 0 during drag if accepting; -1 otherwise

Each drag step is announced via the Announce global API. Component-owned announcement strings live in the drag-drop morfo.translations catalog. Disable with announceEnabled={false} on the Provider when wiring your own.

Data Attributes

Part Attribute Values
Provider data-drag-drop Always present
Provider data-dragging Present during active drag
Draggable data-drag-drop-draggable Always present
Draggable data-dragging This draggable is the source
Draggable data-disabled / data-value / data-text-value —
Droppable data-drag-drop-droppable Always present
Droppable data-dragover Pointer / focus over this target
Droppable data-accepting Would accept the current drag
Droppable data-disabled Drops disabled
Preview data-drag-drop-preview Always present
Preview data-active Present during active drag

Keyboard

Focus on Key Action
Draggable Space / Enter Begin drag. Focus jumps to the first accepting Droppable.
Droppable ArrowDown / ArrowRight / Tab Move to next accepting Droppable (wraps).
Droppable ArrowUp / ArrowLeft / Shift+Tab Move to previous accepting Droppable.
Droppable Space / Enter Drop.
any Escape Cancel the drag. Focus returns to the source Draggable.

Droppables are explicitly added/removed from the tab order based on the accept filter — a disabled or non-accepting target is silently skipped during keyboard navigation.

Comparison

Feature Soma Radix Ark UI react-aria dnd-kit
Pointer (mouse + touch + pen) ✅ ❌ ❌ ✅ ✅
Keyboard drag (Space/Enter/Escape) ✅ — — ✅ ✅
Arrow / Tab to navigate drop targets ✅ — — ✅ ⚠️¹
Accept filter per target ✅ — — ✅ ✅
Typed data payload ✅ — — ✅ ✅
Cancel (Escape → source focus) ✅ — — ✅ ✅
Live-region announcements ✅ — — ✅ ⚠️²
Translated messages ✅ — — ❌ ❌
DragPreview (ghost element) ✅ — — ✅ ✅
Integration with Announce global API ✅ — — — —
Sortable (within-list reorder) ⚠️³ — — ✅ ✅
Auto-scroll near viewport edges ❌⁴ — — ✅ ✅

¹ dnd-kit uses keyboard sensors that move items directionally; no explicit Tab between targets. ² dnd-kit provides announcer hooks; consumers must wire the message strings themselves. ³ Consumers implement sort by rendering N Droppables between items (one before, one after each row) and collapsing empty slots. Works but more verbose than a <Sortable> wrapper. First-class Sortable primitive can land when a consumer hits the verbosity wall. ⁴ Use native CSS overflow + scroll behavior of the container. Auto-scroll on viewport edges is a plausible future addition — the API surface would be a autoScroll={true} prop on the Provider.

Usage

Kanban board

<DragDrop.Provider onDrop={moveTask}>
	{#each columns as col (col.id)}
		<DragDrop.Droppable
			accept={(data) => col.accepts.includes(data.kind)}
			textValue={col.title}
		>
			<h3>{col.title}</h3>
			{#each col.tasks as t (t.id)}
				<DragDrop.Draggable value={t.id} data={{ kind: t.kind }} textValue={t.title}>
					{t.title}
				</DragDrop.Draggable>
			{/each}
		</DragDrop.Droppable>
	{/each}

	<DragDrop.Preview>
		{#snippet children({ active })}
			<span class="chip">{active.label}</span>
		{/snippet}
	</DragDrop.Preview>
</DragDrop.Provider>

File explorer drop zone

<DragDrop.Provider onDrop={moveFile}>
	<TreeGrid.Provider>
		{#each files as f (f.id)}
			<TreeGrid.Row value={f.id}>
				<TreeGrid.Cell>
					<DragDrop.Draggable value={f.id} data={{ kind: 'file' }} textValue={f.name}>
						{f.name}
					</DragDrop.Draggable>
				</TreeGrid.Cell>
			</TreeGrid.Row>
		{/each}
	</TreeGrid.Provider>

	{#each folders as folder (folder.id)}
		<DragDrop.Droppable
			accept={(data, value) => data.kind === 'file' && !folder.contains(value)}
			textValue={folder.name}
		>
			📁 {folder.name}
		</DragDrop.Droppable>
	{/each}
</DragDrop.Provider>

Sortable list (reorder)

Render a Droppable "gap" between every pair of items. Each gap's accept returns true when the dragged item isn't adjacent to that gap (the move would be a no-op).

<DragDrop.Provider onDrop={(e) => reorder(e.value, parseInt(e.target.dataset.gapIndex ?? '0'))}>
	{#each items as item, i (item.id)}
		<DragDrop.Droppable
			accept={(_, v) => v !== item.id && v !== items[i - 1]?.id}
			textValue="Gap {i}"
			data-gap-index={i}
		/>
		<DragDrop.Draggable value={item.id} textValue={item.label}>
			{item.label}
		</DragDrop.Draggable>
	{/each}
	<DragDrop.Droppable textValue="End" data-gap-index={items.length} />
</DragDrop.Provider>

Powered by TurnKey Linux.