@pyreon/dnd 0.11.3 → 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"b4c08b42-1","name":"use-drag-monitor.ts"},{"uid":"b4c08b42-3","name":"use-draggable.ts"},{"uid":"b4c08b42-5","name":"use-droppable.ts"},{"uid":"b4c08b42-7","name":"use-file-drop.ts"},{"uid":"b4c08b42-9","name":"use-sortable.ts"},{"uid":"b4c08b42-11","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"b4c08b42-1":{"renderedLength":1206,"gzipLength":590,"brotliLength":0,"metaUid":"b4c08b42-0"},"b4c08b42-3":{"renderedLength":1238,"gzipLength":572,"brotliLength":0,"metaUid":"b4c08b42-2"},"b4c08b42-5":{"renderedLength":1291,"gzipLength":568,"brotliLength":0,"metaUid":"b4c08b42-4"},"b4c08b42-7":{"renderedLength":2195,"gzipLength":907,"brotliLength":0,"metaUid":"b4c08b42-6"},"b4c08b42-9":{"renderedLength":5611,"gzipLength":1930,"brotliLength":0,"metaUid":"b4c08b42-8"},"b4c08b42-11":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"b4c08b42-10"}},"nodeMetas":{"b4c08b42-0":{"id":"/src/use-drag-monitor.ts","moduleParts":{"index.js":"b4c08b42-1"},"imported":[{"uid":"b4c08b42-12"},{"uid":"b4c08b42-13"}],"importedBy":[{"uid":"b4c08b42-10"}]},"b4c08b42-2":{"id":"/src/use-draggable.ts","moduleParts":{"index.js":"b4c08b42-3"},"imported":[{"uid":"b4c08b42-12"},{"uid":"b4c08b42-13"}],"importedBy":[{"uid":"b4c08b42-10"}]},"b4c08b42-4":{"id":"/src/use-droppable.ts","moduleParts":{"index.js":"b4c08b42-5"},"imported":[{"uid":"b4c08b42-12"},{"uid":"b4c08b42-13"}],"importedBy":[{"uid":"b4c08b42-10"}]},"b4c08b42-6":{"id":"/src/use-file-drop.ts","moduleParts":{"index.js":"b4c08b42-7"},"imported":[{"uid":"b4c08b42-14"},{"uid":"b4c08b42-15"},{"uid":"b4c08b42-13"}],"importedBy":[{"uid":"b4c08b42-10"}]},"b4c08b42-8":{"id":"/src/use-sortable.ts","moduleParts":{"index.js":"b4c08b42-9"},"imported":[{"uid":"b4c08b42-16"},{"uid":"b4c08b42-12"},{"uid":"b4c08b42-17"},{"uid":"b4c08b42-18"},{"uid":"b4c08b42-13"}],"importedBy":[{"uid":"b4c08b42-10"}]},"b4c08b42-10":{"id":"/src/index.ts","moduleParts":{"index.js":"b4c08b42-11"},"imported":[{"uid":"b4c08b42-0"},{"uid":"b4c08b42-2"},{"uid":"b4c08b42-4"},{"uid":"b4c08b42-6"},{"uid":"b4c08b42-8"}],"importedBy":[],"isEntry":true},"b4c08b42-12":{"id":"@atlaskit/pragmatic-drag-and-drop/element/adapter","moduleParts":{},"imported":[],"importedBy":[{"uid":"b4c08b42-0"},{"uid":"b4c08b42-2"},{"uid":"b4c08b42-4"},{"uid":"b4c08b42-8"}]},"b4c08b42-13":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"b4c08b42-0"},{"uid":"b4c08b42-2"},{"uid":"b4c08b42-4"},{"uid":"b4c08b42-6"},{"uid":"b4c08b42-8"}]},"b4c08b42-14":{"id":"@atlaskit/pragmatic-drag-and-drop/external/adapter","moduleParts":{},"imported":[],"importedBy":[{"uid":"b4c08b42-6"}]},"b4c08b42-15":{"id":"@atlaskit/pragmatic-drag-and-drop/external/file","moduleParts":{},"imported":[],"importedBy":[{"uid":"b4c08b42-6"}]},"b4c08b42-16":{"id":"@atlaskit/pragmatic-drag-and-drop/combine","moduleParts":{},"imported":[],"importedBy":[{"uid":"b4c08b42-8"}]},"b4c08b42-17":{"id":"@atlaskit/pragmatic-drag-and-drop-auto-scroll/element","moduleParts":{},"imported":[],"importedBy":[{"uid":"b4c08b42-8"}]},"b4c08b42-18":{"id":"@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge","moduleParts":{},"imported":[],"importedBy":[{"uid":"b4c08b42-8"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"e8bee9be-1","name":"use-drag-monitor.ts"},{"uid":"e8bee9be-3","name":"use-draggable.ts"},{"uid":"e8bee9be-5","name":"use-droppable.ts"},{"uid":"e8bee9be-7","name":"use-file-drop.ts"},{"uid":"e8bee9be-9","name":"use-sortable.ts"},{"uid":"e8bee9be-11","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"e8bee9be-1":{"renderedLength":1206,"gzipLength":590,"brotliLength":0,"metaUid":"e8bee9be-0"},"e8bee9be-3":{"renderedLength":1238,"gzipLength":572,"brotliLength":0,"metaUid":"e8bee9be-2"},"e8bee9be-5":{"renderedLength":1291,"gzipLength":568,"brotliLength":0,"metaUid":"e8bee9be-4"},"e8bee9be-7":{"renderedLength":2195,"gzipLength":907,"brotliLength":0,"metaUid":"e8bee9be-6"},"e8bee9be-9":{"renderedLength":5675,"gzipLength":1936,"brotliLength":0,"metaUid":"e8bee9be-8"},"e8bee9be-11":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"e8bee9be-10"}},"nodeMetas":{"e8bee9be-0":{"id":"/src/use-drag-monitor.ts","moduleParts":{"index.js":"e8bee9be-1"},"imported":[{"uid":"e8bee9be-12"},{"uid":"e8bee9be-13"}],"importedBy":[{"uid":"e8bee9be-10"}]},"e8bee9be-2":{"id":"/src/use-draggable.ts","moduleParts":{"index.js":"e8bee9be-3"},"imported":[{"uid":"e8bee9be-12"},{"uid":"e8bee9be-13"}],"importedBy":[{"uid":"e8bee9be-10"}]},"e8bee9be-4":{"id":"/src/use-droppable.ts","moduleParts":{"index.js":"e8bee9be-5"},"imported":[{"uid":"e8bee9be-12"},{"uid":"e8bee9be-13"}],"importedBy":[{"uid":"e8bee9be-10"}]},"e8bee9be-6":{"id":"/src/use-file-drop.ts","moduleParts":{"index.js":"e8bee9be-7"},"imported":[{"uid":"e8bee9be-14"},{"uid":"e8bee9be-15"},{"uid":"e8bee9be-13"}],"importedBy":[{"uid":"e8bee9be-10"}]},"e8bee9be-8":{"id":"/src/use-sortable.ts","moduleParts":{"index.js":"e8bee9be-9"},"imported":[{"uid":"e8bee9be-16"},{"uid":"e8bee9be-12"},{"uid":"e8bee9be-17"},{"uid":"e8bee9be-18"},{"uid":"e8bee9be-13"}],"importedBy":[{"uid":"e8bee9be-10"}]},"e8bee9be-10":{"id":"/src/index.ts","moduleParts":{"index.js":"e8bee9be-11"},"imported":[{"uid":"e8bee9be-0"},{"uid":"e8bee9be-2"},{"uid":"e8bee9be-4"},{"uid":"e8bee9be-6"},{"uid":"e8bee9be-8"}],"importedBy":[],"isEntry":true},"e8bee9be-12":{"id":"@atlaskit/pragmatic-drag-and-drop/element/adapter","moduleParts":{},"imported":[],"importedBy":[{"uid":"e8bee9be-0"},{"uid":"e8bee9be-2"},{"uid":"e8bee9be-4"},{"uid":"e8bee9be-8"}]},"e8bee9be-13":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"e8bee9be-0"},{"uid":"e8bee9be-2"},{"uid":"e8bee9be-4"},{"uid":"e8bee9be-6"},{"uid":"e8bee9be-8"}]},"e8bee9be-14":{"id":"@atlaskit/pragmatic-drag-and-drop/external/adapter","moduleParts":{},"imported":[],"importedBy":[{"uid":"e8bee9be-6"}]},"e8bee9be-15":{"id":"@atlaskit/pragmatic-drag-and-drop/external/file","moduleParts":{},"imported":[],"importedBy":[{"uid":"e8bee9be-6"}]},"e8bee9be-16":{"id":"@atlaskit/pragmatic-drag-and-drop/combine","moduleParts":{},"imported":[],"importedBy":[{"uid":"e8bee9be-8"}]},"e8bee9be-17":{"id":"@atlaskit/pragmatic-drag-and-drop-auto-scroll/element","moduleParts":{},"imported":[],"importedBy":[{"uid":"e8bee9be-8"}]},"e8bee9be-18":{"id":"@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge","moduleParts":{},"imported":[],"importedBy":[{"uid":"e8bee9be-8"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -408,6 +408,9 @@ function useSortable(options) {
408
408
  onCleanup(() => {
409
409
  for (const cleanup of cleanups) cleanup();
410
410
  cleanups.length = 0;
411
+ activeId.set(null);
412
+ overId.set(null);
413
+ overEdge.set(null);
411
414
  });
412
415
  return {
413
416
  containerRef,
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/use-drag-monitor.ts","../src/use-draggable.ts","../src/use-droppable.ts","../src/use-file-drop.ts","../src/use-sortable.ts"],"sourcesContent":["import { monitorForElements } from \"@atlaskit/pragmatic-drag-and-drop/element/adapter\"\nimport { onCleanup, signal } from \"@pyreon/reactivity\"\nimport type { DragData } from \"./types\"\n\nexport interface UseDragMonitorOptions {\n /** Called on any drag start in the page. */\n onDragStart?: (data: DragData) => void\n /** Called on any drop in the page. */\n onDrop?: (sourceData: DragData, targetData: DragData) => void\n /** Filter which drags to monitor. */\n canMonitor?: (data: DragData) => boolean\n}\n\nexport interface UseDragMonitorResult {\n /** Whether any element is currently being dragged. */\n isDragging: () => boolean\n /** Data of the currently dragging element (null if not dragging). */\n dragData: () => DragData | null\n}\n\n/**\n * Monitor all drag operations on the page.\n * Useful for global drag indicators, analytics, or coordination between\n * multiple drag-and-drop areas.\n *\n * @example\n * ```tsx\n * const { isDragging, dragData } = useDragMonitor({\n * canMonitor: (data) => data.type === \"card\",\n * onDrop: (source, target) => logDrop(source, target),\n * })\n *\n * <Show when={isDragging()}>\n * <div class=\"global-drag-overlay\">\n * Dragging: {() => dragData()?.name}\n * </div>\n * </Show>\n * ```\n */\nexport function useDragMonitor(options?: UseDragMonitorOptions): UseDragMonitorResult {\n const isDragging = signal(false)\n const dragData = signal<DragData | null>(null)\n\n const canMonitorFn = options?.canMonitor\n ? ({ source }: { source: { data: Record<string, unknown> } }) =>\n options.canMonitor?.(source.data as DragData) ?? true\n : null\n\n const cleanup = monitorForElements({\n ...(canMonitorFn ? { canMonitor: canMonitorFn } : {}),\n onDragStart: ({ source }) => {\n isDragging.set(true)\n dragData.set(source.data as DragData)\n options?.onDragStart?.(source.data as DragData)\n },\n onDrop: ({ source, location }) => {\n isDragging.set(false)\n dragData.set(null)\n const targetData = location.current.dropTargets[0]?.data ?? {}\n options?.onDrop?.(source.data as DragData, targetData as DragData)\n },\n })\n\n onCleanup(cleanup)\n\n return { isDragging, dragData }\n}\n","import { draggable } from \"@atlaskit/pragmatic-drag-and-drop/element/adapter\"\nimport { onCleanup, signal } from \"@pyreon/reactivity\"\nimport type { DragData, UseDraggableOptions, UseDraggableResult } from \"./types\"\n\n/**\n * Make an element draggable with signal-driven state.\n *\n * @example\n * ```tsx\n * let cardEl: HTMLElement | null = null\n *\n * const { isDragging } = useDraggable({\n * element: () => cardEl,\n * data: { id: card.id, type: \"card\" },\n * })\n *\n * <div ref={(el) => cardEl = el} class={isDragging() ? \"opacity-50\" : \"\"}>\n * {card.title}\n * </div>\n * ```\n */\nexport function useDraggable<T extends DragData = DragData>(\n options: UseDraggableOptions<T>,\n): UseDraggableResult {\n const isDragging = signal(false)\n let cleanup: (() => void) | undefined\n\n function setup() {\n if (cleanup) cleanup()\n\n const el = options.element()\n if (!el) return\n\n const resolveData = () =>\n typeof options.data === \"function\" ? (options.data as () => T)() : options.data\n\n const handle = options.handle?.()\n cleanup = draggable({\n element: el,\n ...(handle ? { dragHandle: handle } : {}),\n getInitialData: resolveData,\n canDrag: () => {\n const disabled = options.disabled\n if (typeof disabled === \"function\") return !disabled()\n return !disabled\n },\n onDragStart: () => {\n isDragging.set(true)\n options.onDragStart?.()\n },\n onDrop: () => {\n isDragging.set(false)\n options.onDragEnd?.()\n },\n })\n }\n\n // Defer setup to next microtask so refs are populated\n queueMicrotask(setup)\n\n onCleanup(() => {\n if (cleanup) cleanup()\n })\n\n return { isDragging }\n}\n","import { dropTargetForElements } from \"@atlaskit/pragmatic-drag-and-drop/element/adapter\"\nimport { onCleanup, signal } from \"@pyreon/reactivity\"\nimport type { DragData, UseDroppableOptions, UseDroppableResult } from \"./types\"\n\n/**\n * Make an element a drop target with signal-driven state.\n *\n * @example\n * ```tsx\n * let zoneEl: HTMLElement | null = null\n *\n * const { isOver } = useDroppable({\n * element: () => zoneEl,\n * onDrop: (data) => handleDrop(data),\n * canDrop: (data) => data.type === \"card\",\n * })\n *\n * <div ref={(el) => zoneEl = el} class={isOver() ? \"bg-blue-50\" : \"\"}>\n * Drop here\n * </div>\n * ```\n */\nexport function useDroppable<T extends DragData = DragData>(\n options: UseDroppableOptions<T>,\n): UseDroppableResult {\n const isOver = signal(false)\n let cleanup: (() => void) | undefined\n\n function setup() {\n if (cleanup) cleanup()\n\n const el = options.element()\n if (!el) return\n\n cleanup = dropTargetForElements({\n element: el,\n getData: () => {\n if (!options.data) return {}\n return typeof options.data === \"function\" ? (options.data as () => T)() : options.data\n },\n canDrop: ({ source }) => {\n if (!options.canDrop) return true\n return options.canDrop(source.data as DragData)\n },\n onDragEnter: ({ source }) => {\n isOver.set(true)\n options.onDragEnter?.(source.data as DragData)\n },\n onDragLeave: () => {\n isOver.set(false)\n options.onDragLeave?.()\n },\n onDrop: ({ source }) => {\n isOver.set(false)\n options.onDrop?.(source.data as DragData)\n },\n })\n }\n\n queueMicrotask(setup)\n\n onCleanup(() => {\n if (cleanup) cleanup()\n })\n\n return { isOver }\n}\n","import {\n dropTargetForExternal,\n monitorForExternal,\n} from \"@atlaskit/pragmatic-drag-and-drop/external/adapter\"\nimport { containsFiles, getFiles } from \"@atlaskit/pragmatic-drag-and-drop/external/file\"\nimport { onCleanup, signal } from \"@pyreon/reactivity\"\n\nexport interface UseFileDropOptions {\n /** Element getter for the drop zone. */\n element: () => HTMLElement | null\n /** Called when files are dropped. */\n onDrop: (files: File[]) => void\n /** Filter accepted file types (e.g. [\"image/*\", \".pdf\"]). */\n accept?: string[]\n /** Maximum number of files. */\n maxFiles?: number\n /** Whether drop is disabled. */\n disabled?: boolean | (() => boolean)\n}\n\nexport interface UseFileDropResult {\n /** Whether files are being dragged over the drop zone. */\n isOver: () => boolean\n /** Whether files are being dragged anywhere on the page. */\n isDraggingFiles: () => boolean\n}\n\n/**\n * File drop zone with signal-driven state.\n * Uses the native file drag events via pragmatic-drag-and-drop.\n *\n * @example\n * ```tsx\n * let dropZone: HTMLElement | null = null\n *\n * const { isOver, isDraggingFiles } = useFileDrop({\n * element: () => dropZone,\n * accept: [\"image/*\", \".pdf\"],\n * maxFiles: 5,\n * onDrop: (files) => upload(files),\n * })\n *\n * <div\n * ref={(el) => dropZone = el}\n * class={isOver() ? \"drop-active\" : isDraggingFiles() ? \"drop-ready\" : \"\"}\n * >\n * Drop files here\n * </div>\n * ```\n */\nexport function useFileDrop(options: UseFileDropOptions): UseFileDropResult {\n const isOver = signal(false)\n const isDraggingFiles = signal(false)\n let cleanup: (() => void) | undefined\n\n function matchesAccept(file: File, accept: string[]): boolean {\n return accept.some((pattern) => {\n if (pattern.startsWith(\".\")) {\n return file.name.toLowerCase().endsWith(pattern.toLowerCase())\n }\n if (pattern.endsWith(\"/*\")) {\n return file.type.startsWith(pattern.slice(0, -1))\n }\n return file.type === pattern\n })\n }\n\n function setup() {\n if (cleanup) cleanup()\n\n const el = options.element()\n if (!el) return\n\n const cleanups: (() => void)[] = []\n\n // Monitor for file drags anywhere on the page\n cleanups.push(\n monitorForExternal({\n canMonitor: ({ source }) => containsFiles({ source }),\n onDragStart: () => isDraggingFiles.set(true),\n onDrop: () => isDraggingFiles.set(false),\n }),\n )\n\n // Drop target on the specific element\n cleanups.push(\n dropTargetForExternal({\n element: el,\n canDrop: ({ source }) => {\n const disabled = options.disabled\n if (typeof disabled === \"function\" ? disabled() : disabled) return false\n return containsFiles({ source })\n },\n onDragEnter: () => isOver.set(true),\n onDragLeave: () => isOver.set(false),\n onDrop: ({ source }) => {\n isOver.set(false)\n isDraggingFiles.set(false)\n\n let files = getFiles({ source })\n\n // Filter by accept\n if (options.accept && options.accept.length > 0) {\n files = files.filter((f) => matchesAccept(f, options.accept as string[]))\n }\n\n // Limit count\n if (options.maxFiles && files.length > options.maxFiles) {\n files = files.slice(0, options.maxFiles)\n }\n\n if (files.length > 0) {\n options.onDrop(files)\n }\n },\n }),\n )\n\n cleanup = () => {\n for (const fn of cleanups) fn()\n }\n }\n\n queueMicrotask(setup)\n\n onCleanup(() => {\n if (cleanup) cleanup()\n })\n\n return { isOver, isDraggingFiles }\n}\n","import { combine } from \"@atlaskit/pragmatic-drag-and-drop/combine\"\nimport { draggable, dropTargetForElements } from \"@atlaskit/pragmatic-drag-and-drop/element/adapter\"\nimport { autoScrollForElements } from \"@atlaskit/pragmatic-drag-and-drop-auto-scroll/element\"\nimport {\n attachClosestEdge,\n type Edge,\n extractClosestEdge,\n} from \"@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge\"\nimport { onCleanup, signal } from \"@pyreon/reactivity\"\nimport type { DropEdge, UseSortableOptions, UseSortableResult } from \"./types\"\n\nconst SORT_KEY = \"__pyreon_sortable_key\"\nconst SORT_ID = \"__pyreon_sortable_id\"\n\nlet _sortableCounter = 0\n\n/**\n * Sortable list with signal-driven state, auto-scroll, and edge detection.\n *\n * Features:\n * - Keyed drag items matching `<For by={...}>` pattern\n * - Auto-scroll when dragging near container edges\n * - Closest-edge detection (drop above/below or left/right)\n * - Axis constraint (vertical/horizontal)\n * - Keyboard reordering (Alt+Arrow keys)\n *\n * @example\n * ```tsx\n * const items = signal([\n * { id: \"1\", name: \"Alice\" },\n * { id: \"2\", name: \"Bob\" },\n * { id: \"3\", name: \"Charlie\" },\n * ])\n *\n * const { containerRef, itemRef, activeId, overId, overEdge } = useSortable({\n * items,\n * by: (item) => item.id,\n * onReorder: (newItems) => items.set(newItems),\n * })\n *\n * <ul ref={containerRef}>\n * <For each={items()} by={item => item.id}>\n * {(item) => (\n * <li\n * ref={itemRef(item.id)}\n * class={activeId() === item.id ? \"dragging\" : \"\"}\n * style={overId() === item.id ? `border-${overEdge()}: 2px solid blue` : \"\"}\n * >\n * {item.name}\n * </li>\n * )}\n * </For>\n * </ul>\n * ```\n */\nexport function useSortable<T>(options: UseSortableOptions<T>): UseSortableResult {\n const sortableId = `sortable-${++_sortableCounter}`\n const activeId = signal<string | number | null>(null)\n const overId = signal<string | number | null>(null)\n const overEdge = signal<DropEdge | null>(null)\n const axis = options.axis ?? \"vertical\"\n\n const cleanups: (() => void)[] = []\n\n /** Perform the reorder based on current active/over/edge state. */\n function performReorder() {\n const dragId = activeId.peek()\n const dropId = overId.peek()\n const edge = overEdge.peek()\n if (dragId == null || dropId == null || dragId === dropId) return\n\n const currentItems = options.items()\n const dragIndex = currentItems.findIndex((item) => options.by(item) === dragId)\n const dropIndex = currentItems.findIndex((item) => options.by(item) === dropId)\n if (dragIndex === -1 || dropIndex === -1) return\n\n const reordered = [...currentItems]\n const [moved] = reordered.splice(dragIndex, 1)\n if (!moved) return\n\n // Determine insert position based on closest edge\n const rawInsert =\n edge === \"bottom\" || edge === \"right\"\n ? dropIndex >= dragIndex\n ? dropIndex\n : dropIndex + 1\n : dropIndex <= dragIndex\n ? dropIndex\n : dropIndex - 1\n const insertAt = Math.max(0, Math.min(rawInsert, reordered.length))\n\n reordered.splice(insertAt, 0, moved)\n options.onReorder(reordered)\n }\n\n function containerRef(el: HTMLElement) {\n // Auto-scroll when dragging near container edges\n cleanups.push(\n autoScrollForElements({\n element: el,\n canScroll: ({ source }) => source.data[SORT_ID] === sortableId,\n }),\n )\n\n // Container is a drop target for reorder finalization\n cleanups.push(\n dropTargetForElements({\n element: el,\n getData: () => ({ [SORT_ID]: sortableId }),\n canDrop: ({ source }) => source.data[SORT_ID] === sortableId,\n onDrop: () => {\n performReorder()\n activeId.set(null)\n overId.set(null)\n overEdge.set(null)\n },\n }),\n )\n\n // Keyboard reordering: Alt+Arrow keys\n const keyHandler = (e: KeyboardEvent) => {\n if (!e.altKey) return\n\n const isUp = axis === \"vertical\" ? e.key === \"ArrowUp\" : e.key === \"ArrowLeft\"\n const isDown = axis === \"vertical\" ? e.key === \"ArrowDown\" : e.key === \"ArrowRight\"\n if (!isUp && !isDown) return\n\n const focused = document.activeElement as HTMLElement | null\n if (!focused || !el.contains(focused)) return\n\n const focusedKey = focused.dataset.pyreonSortKey\n if (!focusedKey) return\n\n e.preventDefault()\n\n const currentItems = options.items()\n const currentIndex = currentItems.findIndex((item) => String(options.by(item)) === focusedKey)\n if (currentIndex === -1) return\n\n const targetIndex = isUp ? currentIndex - 1 : currentIndex + 1\n if (targetIndex < 0 || targetIndex >= currentItems.length) return\n\n const reordered = [...currentItems]\n const temp = reordered[currentIndex]\n reordered[currentIndex] = reordered[targetIndex] as T\n reordered[targetIndex] = temp as T\n options.onReorder(reordered)\n\n // Restore focus after DOM update\n requestAnimationFrame(() => {\n const items = el.querySelectorAll(\"[data-pyreon-sort-key]\")\n for (const item of items) {\n if ((item as HTMLElement).dataset.pyreonSortKey === focusedKey) {\n ;(item as HTMLElement).focus()\n break\n }\n }\n })\n }\n\n el.addEventListener(\"keydown\", keyHandler)\n cleanups.push(() => el.removeEventListener(\"keydown\", keyHandler))\n }\n\n function itemRef(key: string | number): (el: HTMLElement) => void {\n return (el: HTMLElement) => {\n el.dataset.pyreonSortKey = String(key)\n if (!el.hasAttribute(\"tabindex\")) el.setAttribute(\"tabindex\", \"0\")\n el.setAttribute(\"role\", \"listitem\")\n el.setAttribute(\"aria-roledescription\", \"sortable item\")\n\n const allowedEdges: Edge[] = axis === \"vertical\" ? [\"top\", \"bottom\"] : [\"left\", \"right\"]\n\n const cleanup = combine(\n draggable({\n element: el,\n getInitialData: () => ({\n [SORT_KEY]: key,\n [SORT_ID]: sortableId,\n }),\n onDragStart: () => activeId.set(key),\n onDrop: () => {\n queueMicrotask(() => {\n activeId.set(null)\n overId.set(null)\n overEdge.set(null)\n })\n },\n }),\n dropTargetForElements({\n element: el,\n getData: ({ input, element }) =>\n attachClosestEdge(\n { [SORT_KEY]: key, [SORT_ID]: sortableId },\n { input, element, allowedEdges },\n ),\n canDrop: ({ source }) => source.data[SORT_ID] === sortableId,\n onDragEnter: ({ self }) => {\n overId.set(key)\n overEdge.set(extractClosestEdge(self.data) as DropEdge | null)\n },\n onDrag: ({ self }) => {\n overEdge.set(extractClosestEdge(self.data) as DropEdge | null)\n },\n onDragLeave: () => {\n if (overId.peek() === key) {\n overId.set(null)\n overEdge.set(null)\n }\n },\n }),\n )\n\n cleanups.push(cleanup)\n }\n }\n\n onCleanup(() => {\n for (const cleanup of cleanups) cleanup()\n cleanups.length = 0\n })\n\n return { containerRef, itemRef, activeId, overId, overEdge }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/use-drag-monitor.ts","../src/use-draggable.ts","../src/use-droppable.ts","../src/use-file-drop.ts","../src/use-sortable.ts"],"sourcesContent":["import { monitorForElements } from \"@atlaskit/pragmatic-drag-and-drop/element/adapter\"\nimport { onCleanup, signal } from \"@pyreon/reactivity\"\nimport type { DragData } from \"./types\"\n\nexport interface UseDragMonitorOptions {\n /** Called on any drag start in the page. */\n onDragStart?: (data: DragData) => void\n /** Called on any drop in the page. */\n onDrop?: (sourceData: DragData, targetData: DragData) => void\n /** Filter which drags to monitor. */\n canMonitor?: (data: DragData) => boolean\n}\n\nexport interface UseDragMonitorResult {\n /** Whether any element is currently being dragged. */\n isDragging: () => boolean\n /** Data of the currently dragging element (null if not dragging). */\n dragData: () => DragData | null\n}\n\n/**\n * Monitor all drag operations on the page.\n * Useful for global drag indicators, analytics, or coordination between\n * multiple drag-and-drop areas.\n *\n * @example\n * ```tsx\n * const { isDragging, dragData } = useDragMonitor({\n * canMonitor: (data) => data.type === \"card\",\n * onDrop: (source, target) => logDrop(source, target),\n * })\n *\n * <Show when={isDragging()}>\n * <div class=\"global-drag-overlay\">\n * Dragging: {() => dragData()?.name}\n * </div>\n * </Show>\n * ```\n */\nexport function useDragMonitor(options?: UseDragMonitorOptions): UseDragMonitorResult {\n const isDragging = signal(false)\n const dragData = signal<DragData | null>(null)\n\n const canMonitorFn = options?.canMonitor\n ? ({ source }: { source: { data: Record<string, unknown> } }) =>\n options.canMonitor?.(source.data as DragData) ?? true\n : null\n\n const cleanup = monitorForElements({\n ...(canMonitorFn ? { canMonitor: canMonitorFn } : {}),\n onDragStart: ({ source }) => {\n isDragging.set(true)\n dragData.set(source.data as DragData)\n options?.onDragStart?.(source.data as DragData)\n },\n onDrop: ({ source, location }) => {\n isDragging.set(false)\n dragData.set(null)\n const targetData = location.current.dropTargets[0]?.data ?? {}\n options?.onDrop?.(source.data as DragData, targetData as DragData)\n },\n })\n\n onCleanup(cleanup)\n\n return { isDragging, dragData }\n}\n","import { draggable } from \"@atlaskit/pragmatic-drag-and-drop/element/adapter\"\nimport { onCleanup, signal } from \"@pyreon/reactivity\"\nimport type { DragData, UseDraggableOptions, UseDraggableResult } from \"./types\"\n\n/**\n * Make an element draggable with signal-driven state.\n *\n * @example\n * ```tsx\n * let cardEl: HTMLElement | null = null\n *\n * const { isDragging } = useDraggable({\n * element: () => cardEl,\n * data: { id: card.id, type: \"card\" },\n * })\n *\n * <div ref={(el) => cardEl = el} class={isDragging() ? \"opacity-50\" : \"\"}>\n * {card.title}\n * </div>\n * ```\n */\nexport function useDraggable<T extends DragData = DragData>(\n options: UseDraggableOptions<T>,\n): UseDraggableResult {\n const isDragging = signal(false)\n let cleanup: (() => void) | undefined\n\n function setup() {\n if (cleanup) cleanup()\n\n const el = options.element()\n if (!el) return\n\n const resolveData = () =>\n typeof options.data === \"function\" ? (options.data as () => T)() : options.data\n\n const handle = options.handle?.()\n cleanup = draggable({\n element: el,\n ...(handle ? { dragHandle: handle } : {}),\n getInitialData: resolveData,\n canDrag: () => {\n const disabled = options.disabled\n if (typeof disabled === \"function\") return !disabled()\n return !disabled\n },\n onDragStart: () => {\n isDragging.set(true)\n options.onDragStart?.()\n },\n onDrop: () => {\n isDragging.set(false)\n options.onDragEnd?.()\n },\n })\n }\n\n // Defer setup to next microtask so refs are populated\n queueMicrotask(setup)\n\n onCleanup(() => {\n if (cleanup) cleanup()\n })\n\n return { isDragging }\n}\n","import { dropTargetForElements } from \"@atlaskit/pragmatic-drag-and-drop/element/adapter\"\nimport { onCleanup, signal } from \"@pyreon/reactivity\"\nimport type { DragData, UseDroppableOptions, UseDroppableResult } from \"./types\"\n\n/**\n * Make an element a drop target with signal-driven state.\n *\n * @example\n * ```tsx\n * let zoneEl: HTMLElement | null = null\n *\n * const { isOver } = useDroppable({\n * element: () => zoneEl,\n * onDrop: (data) => handleDrop(data),\n * canDrop: (data) => data.type === \"card\",\n * })\n *\n * <div ref={(el) => zoneEl = el} class={isOver() ? \"bg-blue-50\" : \"\"}>\n * Drop here\n * </div>\n * ```\n */\nexport function useDroppable<T extends DragData = DragData>(\n options: UseDroppableOptions<T>,\n): UseDroppableResult {\n const isOver = signal(false)\n let cleanup: (() => void) | undefined\n\n function setup() {\n if (cleanup) cleanup()\n\n const el = options.element()\n if (!el) return\n\n cleanup = dropTargetForElements({\n element: el,\n getData: () => {\n if (!options.data) return {}\n return typeof options.data === \"function\" ? (options.data as () => T)() : options.data\n },\n canDrop: ({ source }) => {\n if (!options.canDrop) return true\n return options.canDrop(source.data as DragData)\n },\n onDragEnter: ({ source }) => {\n isOver.set(true)\n options.onDragEnter?.(source.data as DragData)\n },\n onDragLeave: () => {\n isOver.set(false)\n options.onDragLeave?.()\n },\n onDrop: ({ source }) => {\n isOver.set(false)\n options.onDrop?.(source.data as DragData)\n },\n })\n }\n\n queueMicrotask(setup)\n\n onCleanup(() => {\n if (cleanup) cleanup()\n })\n\n return { isOver }\n}\n","import {\n dropTargetForExternal,\n monitorForExternal,\n} from \"@atlaskit/pragmatic-drag-and-drop/external/adapter\"\nimport { containsFiles, getFiles } from \"@atlaskit/pragmatic-drag-and-drop/external/file\"\nimport { onCleanup, signal } from \"@pyreon/reactivity\"\n\nexport interface UseFileDropOptions {\n /** Element getter for the drop zone. */\n element: () => HTMLElement | null\n /** Called when files are dropped. */\n onDrop: (files: File[]) => void\n /** Filter accepted file types (e.g. [\"image/*\", \".pdf\"]). */\n accept?: string[]\n /** Maximum number of files. */\n maxFiles?: number\n /** Whether drop is disabled. */\n disabled?: boolean | (() => boolean)\n}\n\nexport interface UseFileDropResult {\n /** Whether files are being dragged over the drop zone. */\n isOver: () => boolean\n /** Whether files are being dragged anywhere on the page. */\n isDraggingFiles: () => boolean\n}\n\n/**\n * File drop zone with signal-driven state.\n * Uses the native file drag events via pragmatic-drag-and-drop.\n *\n * @example\n * ```tsx\n * let dropZone: HTMLElement | null = null\n *\n * const { isOver, isDraggingFiles } = useFileDrop({\n * element: () => dropZone,\n * accept: [\"image/*\", \".pdf\"],\n * maxFiles: 5,\n * onDrop: (files) => upload(files),\n * })\n *\n * <div\n * ref={(el) => dropZone = el}\n * class={isOver() ? \"drop-active\" : isDraggingFiles() ? \"drop-ready\" : \"\"}\n * >\n * Drop files here\n * </div>\n * ```\n */\nexport function useFileDrop(options: UseFileDropOptions): UseFileDropResult {\n const isOver = signal(false)\n const isDraggingFiles = signal(false)\n let cleanup: (() => void) | undefined\n\n function matchesAccept(file: File, accept: string[]): boolean {\n return accept.some((pattern) => {\n if (pattern.startsWith(\".\")) {\n return file.name.toLowerCase().endsWith(pattern.toLowerCase())\n }\n if (pattern.endsWith(\"/*\")) {\n return file.type.startsWith(pattern.slice(0, -1))\n }\n return file.type === pattern\n })\n }\n\n function setup() {\n if (cleanup) cleanup()\n\n const el = options.element()\n if (!el) return\n\n const cleanups: (() => void)[] = []\n\n // Monitor for file drags anywhere on the page\n cleanups.push(\n monitorForExternal({\n canMonitor: ({ source }) => containsFiles({ source }),\n onDragStart: () => isDraggingFiles.set(true),\n onDrop: () => isDraggingFiles.set(false),\n }),\n )\n\n // Drop target on the specific element\n cleanups.push(\n dropTargetForExternal({\n element: el,\n canDrop: ({ source }) => {\n const disabled = options.disabled\n if (typeof disabled === \"function\" ? disabled() : disabled) return false\n return containsFiles({ source })\n },\n onDragEnter: () => isOver.set(true),\n onDragLeave: () => isOver.set(false),\n onDrop: ({ source }) => {\n isOver.set(false)\n isDraggingFiles.set(false)\n\n let files = getFiles({ source })\n\n // Filter by accept\n if (options.accept && options.accept.length > 0) {\n files = files.filter((f) => matchesAccept(f, options.accept as string[]))\n }\n\n // Limit count\n if (options.maxFiles && files.length > options.maxFiles) {\n files = files.slice(0, options.maxFiles)\n }\n\n if (files.length > 0) {\n options.onDrop(files)\n }\n },\n }),\n )\n\n cleanup = () => {\n for (const fn of cleanups) fn()\n }\n }\n\n queueMicrotask(setup)\n\n onCleanup(() => {\n if (cleanup) cleanup()\n })\n\n return { isOver, isDraggingFiles }\n}\n","import { combine } from \"@atlaskit/pragmatic-drag-and-drop/combine\"\nimport { draggable, dropTargetForElements } from \"@atlaskit/pragmatic-drag-and-drop/element/adapter\"\nimport { autoScrollForElements } from \"@atlaskit/pragmatic-drag-and-drop-auto-scroll/element\"\nimport {\n attachClosestEdge,\n type Edge,\n extractClosestEdge,\n} from \"@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge\"\nimport { onCleanup, signal } from \"@pyreon/reactivity\"\nimport type { DropEdge, UseSortableOptions, UseSortableResult } from \"./types\"\n\nconst SORT_KEY = \"__pyreon_sortable_key\"\nconst SORT_ID = \"__pyreon_sortable_id\"\n\nlet _sortableCounter = 0\n\n/**\n * Sortable list with signal-driven state, auto-scroll, and edge detection.\n *\n * Features:\n * - Keyed drag items matching `<For by={...}>` pattern\n * - Auto-scroll when dragging near container edges\n * - Closest-edge detection (drop above/below or left/right)\n * - Axis constraint (vertical/horizontal)\n * - Keyboard reordering (Alt+Arrow keys)\n *\n * @example\n * ```tsx\n * const items = signal([\n * { id: \"1\", name: \"Alice\" },\n * { id: \"2\", name: \"Bob\" },\n * { id: \"3\", name: \"Charlie\" },\n * ])\n *\n * const { containerRef, itemRef, activeId, overId, overEdge } = useSortable({\n * items,\n * by: (item) => item.id,\n * onReorder: (newItems) => items.set(newItems),\n * })\n *\n * <ul ref={containerRef}>\n * <For each={items()} by={item => item.id}>\n * {(item) => (\n * <li\n * ref={itemRef(item.id)}\n * class={activeId() === item.id ? \"dragging\" : \"\"}\n * style={overId() === item.id ? `border-${overEdge()}: 2px solid blue` : \"\"}\n * >\n * {item.name}\n * </li>\n * )}\n * </For>\n * </ul>\n * ```\n */\nexport function useSortable<T>(options: UseSortableOptions<T>): UseSortableResult {\n const sortableId = `sortable-${++_sortableCounter}`\n const activeId = signal<string | number | null>(null)\n const overId = signal<string | number | null>(null)\n const overEdge = signal<DropEdge | null>(null)\n const axis = options.axis ?? \"vertical\"\n\n const cleanups: (() => void)[] = []\n\n /** Perform the reorder based on current active/over/edge state. */\n function performReorder() {\n const dragId = activeId.peek()\n const dropId = overId.peek()\n const edge = overEdge.peek()\n if (dragId == null || dropId == null || dragId === dropId) return\n\n const currentItems = options.items()\n const dragIndex = currentItems.findIndex((item) => options.by(item) === dragId)\n const dropIndex = currentItems.findIndex((item) => options.by(item) === dropId)\n if (dragIndex === -1 || dropIndex === -1) return\n\n const reordered = [...currentItems]\n const [moved] = reordered.splice(dragIndex, 1)\n if (!moved) return\n\n // Determine insert position based on closest edge\n const rawInsert =\n edge === \"bottom\" || edge === \"right\"\n ? dropIndex >= dragIndex\n ? dropIndex\n : dropIndex + 1\n : dropIndex <= dragIndex\n ? dropIndex\n : dropIndex - 1\n const insertAt = Math.max(0, Math.min(rawInsert, reordered.length))\n\n reordered.splice(insertAt, 0, moved)\n options.onReorder(reordered)\n }\n\n function containerRef(el: HTMLElement) {\n // Auto-scroll when dragging near container edges\n cleanups.push(\n autoScrollForElements({\n element: el,\n canScroll: ({ source }) => source.data[SORT_ID] === sortableId,\n }),\n )\n\n // Container is a drop target for reorder finalization\n cleanups.push(\n dropTargetForElements({\n element: el,\n getData: () => ({ [SORT_ID]: sortableId }),\n canDrop: ({ source }) => source.data[SORT_ID] === sortableId,\n onDrop: () => {\n performReorder()\n activeId.set(null)\n overId.set(null)\n overEdge.set(null)\n },\n }),\n )\n\n // Keyboard reordering: Alt+Arrow keys\n const keyHandler = (e: KeyboardEvent) => {\n if (!e.altKey) return\n\n const isUp = axis === \"vertical\" ? e.key === \"ArrowUp\" : e.key === \"ArrowLeft\"\n const isDown = axis === \"vertical\" ? e.key === \"ArrowDown\" : e.key === \"ArrowRight\"\n if (!isUp && !isDown) return\n\n const focused = document.activeElement as HTMLElement | null\n if (!focused || !el.contains(focused)) return\n\n const focusedKey = focused.dataset.pyreonSortKey\n if (!focusedKey) return\n\n e.preventDefault()\n\n const currentItems = options.items()\n const currentIndex = currentItems.findIndex((item) => String(options.by(item)) === focusedKey)\n if (currentIndex === -1) return\n\n const targetIndex = isUp ? currentIndex - 1 : currentIndex + 1\n if (targetIndex < 0 || targetIndex >= currentItems.length) return\n\n const reordered = [...currentItems]\n const temp = reordered[currentIndex]\n reordered[currentIndex] = reordered[targetIndex] as T\n reordered[targetIndex] = temp as T\n options.onReorder(reordered)\n\n // Restore focus after DOM update\n requestAnimationFrame(() => {\n const items = el.querySelectorAll(\"[data-pyreon-sort-key]\")\n for (const item of items) {\n if ((item as HTMLElement).dataset.pyreonSortKey === focusedKey) {\n ;(item as HTMLElement).focus()\n break\n }\n }\n })\n }\n\n el.addEventListener(\"keydown\", keyHandler)\n cleanups.push(() => el.removeEventListener(\"keydown\", keyHandler))\n }\n\n function itemRef(key: string | number): (el: HTMLElement) => void {\n return (el: HTMLElement) => {\n el.dataset.pyreonSortKey = String(key)\n if (!el.hasAttribute(\"tabindex\")) el.setAttribute(\"tabindex\", \"0\")\n el.setAttribute(\"role\", \"listitem\")\n el.setAttribute(\"aria-roledescription\", \"sortable item\")\n\n const allowedEdges: Edge[] = axis === \"vertical\" ? [\"top\", \"bottom\"] : [\"left\", \"right\"]\n\n const cleanup = combine(\n draggable({\n element: el,\n getInitialData: () => ({\n [SORT_KEY]: key,\n [SORT_ID]: sortableId,\n }),\n onDragStart: () => activeId.set(key),\n onDrop: () => {\n queueMicrotask(() => {\n activeId.set(null)\n overId.set(null)\n overEdge.set(null)\n })\n },\n }),\n dropTargetForElements({\n element: el,\n getData: ({ input, element }) =>\n attachClosestEdge(\n { [SORT_KEY]: key, [SORT_ID]: sortableId },\n { input, element, allowedEdges },\n ),\n canDrop: ({ source }) => source.data[SORT_ID] === sortableId,\n onDragEnter: ({ self }) => {\n overId.set(key)\n overEdge.set(extractClosestEdge(self.data) as DropEdge | null)\n },\n onDrag: ({ self }) => {\n overEdge.set(extractClosestEdge(self.data) as DropEdge | null)\n },\n onDragLeave: () => {\n if (overId.peek() === key) {\n overId.set(null)\n overEdge.set(null)\n }\n },\n }),\n )\n\n cleanups.push(cleanup)\n }\n }\n\n onCleanup(() => {\n for (const cleanup of cleanups) cleanup()\n cleanups.length = 0\n activeId.set(null)\n overId.set(null)\n overEdge.set(null)\n })\n\n return { containerRef, itemRef, activeId, overId, overEdge }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/dnd",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "Signal-driven drag and drop for Pyreon — wraps @atlaskit/pragmatic-drag-and-drop",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,10 +42,10 @@
42
42
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@pyreon/core": "^0.11.3",
46
- "@pyreon/reactivity": "^0.11.3"
45
+ "@pyreon/core": "^0.11.4",
46
+ "@pyreon/reactivity": "^0.11.4"
47
47
  },
48
48
  "devDependencies": {
49
- "@pyreon/typescript": "^0.11.3"
49
+ "@pyreon/typescript": "^0.11.4"
50
50
  }
51
51
  }
@@ -267,6 +267,398 @@ describe("useDragMonitor", () => {
267
267
  })
268
268
  })
269
269
 
270
+ // ─── useSortable: containerRef and auto-scroll ────────────────────────────
271
+
272
+ describe("useSortable containerRef", () => {
273
+ it("containerRef registers auto-scroll and drop target on element", async () => {
274
+ const { useSortable } = await import("../use-sortable")
275
+ const items = signal([{ id: "1" }, { id: "2" }, { id: "3" }])
276
+
277
+ const { containerRef } = useSortable({
278
+ items,
279
+ by: (item) => item.id,
280
+ onReorder: () => {},
281
+ })
282
+
283
+ const container = document.createElement("ul")
284
+ // Should not throw — sets up autoScrollForElements + dropTargetForElements
285
+ expect(() => containerRef(container)).not.toThrow()
286
+ })
287
+
288
+ it("containerRef adds keydown event listener to element", async () => {
289
+ const { useSortable } = await import("../use-sortable")
290
+ const items = signal([{ id: "1" }, { id: "2" }])
291
+
292
+ const { containerRef } = useSortable({
293
+ items,
294
+ by: (item) => item.id,
295
+ onReorder: () => {},
296
+ })
297
+
298
+ const container = document.createElement("ul")
299
+ const spy = vi.spyOn(container, "addEventListener")
300
+ containerRef(container)
301
+
302
+ expect(spy).toHaveBeenCalledWith("keydown", expect.any(Function))
303
+ spy.mockRestore()
304
+ })
305
+ })
306
+
307
+ // ─── useSortable: overEdge signal ─────────────────────────────────────────
308
+
309
+ describe("useSortable overEdge", () => {
310
+ it("overEdge signal is initially null", async () => {
311
+ const { useSortable } = await import("../use-sortable")
312
+ const items = signal([{ id: "1" }, { id: "2" }])
313
+
314
+ const { overEdge } = useSortable({
315
+ items,
316
+ by: (item) => item.id,
317
+ onReorder: () => {},
318
+ })
319
+
320
+ expect(overEdge()).toBeNull()
321
+ })
322
+
323
+ it("overEdge is part of the returned API alongside activeId and overId", async () => {
324
+ const { useSortable } = await import("../use-sortable")
325
+ const items = signal([{ id: "a" }, { id: "b" }])
326
+
327
+ const result = useSortable({
328
+ items,
329
+ by: (item) => item.id,
330
+ onReorder: () => {},
331
+ })
332
+
333
+ expect(result).toHaveProperty("overEdge")
334
+ expect(result).toHaveProperty("activeId")
335
+ expect(result).toHaveProperty("overId")
336
+ expect(result).toHaveProperty("containerRef")
337
+ expect(result).toHaveProperty("itemRef")
338
+ })
339
+ })
340
+
341
+ // ─── useSortable: keyboard handler ─────────────────────────────────────────
342
+
343
+ describe("useSortable keyboard reordering", () => {
344
+ it("Alt+ArrowDown swaps focused item with next (vertical axis)", async () => {
345
+ const { useSortable } = await import("../use-sortable")
346
+ const reordered: { id: string }[][] = []
347
+ const items = signal([{ id: "1" }, { id: "2" }, { id: "3" }])
348
+
349
+ const { containerRef, itemRef } = useSortable({
350
+ items,
351
+ by: (item) => item.id,
352
+ onReorder: (newItems) => reordered.push(newItems),
353
+ })
354
+
355
+ // Build a container with items
356
+ const container = document.createElement("ul")
357
+ document.body.appendChild(container)
358
+ containerRef(container)
359
+
360
+ const li1 = document.createElement("li")
361
+ const li2 = document.createElement("li")
362
+ const li3 = document.createElement("li")
363
+ itemRef("1")(li1)
364
+ itemRef("2")(li2)
365
+ itemRef("3")(li3)
366
+ container.appendChild(li1)
367
+ container.appendChild(li2)
368
+ container.appendChild(li3)
369
+
370
+ // Focus the first item
371
+ li1.focus()
372
+
373
+ // Dispatch Alt+ArrowDown
374
+ const event = new KeyboardEvent("keydown", {
375
+ key: "ArrowDown",
376
+ altKey: true,
377
+ bubbles: true,
378
+ })
379
+ container.dispatchEvent(event)
380
+
381
+ expect(reordered.length).toBe(1)
382
+ expect(reordered[0]!.map((i) => i.id)).toEqual(["2", "1", "3"])
383
+
384
+ document.body.removeChild(container)
385
+ })
386
+
387
+ it("Alt+ArrowUp swaps focused item with previous (vertical axis)", async () => {
388
+ const { useSortable } = await import("../use-sortable")
389
+ const reordered: { id: string }[][] = []
390
+ const items = signal([{ id: "1" }, { id: "2" }, { id: "3" }])
391
+
392
+ const { containerRef, itemRef } = useSortable({
393
+ items,
394
+ by: (item) => item.id,
395
+ onReorder: (newItems) => reordered.push(newItems),
396
+ })
397
+
398
+ const container = document.createElement("ul")
399
+ document.body.appendChild(container)
400
+ containerRef(container)
401
+
402
+ const li1 = document.createElement("li")
403
+ const li2 = document.createElement("li")
404
+ const li3 = document.createElement("li")
405
+ itemRef("1")(li1)
406
+ itemRef("2")(li2)
407
+ itemRef("3")(li3)
408
+ container.appendChild(li1)
409
+ container.appendChild(li2)
410
+ container.appendChild(li3)
411
+
412
+ // Focus the second item
413
+ li2.focus()
414
+
415
+ const event = new KeyboardEvent("keydown", {
416
+ key: "ArrowUp",
417
+ altKey: true,
418
+ bubbles: true,
419
+ })
420
+ container.dispatchEvent(event)
421
+
422
+ expect(reordered.length).toBe(1)
423
+ expect(reordered[0]!.map((i) => i.id)).toEqual(["2", "1", "3"])
424
+
425
+ document.body.removeChild(container)
426
+ })
427
+
428
+ it("ignores keyboard events without Alt key", async () => {
429
+ const { useSortable } = await import("../use-sortable")
430
+ const reordered: { id: string }[][] = []
431
+ const items = signal([{ id: "1" }, { id: "2" }])
432
+
433
+ const { containerRef, itemRef } = useSortable({
434
+ items,
435
+ by: (item) => item.id,
436
+ onReorder: (newItems) => reordered.push(newItems),
437
+ })
438
+
439
+ const container = document.createElement("ul")
440
+ document.body.appendChild(container)
441
+ containerRef(container)
442
+
443
+ const li1 = document.createElement("li")
444
+ const li2 = document.createElement("li")
445
+ itemRef("1")(li1)
446
+ itemRef("2")(li2)
447
+ container.appendChild(li1)
448
+ container.appendChild(li2)
449
+
450
+ li1.focus()
451
+
452
+ const event = new KeyboardEvent("keydown", {
453
+ key: "ArrowDown",
454
+ altKey: false,
455
+ bubbles: true,
456
+ })
457
+ container.dispatchEvent(event)
458
+
459
+ expect(reordered.length).toBe(0)
460
+
461
+ document.body.removeChild(container)
462
+ })
463
+
464
+ it("ignores Alt+ArrowDown at the last item (boundary)", async () => {
465
+ const { useSortable } = await import("../use-sortable")
466
+ const reordered: { id: string }[][] = []
467
+ const items = signal([{ id: "1" }, { id: "2" }])
468
+
469
+ const { containerRef, itemRef } = useSortable({
470
+ items,
471
+ by: (item) => item.id,
472
+ onReorder: (newItems) => reordered.push(newItems),
473
+ })
474
+
475
+ const container = document.createElement("ul")
476
+ document.body.appendChild(container)
477
+ containerRef(container)
478
+
479
+ const li1 = document.createElement("li")
480
+ const li2 = document.createElement("li")
481
+ itemRef("1")(li1)
482
+ itemRef("2")(li2)
483
+ container.appendChild(li1)
484
+ container.appendChild(li2)
485
+
486
+ // Focus last item
487
+ li2.focus()
488
+
489
+ const event = new KeyboardEvent("keydown", {
490
+ key: "ArrowDown",
491
+ altKey: true,
492
+ bubbles: true,
493
+ })
494
+ container.dispatchEvent(event)
495
+
496
+ expect(reordered.length).toBe(0) // no reorder at boundary
497
+
498
+ document.body.removeChild(container)
499
+ })
500
+
501
+ it("horizontal axis uses ArrowLeft/ArrowRight", async () => {
502
+ const { useSortable } = await import("../use-sortable")
503
+ const reordered: { id: string }[][] = []
504
+ const items = signal([{ id: "1" }, { id: "2" }, { id: "3" }])
505
+
506
+ const { containerRef, itemRef } = useSortable({
507
+ items,
508
+ by: (item) => item.id,
509
+ onReorder: (newItems) => reordered.push(newItems),
510
+ axis: "horizontal",
511
+ })
512
+
513
+ const container = document.createElement("div")
514
+ document.body.appendChild(container)
515
+ containerRef(container)
516
+
517
+ const el1 = document.createElement("div")
518
+ const el2 = document.createElement("div")
519
+ const el3 = document.createElement("div")
520
+ itemRef("1")(el1)
521
+ itemRef("2")(el2)
522
+ itemRef("3")(el3)
523
+ container.appendChild(el1)
524
+ container.appendChild(el2)
525
+ container.appendChild(el3)
526
+
527
+ el1.focus()
528
+
529
+ // ArrowDown should be ignored in horizontal mode
530
+ container.dispatchEvent(
531
+ new KeyboardEvent("keydown", { key: "ArrowDown", altKey: true, bubbles: true }),
532
+ )
533
+ expect(reordered.length).toBe(0)
534
+
535
+ // ArrowRight should work
536
+ container.dispatchEvent(
537
+ new KeyboardEvent("keydown", { key: "ArrowRight", altKey: true, bubbles: true }),
538
+ )
539
+ expect(reordered.length).toBe(1)
540
+ expect(reordered[0]!.map((i) => i.id)).toEqual(["2", "1", "3"])
541
+
542
+ document.body.removeChild(container)
543
+ })
544
+ })
545
+
546
+ // ─── useFileDrop: MIME type filtering ──────────────────────────────────────
547
+
548
+ describe("useFileDrop MIME filtering logic", () => {
549
+ it("matchesAccept handles extension patterns (.pdf)", async () => {
550
+ // We can't test matchesAccept directly since it's private,
551
+ // but we can verify the useFileDrop options are accepted
552
+ const { useFileDrop } = await import("../use-file-drop")
553
+ const el = document.createElement("div")
554
+ const { isOver } = useFileDrop({
555
+ element: () => el,
556
+ accept: [".pdf", ".doc", ".docx"],
557
+ onDrop: () => {},
558
+ })
559
+ expect(isOver()).toBe(false)
560
+ })
561
+
562
+ it("matchesAccept handles wildcard MIME types (image/*)", async () => {
563
+ const { useFileDrop } = await import("../use-file-drop")
564
+ const el = document.createElement("div")
565
+ const { isOver } = useFileDrop({
566
+ element: () => el,
567
+ accept: ["image/*"],
568
+ onDrop: () => {},
569
+ })
570
+ expect(isOver()).toBe(false)
571
+ })
572
+
573
+ it("matchesAccept handles exact MIME types (application/json)", async () => {
574
+ const { useFileDrop } = await import("../use-file-drop")
575
+ const el = document.createElement("div")
576
+ const { isOver } = useFileDrop({
577
+ element: () => el,
578
+ accept: ["application/json", "text/plain"],
579
+ onDrop: () => {},
580
+ })
581
+ expect(isOver()).toBe(false)
582
+ })
583
+
584
+ it("handles maxFiles option", async () => {
585
+ const { useFileDrop } = await import("../use-file-drop")
586
+ const el = document.createElement("div")
587
+ const { isOver } = useFileDrop({
588
+ element: () => el,
589
+ accept: ["image/*"],
590
+ maxFiles: 1,
591
+ onDrop: () => {},
592
+ })
593
+ expect(isOver()).toBe(false)
594
+ })
595
+
596
+ it("handles null element gracefully", async () => {
597
+ const { useFileDrop } = await import("../use-file-drop")
598
+ const { isOver, isDraggingFiles } = useFileDrop({
599
+ element: () => null,
600
+ onDrop: () => {},
601
+ })
602
+ expect(isOver()).toBe(false)
603
+ expect(isDraggingFiles()).toBe(false)
604
+ })
605
+
606
+ it("accepts disabled as reactive getter", async () => {
607
+ const { useFileDrop } = await import("../use-file-drop")
608
+ const el = document.createElement("div")
609
+ const isDisabled = signal(false)
610
+ const { isOver } = useFileDrop({
611
+ element: () => el,
612
+ disabled: isDisabled,
613
+ onDrop: () => {},
614
+ })
615
+ expect(isOver()).toBe(false)
616
+ })
617
+ })
618
+
619
+ // ─── useDragMonitor: canMonitor filter ─────────────────────────────────────
620
+
621
+ describe("useDragMonitor canMonitor filter", () => {
622
+ it("canMonitor option is a function that receives drag data", async () => {
623
+ const { useDragMonitor } = await import("../use-drag-monitor")
624
+ const canMonitorCalls: unknown[] = []
625
+
626
+ const { isDragging } = useDragMonitor({
627
+ canMonitor: (data) => {
628
+ canMonitorCalls.push(data)
629
+ return data.type === "card"
630
+ },
631
+ })
632
+
633
+ expect(isDragging()).toBe(false)
634
+ // canMonitor is passed to the underlying pragmatic-dnd monitor
635
+ // We can't trigger real drag events, but verify setup doesn't throw
636
+ })
637
+
638
+ it("creates monitor without canMonitor (monitors all drags)", async () => {
639
+ const { useDragMonitor } = await import("../use-drag-monitor")
640
+ const { isDragging, dragData } = useDragMonitor()
641
+
642
+ expect(isDragging()).toBe(false)
643
+ expect(dragData()).toBeNull()
644
+ })
645
+
646
+ it("creates monitor with all options", async () => {
647
+ const { useDragMonitor } = await import("../use-drag-monitor")
648
+ const starts: unknown[] = []
649
+ const drops: unknown[] = []
650
+
651
+ const { isDragging } = useDragMonitor({
652
+ canMonitor: (data) => data.type === "task",
653
+ onDragStart: (data) => starts.push(data),
654
+ onDrop: (source, target) => drops.push({ source, target }),
655
+ })
656
+
657
+ expect(isDragging()).toBe(false)
658
+ // Callbacks are registered — verify no errors on setup
659
+ })
660
+ })
661
+
270
662
  // ─── Module exports ─────────────────────────────────────────────────────────
271
663
 
272
664
  describe("module exports", () => {
@@ -218,6 +218,9 @@ export function useSortable<T>(options: UseSortableOptions<T>): UseSortableResul
218
218
  onCleanup(() => {
219
219
  for (const cleanup of cleanups) cleanup()
220
220
  cleanups.length = 0
221
+ activeId.set(null)
222
+ overId.set(null)
223
+ overEdge.set(null)
221
224
  })
222
225
 
223
226
  return { containerRef, itemRef, activeId, overId, overEdge }