@spark-ui/hooks 11.6.1 → 11.7.1
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 +14 -0
- package/dist/use-sortable-list/index.d.mts +98 -0
- package/dist/use-sortable-list/index.d.ts +98 -0
- package/dist/use-sortable-list/index.js +128 -0
- package/dist/use-sortable-list/index.js.map +1 -0
- package/dist/use-sortable-list/index.mjs +101 -0
- package/dist/use-sortable-list/index.mjs.map +1 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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.1](https://github.com/leboncoin/spark-web/compare/v11.7.0...v11.7.1) (2025-11-25)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @spark-ui/hooks
|
|
9
|
+
|
|
10
|
+
# [11.7.0](https://github.com/leboncoin/spark-web/compare/v11.6.1...v11.7.0) (2025-11-24)
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- **hooks:** make generic usage optional on useSortableList ([e435a16](https://github.com/leboncoin/spark-web/commit/e435a1614201599a19dc2cc5125b88e474a4c32a))
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- **hooks:** useSortableList hook ([dd34c3d](https://github.com/leboncoin/spark-web/commit/dd34c3d7856f3fe4426c55d7e7600f0e2df38aa7))
|
|
19
|
+
|
|
6
20
|
## [11.6.1](https://github.com/leboncoin/spark-web/compare/v11.6.0...v11.6.1) (2025-11-21)
|
|
7
21
|
|
|
8
22
|
**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.
|
|
3
|
+
"version": "11.7.1",
|
|
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": "
|
|
52
|
+
"gitHead": "c9d9deca2425a6b50f428ddd3bc71f92ae4fa9b8"
|
|
53
53
|
}
|