@spark-ui/hooks 11.6.1 → 11.7.0

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [11.7.0](https://github.com/leboncoin/spark-web/compare/v11.6.1...v11.7.0) (2025-11-24)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **hooks:** make generic usage optional on useSortableList ([e435a16](https://github.com/leboncoin/spark-web/commit/e435a1614201599a19dc2cc5125b88e474a4c32a))
11
+
12
+ ### Features
13
+
14
+ - **hooks:** useSortableList hook ([dd34c3d](https://github.com/leboncoin/spark-web/commit/dd34c3d7856f3fe4426c55d7e7600f0e2df38aa7))
15
+
6
16
  ## [11.6.1](https://github.com/leboncoin/spark-web/compare/v11.6.0...v11.6.1) (2025-11-21)
7
17
 
8
18
  **Note:** Version bump only for package @spark-ui/hooks
@@ -0,0 +1,98 @@
1
+ import { Ref } from 'react';
2
+
3
+ interface UseSortableListOptions<T> {
4
+ /**
5
+ * The list of items to be sortable
6
+ */
7
+ items: T[];
8
+ /**
9
+ * Callback called when items are reordered
10
+ * @param newItems - The reordered items array
11
+ */
12
+ onReorder: (newItems: T[]) => void;
13
+ /**
14
+ * Function to generate a unique key for each item
15
+ * @param item - The item to generate a key for
16
+ * @returns A unique string key for the item
17
+ */
18
+ getItemKey: (item: T) => string;
19
+ }
20
+ interface SortableItemProps<TElement extends HTMLElement = HTMLElement> {
21
+ /**
22
+ * Whether the item is draggable
23
+ */
24
+ draggable: boolean;
25
+ /**
26
+ * Handler for drag start event
27
+ */
28
+ onDragStart: (e: React.DragEvent) => void;
29
+ /**
30
+ * Handler for drag enter event
31
+ */
32
+ onDragEnter: (e: React.DragEvent) => void;
33
+ /**
34
+ * Handler for drag over event
35
+ */
36
+ onDragOver: (e: React.DragEvent) => void;
37
+ /**
38
+ * Handler for drag leave event
39
+ */
40
+ onDragLeave: (e: React.DragEvent) => void;
41
+ /**
42
+ * Handler for drag end event
43
+ */
44
+ onDragEnd: (e: React.DragEvent) => void;
45
+ /**
46
+ * Handler for drop event
47
+ */
48
+ onDrop: (e: React.DragEvent) => void;
49
+ /**
50
+ * Handler for keyboard navigation
51
+ */
52
+ onKeyDown: (e: React.KeyboardEvent) => void;
53
+ /**
54
+ * Tab index for keyboard navigation
55
+ */
56
+ tabIndex: number;
57
+ /**
58
+ * Ref callback to attach to the item element
59
+ */
60
+ ref: Ref<TElement>;
61
+ }
62
+ interface UseSortableListReturn<T> {
63
+ /**
64
+ * Get props to spread on a sortable item element (includes ref)
65
+ * @param item - The item to get props for
66
+ * @param index - The current index of the item in the list
67
+ * @returns Props object to spread on the element
68
+ */
69
+ getItemProps: <TElement extends HTMLElement = HTMLElement>(item: T, index: number) => SortableItemProps<TElement>;
70
+ }
71
+ /**
72
+ * Hook to make a list of items sortable via drag and drop and keyboard navigation
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * const { getItemProps } = useSortableList({
77
+ * items: myItems,
78
+ * onReorder: setMyItems,
79
+ * getItemKey: (item) => item.id
80
+ * })
81
+ *
82
+ * return (
83
+ * <ul>
84
+ * {myItems.map((item, index) => (
85
+ * <li
86
+ * key={getItemKey(item)}
87
+ * {...getItemProps(item, index)}
88
+ * >
89
+ * {item.name}
90
+ * </li>
91
+ * ))}
92
+ * </ul>
93
+ * )
94
+ * ```
95
+ */
96
+ declare function useSortableList<T>({ items, onReorder, getItemKey, }: UseSortableListOptions<T>): UseSortableListReturn<T>;
97
+
98
+ export { type SortableItemProps, type UseSortableListOptions, type UseSortableListReturn, useSortableList };
@@ -0,0 +1,98 @@
1
+ import { Ref } from 'react';
2
+
3
+ interface UseSortableListOptions<T> {
4
+ /**
5
+ * The list of items to be sortable
6
+ */
7
+ items: T[];
8
+ /**
9
+ * Callback called when items are reordered
10
+ * @param newItems - The reordered items array
11
+ */
12
+ onReorder: (newItems: T[]) => void;
13
+ /**
14
+ * Function to generate a unique key for each item
15
+ * @param item - The item to generate a key for
16
+ * @returns A unique string key for the item
17
+ */
18
+ getItemKey: (item: T) => string;
19
+ }
20
+ interface SortableItemProps<TElement extends HTMLElement = HTMLElement> {
21
+ /**
22
+ * Whether the item is draggable
23
+ */
24
+ draggable: boolean;
25
+ /**
26
+ * Handler for drag start event
27
+ */
28
+ onDragStart: (e: React.DragEvent) => void;
29
+ /**
30
+ * Handler for drag enter event
31
+ */
32
+ onDragEnter: (e: React.DragEvent) => void;
33
+ /**
34
+ * Handler for drag over event
35
+ */
36
+ onDragOver: (e: React.DragEvent) => void;
37
+ /**
38
+ * Handler for drag leave event
39
+ */
40
+ onDragLeave: (e: React.DragEvent) => void;
41
+ /**
42
+ * Handler for drag end event
43
+ */
44
+ onDragEnd: (e: React.DragEvent) => void;
45
+ /**
46
+ * Handler for drop event
47
+ */
48
+ onDrop: (e: React.DragEvent) => void;
49
+ /**
50
+ * Handler for keyboard navigation
51
+ */
52
+ onKeyDown: (e: React.KeyboardEvent) => void;
53
+ /**
54
+ * Tab index for keyboard navigation
55
+ */
56
+ tabIndex: number;
57
+ /**
58
+ * Ref callback to attach to the item element
59
+ */
60
+ ref: Ref<TElement>;
61
+ }
62
+ interface UseSortableListReturn<T> {
63
+ /**
64
+ * Get props to spread on a sortable item element (includes ref)
65
+ * @param item - The item to get props for
66
+ * @param index - The current index of the item in the list
67
+ * @returns Props object to spread on the element
68
+ */
69
+ getItemProps: <TElement extends HTMLElement = HTMLElement>(item: T, index: number) => SortableItemProps<TElement>;
70
+ }
71
+ /**
72
+ * Hook to make a list of items sortable via drag and drop and keyboard navigation
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * const { getItemProps } = useSortableList({
77
+ * items: myItems,
78
+ * onReorder: setMyItems,
79
+ * getItemKey: (item) => item.id
80
+ * })
81
+ *
82
+ * return (
83
+ * <ul>
84
+ * {myItems.map((item, index) => (
85
+ * <li
86
+ * key={getItemKey(item)}
87
+ * {...getItemProps(item, index)}
88
+ * >
89
+ * {item.name}
90
+ * </li>
91
+ * ))}
92
+ * </ul>
93
+ * )
94
+ * ```
95
+ */
96
+ declare function useSortableList<T>({ items, onReorder, getItemKey, }: UseSortableListOptions<T>): UseSortableListReturn<T>;
97
+
98
+ export { type SortableItemProps, type UseSortableListOptions, type UseSortableListReturn, useSortableList };
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/use-sortable-list/index.ts
21
+ var use_sortable_list_exports = {};
22
+ __export(use_sortable_list_exports, {
23
+ useSortableList: () => useSortableList
24
+ });
25
+ module.exports = __toCommonJS(use_sortable_list_exports);
26
+
27
+ // src/use-sortable-list/useSortableList.tsx
28
+ var import_react = require("react");
29
+ function useSortableList({
30
+ items,
31
+ onReorder,
32
+ getItemKey
33
+ }) {
34
+ const itemRefs = (0, import_react.useRef)(/* @__PURE__ */ new Map());
35
+ const handleDragStart = (e, index) => {
36
+ e.dataTransfer.effectAllowed = "move";
37
+ e.dataTransfer.setData("text/plain", index.toString());
38
+ e.currentTarget.style.opacity = "var(--opacity-dim-3)";
39
+ };
40
+ const handleDragEnter = (e) => {
41
+ e.preventDefault();
42
+ e.currentTarget.setAttribute("data-drag-over", "true");
43
+ };
44
+ const handleDragOver = (e) => {
45
+ e.preventDefault();
46
+ e.dataTransfer.dropEffect = "move";
47
+ };
48
+ const handleDragLeave = (e) => {
49
+ const relatedTarget = e.relatedTarget;
50
+ const currentTarget = e.currentTarget;
51
+ if (!relatedTarget || !currentTarget.contains(relatedTarget)) {
52
+ e.currentTarget.removeAttribute("data-drag-over");
53
+ }
54
+ };
55
+ const handleDragEnd = (e) => {
56
+ ;
57
+ e.currentTarget.style.opacity = "";
58
+ e.currentTarget.removeAttribute("data-drag-over");
59
+ };
60
+ const handleDrop = (e, dropIndex) => {
61
+ e.preventDefault();
62
+ e.currentTarget.removeAttribute("data-drag-over");
63
+ const dragIndex = parseInt(e.dataTransfer.getData("text/plain"), 10);
64
+ if (!isNaN(dragIndex) && dragIndex !== dropIndex && dragIndex >= 0 && dragIndex < items.length) {
65
+ const newItems = [...items];
66
+ const [removed] = newItems.splice(dragIndex, 1);
67
+ if (removed) {
68
+ newItems.splice(dropIndex, 0, removed);
69
+ onReorder(newItems);
70
+ }
71
+ }
72
+ };
73
+ const handleKeyDown = (e, _item, index) => {
74
+ let direction = 0;
75
+ if (e.key === "ArrowUp") {
76
+ direction = -1;
77
+ } else if (e.key === "ArrowDown") {
78
+ direction = 1;
79
+ } else {
80
+ return;
81
+ }
82
+ const targetIndex = index + direction;
83
+ if (targetIndex < 0 || targetIndex >= items.length) return;
84
+ e.preventDefault();
85
+ const newItems = [...items];
86
+ const currentItem = newItems[index];
87
+ const targetItem = newItems[targetIndex];
88
+ if (currentItem && targetItem) {
89
+ ;
90
+ [newItems[index], newItems[targetIndex]] = [targetItem, currentItem];
91
+ onReorder(newItems);
92
+ requestAnimationFrame(() => {
93
+ const itemKey = getItemKey(currentItem);
94
+ const element = itemRefs.current.get(itemKey);
95
+ element?.focus();
96
+ });
97
+ }
98
+ };
99
+ const getItemProps = (item, index) => {
100
+ const itemKey = getItemKey(item);
101
+ return {
102
+ draggable: true,
103
+ onDragStart: (e) => handleDragStart(e, index),
104
+ onDragEnter: handleDragEnter,
105
+ onDragOver: handleDragOver,
106
+ onDragLeave: handleDragLeave,
107
+ onDragEnd: handleDragEnd,
108
+ onDrop: (e) => handleDrop(e, index),
109
+ onKeyDown: (e) => handleKeyDown(e, item, index),
110
+ tabIndex: 0,
111
+ ref: (node) => {
112
+ if (node) {
113
+ itemRefs.current.set(itemKey, node);
114
+ } else {
115
+ itemRefs.current.delete(itemKey);
116
+ }
117
+ }
118
+ };
119
+ };
120
+ return {
121
+ getItemProps
122
+ };
123
+ }
124
+ // Annotate the CommonJS export names for ESM import in node:
125
+ 0 && (module.exports = {
126
+ useSortableList
127
+ });
128
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/use-sortable-list/index.ts","../../src/use-sortable-list/useSortableList.tsx"],"sourcesContent":["export { useSortableList } from './useSortableList'\nexport type {\n UseSortableListOptions,\n UseSortableListReturn,\n SortableItemProps,\n} from './useSortableList'\n","/* eslint-disable max-lines-per-function */\nimport { Ref, useRef } from 'react'\n\nexport interface UseSortableListOptions<T> {\n /**\n * The list of items to be sortable\n */\n items: T[]\n /**\n * Callback called when items are reordered\n * @param newItems - The reordered items array\n */\n onReorder: (newItems: T[]) => void\n /**\n * Function to generate a unique key for each item\n * @param item - The item to generate a key for\n * @returns A unique string key for the item\n */\n getItemKey: (item: T) => string\n}\n\nexport interface SortableItemProps<TElement extends HTMLElement = HTMLElement> {\n /**\n * Whether the item is draggable\n */\n draggable: boolean\n /**\n * Handler for drag start event\n */\n onDragStart: (e: React.DragEvent) => void\n /**\n * Handler for drag enter event\n */\n onDragEnter: (e: React.DragEvent) => void\n /**\n * Handler for drag over event\n */\n onDragOver: (e: React.DragEvent) => void\n /**\n * Handler for drag leave event\n */\n onDragLeave: (e: React.DragEvent) => void\n /**\n * Handler for drag end event\n */\n onDragEnd: (e: React.DragEvent) => void\n /**\n * Handler for drop event\n */\n onDrop: (e: React.DragEvent) => void\n /**\n * Handler for keyboard navigation\n */\n onKeyDown: (e: React.KeyboardEvent) => void\n /**\n * Tab index for keyboard navigation\n */\n tabIndex: number\n /**\n * Ref callback to attach to the item element\n */\n ref: Ref<TElement>\n}\n\nexport interface UseSortableListReturn<T> {\n /**\n * Get props to spread on a sortable item element (includes ref)\n * @param item - The item to get props for\n * @param index - The current index of the item in the list\n * @returns Props object to spread on the element\n */\n getItemProps: <TElement extends HTMLElement = HTMLElement>(\n item: T,\n index: number\n ) => SortableItemProps<TElement>\n}\n\n/**\n * Hook to make a list of items sortable via drag and drop and keyboard navigation\n *\n * @example\n * ```tsx\n * const { getItemProps } = useSortableList({\n * items: myItems,\n * onReorder: setMyItems,\n * getItemKey: (item) => item.id\n * })\n *\n * return (\n * <ul>\n * {myItems.map((item, index) => (\n * <li\n * key={getItemKey(item)}\n * {...getItemProps(item, index)}\n * >\n * {item.name}\n * </li>\n * ))}\n * </ul>\n * )\n * ```\n */\nexport function useSortableList<T>({\n items,\n onReorder,\n getItemKey,\n}: UseSortableListOptions<T>): UseSortableListReturn<T> {\n // Refs to maintain focus after keyboard reordering\n // Uses a key based on the item rather than index\n const itemRefs = useRef<Map<string, HTMLElement>>(new Map())\n\n const handleDragStart = (e: React.DragEvent, index: number) => {\n e.dataTransfer.effectAllowed = 'move'\n e.dataTransfer.setData('text/plain', index.toString())\n // Apply inline style for opacity during drag\n ;(e.currentTarget as HTMLElement).style.opacity = 'var(--opacity-dim-3)'\n }\n\n const handleDragEnter = (e: React.DragEvent) => {\n e.preventDefault()\n e.currentTarget.setAttribute('data-drag-over', 'true')\n }\n\n const handleDragOver = (e: React.DragEvent) => {\n e.preventDefault()\n e.dataTransfer.dropEffect = 'move'\n }\n\n const handleDragLeave = (e: React.DragEvent) => {\n // Only remove the attribute if we're actually leaving the element\n // (not just moving to a child element)\n const relatedTarget = e.relatedTarget as Node | null\n const currentTarget = e.currentTarget as Node\n\n if (!relatedTarget || !currentTarget.contains(relatedTarget)) {\n e.currentTarget.removeAttribute('data-drag-over')\n }\n }\n\n const handleDragEnd = (e: React.DragEvent) => {\n // Remove inline style for opacity\n ;(e.currentTarget as HTMLElement).style.opacity = ''\n e.currentTarget.removeAttribute('data-drag-over')\n }\n\n const handleDrop = (e: React.DragEvent, dropIndex: number) => {\n e.preventDefault()\n e.currentTarget.removeAttribute('data-drag-over')\n\n const dragIndex = parseInt(e.dataTransfer.getData('text/plain'), 10)\n\n if (\n !isNaN(dragIndex) &&\n dragIndex !== dropIndex &&\n dragIndex >= 0 &&\n dragIndex < items.length\n ) {\n const newItems = [...items]\n const [removed] = newItems.splice(dragIndex, 1)\n if (removed) {\n newItems.splice(dropIndex, 0, removed)\n onReorder(newItems)\n }\n }\n }\n\n const handleKeyDown = (e: React.KeyboardEvent, _item: T, index: number) => {\n // Determine direction\n let direction = 0\n if (e.key === 'ArrowUp') {\n direction = -1\n } else if (e.key === 'ArrowDown') {\n direction = 1\n } else {\n return\n }\n\n const targetIndex = index + direction\n\n // Validate move is within bounds\n if (targetIndex < 0 || targetIndex >= items.length) return\n\n e.preventDefault()\n\n // Create new array and swap items\n const newItems = [...items]\n const currentItem = newItems[index]\n const targetItem = newItems[targetIndex]\n\n if (currentItem && targetItem) {\n ;[newItems[index], newItems[targetIndex]] = [targetItem, currentItem]\n onReorder(newItems)\n\n // Maintain focus on the moved item\n requestAnimationFrame(() => {\n const itemKey = getItemKey(currentItem)\n const element = itemRefs.current.get(itemKey)\n element?.focus()\n })\n }\n }\n\n const getItemProps = <TElement extends HTMLElement = HTMLElement>(\n item: T,\n index: number\n ): SortableItemProps<TElement> => {\n const itemKey = getItemKey(item)\n\n return {\n draggable: true,\n onDragStart: e => handleDragStart(e, index),\n onDragEnter: handleDragEnter,\n onDragOver: handleDragOver,\n onDragLeave: handleDragLeave,\n onDragEnd: handleDragEnd,\n onDrop: e => handleDrop(e, index),\n onKeyDown: e => handleKeyDown(e, item, index),\n tabIndex: 0,\n ref: (node: TElement | null) => {\n if (node) {\n itemRefs.current.set(itemKey, node as HTMLElement)\n } else {\n itemRefs.current.delete(itemKey)\n }\n },\n }\n }\n\n return {\n getItemProps,\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,101 @@
1
+ // src/use-sortable-list/useSortableList.tsx
2
+ import { useRef } from "react";
3
+ function useSortableList({
4
+ items,
5
+ onReorder,
6
+ getItemKey
7
+ }) {
8
+ const itemRefs = useRef(/* @__PURE__ */ new Map());
9
+ const handleDragStart = (e, index) => {
10
+ e.dataTransfer.effectAllowed = "move";
11
+ e.dataTransfer.setData("text/plain", index.toString());
12
+ e.currentTarget.style.opacity = "var(--opacity-dim-3)";
13
+ };
14
+ const handleDragEnter = (e) => {
15
+ e.preventDefault();
16
+ e.currentTarget.setAttribute("data-drag-over", "true");
17
+ };
18
+ const handleDragOver = (e) => {
19
+ e.preventDefault();
20
+ e.dataTransfer.dropEffect = "move";
21
+ };
22
+ const handleDragLeave = (e) => {
23
+ const relatedTarget = e.relatedTarget;
24
+ const currentTarget = e.currentTarget;
25
+ if (!relatedTarget || !currentTarget.contains(relatedTarget)) {
26
+ e.currentTarget.removeAttribute("data-drag-over");
27
+ }
28
+ };
29
+ const handleDragEnd = (e) => {
30
+ ;
31
+ e.currentTarget.style.opacity = "";
32
+ e.currentTarget.removeAttribute("data-drag-over");
33
+ };
34
+ const handleDrop = (e, dropIndex) => {
35
+ e.preventDefault();
36
+ e.currentTarget.removeAttribute("data-drag-over");
37
+ const dragIndex = parseInt(e.dataTransfer.getData("text/plain"), 10);
38
+ if (!isNaN(dragIndex) && dragIndex !== dropIndex && dragIndex >= 0 && dragIndex < items.length) {
39
+ const newItems = [...items];
40
+ const [removed] = newItems.splice(dragIndex, 1);
41
+ if (removed) {
42
+ newItems.splice(dropIndex, 0, removed);
43
+ onReorder(newItems);
44
+ }
45
+ }
46
+ };
47
+ const handleKeyDown = (e, _item, index) => {
48
+ let direction = 0;
49
+ if (e.key === "ArrowUp") {
50
+ direction = -1;
51
+ } else if (e.key === "ArrowDown") {
52
+ direction = 1;
53
+ } else {
54
+ return;
55
+ }
56
+ const targetIndex = index + direction;
57
+ if (targetIndex < 0 || targetIndex >= items.length) return;
58
+ e.preventDefault();
59
+ const newItems = [...items];
60
+ const currentItem = newItems[index];
61
+ const targetItem = newItems[targetIndex];
62
+ if (currentItem && targetItem) {
63
+ ;
64
+ [newItems[index], newItems[targetIndex]] = [targetItem, currentItem];
65
+ onReorder(newItems);
66
+ requestAnimationFrame(() => {
67
+ const itemKey = getItemKey(currentItem);
68
+ const element = itemRefs.current.get(itemKey);
69
+ element?.focus();
70
+ });
71
+ }
72
+ };
73
+ const getItemProps = (item, index) => {
74
+ const itemKey = getItemKey(item);
75
+ return {
76
+ draggable: true,
77
+ onDragStart: (e) => handleDragStart(e, index),
78
+ onDragEnter: handleDragEnter,
79
+ onDragOver: handleDragOver,
80
+ onDragLeave: handleDragLeave,
81
+ onDragEnd: handleDragEnd,
82
+ onDrop: (e) => handleDrop(e, index),
83
+ onKeyDown: (e) => handleKeyDown(e, item, index),
84
+ tabIndex: 0,
85
+ ref: (node) => {
86
+ if (node) {
87
+ itemRefs.current.set(itemKey, node);
88
+ } else {
89
+ itemRefs.current.delete(itemKey);
90
+ }
91
+ }
92
+ };
93
+ };
94
+ return {
95
+ getItemProps
96
+ };
97
+ }
98
+ export {
99
+ useSortableList
100
+ };
101
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/use-sortable-list/useSortableList.tsx"],"sourcesContent":["/* eslint-disable max-lines-per-function */\nimport { Ref, useRef } from 'react'\n\nexport interface UseSortableListOptions<T> {\n /**\n * The list of items to be sortable\n */\n items: T[]\n /**\n * Callback called when items are reordered\n * @param newItems - The reordered items array\n */\n onReorder: (newItems: T[]) => void\n /**\n * Function to generate a unique key for each item\n * @param item - The item to generate a key for\n * @returns A unique string key for the item\n */\n getItemKey: (item: T) => string\n}\n\nexport interface SortableItemProps<TElement extends HTMLElement = HTMLElement> {\n /**\n * Whether the item is draggable\n */\n draggable: boolean\n /**\n * Handler for drag start event\n */\n onDragStart: (e: React.DragEvent) => void\n /**\n * Handler for drag enter event\n */\n onDragEnter: (e: React.DragEvent) => void\n /**\n * Handler for drag over event\n */\n onDragOver: (e: React.DragEvent) => void\n /**\n * Handler for drag leave event\n */\n onDragLeave: (e: React.DragEvent) => void\n /**\n * Handler for drag end event\n */\n onDragEnd: (e: React.DragEvent) => void\n /**\n * Handler for drop event\n */\n onDrop: (e: React.DragEvent) => void\n /**\n * Handler for keyboard navigation\n */\n onKeyDown: (e: React.KeyboardEvent) => void\n /**\n * Tab index for keyboard navigation\n */\n tabIndex: number\n /**\n * Ref callback to attach to the item element\n */\n ref: Ref<TElement>\n}\n\nexport interface UseSortableListReturn<T> {\n /**\n * Get props to spread on a sortable item element (includes ref)\n * @param item - The item to get props for\n * @param index - The current index of the item in the list\n * @returns Props object to spread on the element\n */\n getItemProps: <TElement extends HTMLElement = HTMLElement>(\n item: T,\n index: number\n ) => SortableItemProps<TElement>\n}\n\n/**\n * Hook to make a list of items sortable via drag and drop and keyboard navigation\n *\n * @example\n * ```tsx\n * const { getItemProps } = useSortableList({\n * items: myItems,\n * onReorder: setMyItems,\n * getItemKey: (item) => item.id\n * })\n *\n * return (\n * <ul>\n * {myItems.map((item, index) => (\n * <li\n * key={getItemKey(item)}\n * {...getItemProps(item, index)}\n * >\n * {item.name}\n * </li>\n * ))}\n * </ul>\n * )\n * ```\n */\nexport function useSortableList<T>({\n items,\n onReorder,\n getItemKey,\n}: UseSortableListOptions<T>): UseSortableListReturn<T> {\n // Refs to maintain focus after keyboard reordering\n // Uses a key based on the item rather than index\n const itemRefs = useRef<Map<string, HTMLElement>>(new Map())\n\n const handleDragStart = (e: React.DragEvent, index: number) => {\n e.dataTransfer.effectAllowed = 'move'\n e.dataTransfer.setData('text/plain', index.toString())\n // Apply inline style for opacity during drag\n ;(e.currentTarget as HTMLElement).style.opacity = 'var(--opacity-dim-3)'\n }\n\n const handleDragEnter = (e: React.DragEvent) => {\n e.preventDefault()\n e.currentTarget.setAttribute('data-drag-over', 'true')\n }\n\n const handleDragOver = (e: React.DragEvent) => {\n e.preventDefault()\n e.dataTransfer.dropEffect = 'move'\n }\n\n const handleDragLeave = (e: React.DragEvent) => {\n // Only remove the attribute if we're actually leaving the element\n // (not just moving to a child element)\n const relatedTarget = e.relatedTarget as Node | null\n const currentTarget = e.currentTarget as Node\n\n if (!relatedTarget || !currentTarget.contains(relatedTarget)) {\n e.currentTarget.removeAttribute('data-drag-over')\n }\n }\n\n const handleDragEnd = (e: React.DragEvent) => {\n // Remove inline style for opacity\n ;(e.currentTarget as HTMLElement).style.opacity = ''\n e.currentTarget.removeAttribute('data-drag-over')\n }\n\n const handleDrop = (e: React.DragEvent, dropIndex: number) => {\n e.preventDefault()\n e.currentTarget.removeAttribute('data-drag-over')\n\n const dragIndex = parseInt(e.dataTransfer.getData('text/plain'), 10)\n\n if (\n !isNaN(dragIndex) &&\n dragIndex !== dropIndex &&\n dragIndex >= 0 &&\n dragIndex < items.length\n ) {\n const newItems = [...items]\n const [removed] = newItems.splice(dragIndex, 1)\n if (removed) {\n newItems.splice(dropIndex, 0, removed)\n onReorder(newItems)\n }\n }\n }\n\n const handleKeyDown = (e: React.KeyboardEvent, _item: T, index: number) => {\n // Determine direction\n let direction = 0\n if (e.key === 'ArrowUp') {\n direction = -1\n } else if (e.key === 'ArrowDown') {\n direction = 1\n } else {\n return\n }\n\n const targetIndex = index + direction\n\n // Validate move is within bounds\n if (targetIndex < 0 || targetIndex >= items.length) return\n\n e.preventDefault()\n\n // Create new array and swap items\n const newItems = [...items]\n const currentItem = newItems[index]\n const targetItem = newItems[targetIndex]\n\n if (currentItem && targetItem) {\n ;[newItems[index], newItems[targetIndex]] = [targetItem, currentItem]\n onReorder(newItems)\n\n // Maintain focus on the moved item\n requestAnimationFrame(() => {\n const itemKey = getItemKey(currentItem)\n const element = itemRefs.current.get(itemKey)\n element?.focus()\n })\n }\n }\n\n const getItemProps = <TElement extends HTMLElement = HTMLElement>(\n item: T,\n index: number\n ): SortableItemProps<TElement> => {\n const itemKey = getItemKey(item)\n\n return {\n draggable: true,\n onDragStart: e => handleDragStart(e, index),\n onDragEnter: handleDragEnter,\n onDragOver: handleDragOver,\n onDragLeave: handleDragLeave,\n onDragEnd: handleDragEnd,\n onDrop: e => handleDrop(e, index),\n onKeyDown: e => handleKeyDown(e, item, index),\n tabIndex: 0,\n ref: (node: TElement | null) => {\n if (node) {\n itemRefs.current.set(itemKey, node as HTMLElement)\n } else {\n itemRefs.current.delete(itemKey)\n }\n },\n }\n }\n\n return {\n getItemProps,\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spark-ui/hooks",
3
- "version": "11.6.1",
3
+ "version": "11.7.0",
4
4
  "description": "Common hooks for Spark UI",
5
5
  "exports": {
6
6
  "./*": {
@@ -49,5 +49,5 @@
49
49
  },
50
50
  "homepage": "https://sparkui.vercel.app",
51
51
  "license": "MIT",
52
- "gitHead": "98834c44402e2b699093384f86a18249e502d42d"
52
+ "gitHead": "144e3710f9b2d970a559d925019983fe10852c15"
53
53
  }