@uxf/core-react 11.91.0 → 11.93.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/README.md CHANGED
@@ -395,6 +395,210 @@ import { twScreens } from "@generated/tw-tokens/tw-screens";
395
395
  const isDesktop = useMediaQuery(`(min-width: ${twScreens.sm})`);
396
396
  ```
397
397
 
398
+ ## Drag and Drop (DND) Hooks
399
+
400
+ Provides a set of hooks built on top of [@dnd-kit](https://dndkit.com/) for implementing drag-and-drop functionality with different patterns.
401
+
402
+ ### useSortableCore
403
+
404
+ Low-level hook that provides the core drag-and-drop functionality. Used as a building block for other sortable hooks.
405
+
406
+ ```tsx
407
+ import { useSortableCore } from "@uxf/core-react/dnd/hooks/use-sortable-core";
408
+
409
+ const {
410
+ activeElement, // Currently dragged element
411
+ setActiveElement, // Set active element manually
412
+ onDragStart, // Drag start handler
413
+ onDragCancel, // Drag cancel handler
414
+ sensors, // DND sensors (pointer, keyboard)
415
+ dropAnimationConfig // Animation configuration for drop
416
+ } = useSortableCore();
417
+ ```
418
+
419
+ ---
420
+ ### useSortableSingle
421
+
422
+ Hook for implementing drag-and-drop sorting within a single list. Manages item reordering and state internally.
423
+
424
+ ```tsx
425
+ import { DndContext, DragOverlay } from "@dnd-kit/core";
426
+ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
427
+ import { useSortableSingle } from "@uxf/core-react/dnd/hooks/use-sortable-single";
428
+ import { SortableItem } from "./sortable-item"; // Your sortable item component
429
+
430
+ interface Task {
431
+ id: string;
432
+ title: string;
433
+ }
434
+
435
+ function TaskList() {
436
+ const initialTasks: Task[] = [
437
+ { id: "1", title: "Write documentation" },
438
+ { id: "2", title: "Review PR" },
439
+ { id: "3", title: "Deploy to production" }
440
+ ];
441
+
442
+ const {
443
+ items, // Current items state
444
+ setItems, // Update items manually
445
+ activeItem, // Currently dragged item
446
+ onDragStart,
447
+ onDragEnd,
448
+ onDragCancel,
449
+ sensors,
450
+ dropAnimationConfig
451
+ } = useSortableSingle(
452
+ initialTasks,
453
+ (fromIndex, toIndex, updatedList) => {
454
+ // Optional callback when items are reordered
455
+ console.log(`Moved from ${fromIndex} to ${toIndex}`, updatedList);
456
+ }
457
+ );
458
+
459
+ return (
460
+ <DndContext
461
+ sensors={sensors}
462
+ onDragStart={onDragStart}
463
+ onDragEnd={onDragEnd}
464
+ onDragCancel={onDragCancel}
465
+ >
466
+ <SortableContext items={items.map(item => item.id)} strategy={verticalListSortingStrategy}>
467
+ {items.map(task => (
468
+ <SortableItem key={task.id} id={task.id}>
469
+ {task.title}
470
+ </SortableItem>
471
+ ))}
472
+ </SortableContext>
473
+
474
+ <DragOverlay dropAnimation={dropAnimationConfig}>
475
+ {activeItem ? <div>{activeItem.title}</div> : null}
476
+ </DragOverlay>
477
+ </DndContext>
478
+ );
479
+ }
480
+ ```
481
+
482
+ ---
483
+ ### useSortableMulti
484
+
485
+ Hook for implementing drag-and-drop between multiple sections/lists. Ideal for kanban boards, categorized lists, or visible/hidden column management.
486
+
487
+ ```tsx
488
+ import { DndContext, DragOverlay, rectIntersection } from "@dnd-kit/core";
489
+ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
490
+ import { useSortableMulti } from "@uxf/core-react/dnd/hooks/use-sortable-multi";
491
+ import { SortableItem } from "./sortable-item"; // Your sortable item component
492
+
493
+ interface Task {
494
+ id: string;
495
+ title: string;
496
+ }
497
+
498
+ interface Sections {
499
+ todo: Task[];
500
+ inProgress: Task[];
501
+ done: Task[];
502
+ }
503
+
504
+ function KanbanBoard() {
505
+ const [sections, setSections] = useState<Sections>({
506
+ todo: [
507
+ { id: "1", title: "Design UI" },
508
+ { id: "2", title: "Write tests" }
509
+ ],
510
+ inProgress: [
511
+ { id: "3", title: "Implement feature" }
512
+ ],
513
+ done: [
514
+ { id: "4", title: "Setup project" }
515
+ ]
516
+ });
517
+
518
+ const {
519
+ getSectionItemsIds, // Get IDs for a section (handles empty sections)
520
+ activeItem, // Currently dragged item
521
+ onDragStart,
522
+ onDragEnd,
523
+ onDragCancel,
524
+ sensors,
525
+ dropAnimationConfig
526
+ } = useSortableMulti<Task, Sections>(
527
+ sections,
528
+ setSections,
529
+ (sourceKey, destKey, fromIndex, toIndex, nextState) => {
530
+ // Optional callback when items are moved between sections
531
+ console.log(`Moved from ${String(sourceKey)} to ${String(destKey)}`);
532
+ // Save to backend, analytics, etc.
533
+ }
534
+ );
535
+
536
+ return (
537
+ <DndContext
538
+ sensors={sensors}
539
+ collisionDetection={rectIntersection}
540
+ onDragStart={onDragStart}
541
+ onDragEnd={onDragEnd}
542
+ onDragCancel={onDragCancel}
543
+ >
544
+ <div className="kanban-board">
545
+ {(Object.keys(sections) as Array<keyof Sections>).map((sectionKey) => (
546
+ <div key={sectionKey} className="kanban-column">
547
+ <h3>{sectionKey}</h3>
548
+ <SortableContext
549
+ items={getSectionItemsIds(sectionKey)}
550
+ strategy={verticalListSortingStrategy}
551
+ >
552
+ {sections[sectionKey].map(task => (
553
+ <SortableItem key={task.id} id={task.id}>
554
+ {task.title}
555
+ </SortableItem>
556
+ ))}
557
+ </SortableContext>
558
+ </div>
559
+ ))}
560
+ </div>
561
+
562
+ <DragOverlay dropAnimation={dropAnimationConfig}>
563
+ {activeItem ? <div>{activeItem.title}</div> : null}
564
+ </DragOverlay>
565
+ </DndContext>
566
+ );
567
+ }
568
+ ```
569
+
570
+ **Notes:**
571
+ - Items must have an `id: string` property
572
+ - `getSectionItemsIds` automatically handles empty sections by returning placeholder IDs
573
+ - Use `rectIntersection` collision detection for better multi-section behavior
574
+ - The hook manages item movement between sections automatically
575
+
576
+ ---
577
+ ### useSortableItems
578
+
579
+ Legacy hook for drag-and-drop sorting. Consider using `useSortableSingle` for new implementations.
580
+
581
+ ```tsx
582
+ import { useSortableItems } from "@uxf/core-react/dnd/hooks/use-sortable-items";
583
+
584
+ const {
585
+ memoizedSensors,
586
+ dropAnimationConfig,
587
+ itemsIds,
588
+ activeItem,
589
+ onDragStart,
590
+ onDragEnd,
591
+ onDragCancel
592
+ } = useSortableItems({
593
+ items: myItems,
594
+ handleDragEnd: (activeIndex, overIndex) => {
595
+ // Handle reorder
596
+ }
597
+ });
598
+ ```
599
+
600
+ ---
601
+
398
602
  ## Translations
399
603
 
400
604
  Provides a translation system that can be used in both single-language and multi-language projects.
@@ -0,0 +1,9 @@
1
+ import { Active, DragStartEvent } from "@dnd-kit/core";
2
+ export declare const useSortableCore: () => {
3
+ activeElement: Active | null;
4
+ setActiveElement: import("react").Dispatch<import("react").SetStateAction<Active | null>>;
5
+ onDragStart: ({ active }: DragStartEvent) => void;
6
+ onDragCancel: () => void;
7
+ sensors: import("@dnd-kit/core").SensorDescriptor<import("@dnd-kit/core").SensorOptions>[];
8
+ dropAnimationConfig: import("@dnd-kit/core/dist/components/DragOverlay/hooks/useDropAnimation").DropAnimationOptions;
9
+ };
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useSortableCore = void 0;
4
+ const core_1 = require("@dnd-kit/core");
5
+ const sortable_1 = require("@dnd-kit/sortable");
6
+ const react_1 = require("react");
7
+ const useSortableCore = () => {
8
+ const [activeElement, setActiveElement] = (0, react_1.useState)(null);
9
+ const sensors = (0, core_1.useSensors)((0, core_1.useSensor)(core_1.PointerSensor), (0, core_1.useSensor)(core_1.KeyboardSensor, {
10
+ coordinateGetter: sortable_1.sortableKeyboardCoordinates,
11
+ }));
12
+ const dropAnimationConfig = {
13
+ sideEffects: (0, core_1.defaultDropAnimationSideEffects)({
14
+ styles: { active: { opacity: "0.4" } },
15
+ }),
16
+ };
17
+ const onDragStart = (0, react_1.useCallback)(({ active }) => {
18
+ setActiveElement(active);
19
+ }, []);
20
+ const onDragCancel = (0, react_1.useCallback)(() => {
21
+ setActiveElement(null);
22
+ }, []);
23
+ return {
24
+ activeElement,
25
+ setActiveElement,
26
+ onDragStart,
27
+ onDragCancel,
28
+ sensors,
29
+ dropAnimationConfig,
30
+ };
31
+ };
32
+ exports.useSortableCore = useSortableCore;
@@ -0,0 +1,15 @@
1
+ import { DragEndEvent, DragStartEvent } from "@dnd-kit/core";
2
+ type Props = {
3
+ items: any[];
4
+ handleDragEnd: (activeIndex: number, overIndex: number) => void;
5
+ };
6
+ export declare const useSortableItems: ({ items, handleDragEnd }: Props) => {
7
+ memoizedSensors: import("@dnd-kit/core").SensorDescriptor<import("@dnd-kit/core").SensorOptions>[];
8
+ dropAnimationConfig: import("@dnd-kit/core/dist/components/DragOverlay/hooks/useDropAnimation").DropAnimationOptions;
9
+ itemsIds: any[];
10
+ activeItem: any;
11
+ onDragStart: ({ active }: DragStartEvent) => void;
12
+ onDragEnd: ({ active, over }: DragEndEvent) => void;
13
+ onDragCancel: () => void;
14
+ };
15
+ export {};
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useSortableItems = void 0;
4
+ const core_1 = require("@dnd-kit/core");
5
+ const sortable_1 = require("@dnd-kit/sortable");
6
+ const react_1 = require("react");
7
+ const useSortableItems = ({ items, handleDragEnd }) => {
8
+ const [sortableItems, setSortableItems] = (0, react_1.useState)(items);
9
+ const [activeElement, setActiveElement] = (0, react_1.useState)(null);
10
+ const activeItem = (0, react_1.useMemo)(() => sortableItems.find((item) => item.id === (activeElement === null || activeElement === void 0 ? void 0 : activeElement.id)), [activeElement, sortableItems]);
11
+ const sensors = (0, core_1.useSensors)((0, core_1.useSensor)(core_1.PointerSensor), (0, core_1.useSensor)(core_1.KeyboardSensor, {
12
+ coordinateGetter: sortable_1.sortableKeyboardCoordinates,
13
+ }));
14
+ // eslint-disable-next-line react-hooks/exhaustive-deps
15
+ const memoizedSensors = (0, react_1.useMemo)(() => sensors, []);
16
+ const idsStringified = JSON.stringify(sortableItems.map((i) => i.id));
17
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18
+ const itemsIds = (0, react_1.useMemo)(() => sortableItems.map((i) => i.id), [idsStringified]);
19
+ (0, react_1.useEffect)(() => {
20
+ setSortableItems(items);
21
+ }, [items]);
22
+ const dropAnimationConfig = {
23
+ sideEffects: (0, core_1.defaultDropAnimationSideEffects)({
24
+ styles: {
25
+ active: {
26
+ opacity: "0.4",
27
+ },
28
+ },
29
+ }),
30
+ };
31
+ const onDragStart = (0, react_1.useCallback)(({ active }) => {
32
+ setActiveElement(active);
33
+ }, [setActiveElement]);
34
+ const onDragEnd = (0, react_1.useCallback)(({ active, over }) => {
35
+ if (over && active.id !== over.id) {
36
+ const activeIndex = sortableItems.findIndex(({ id }) => id === active.id);
37
+ const overIndex = sortableItems.findIndex(({ id }) => id === over.id);
38
+ const resorted = (0, sortable_1.arrayMove)(sortableItems, activeIndex, overIndex);
39
+ handleDragEnd(activeIndex, overIndex);
40
+ setSortableItems(resorted);
41
+ }
42
+ setActiveElement(null);
43
+ }, [setActiveElement, setSortableItems, handleDragEnd, sortableItems]);
44
+ const onDragCancel = (0, react_1.useCallback)(() => {
45
+ setActiveElement(null);
46
+ }, [setActiveElement]);
47
+ return {
48
+ memoizedSensors,
49
+ dropAnimationConfig,
50
+ itemsIds,
51
+ activeItem,
52
+ onDragStart,
53
+ onDragEnd,
54
+ onDragCancel,
55
+ };
56
+ };
57
+ exports.useSortableItems = useSortableItems;
@@ -0,0 +1,12 @@
1
+ import { DragEndEvent } from "@dnd-kit/core";
2
+ export declare function useSortableMulti<T extends {
3
+ id: string;
4
+ }, SectionMap extends Record<string, T[]>>(sections: SectionMap, setSections: (next: SectionMap) => void, onMove?: (source: keyof SectionMap, destination: keyof SectionMap, fromIndex: number, toIndex: number, nextState: SectionMap) => void): {
5
+ getSectionItemsIds: (section: keyof SectionMap) => string[];
6
+ activeItem: T | null;
7
+ onDragStart: ({ active }: import("@dnd-kit/core").DragStartEvent) => void;
8
+ onDragEnd: ({ active, over }: DragEndEvent) => void;
9
+ onDragCancel: () => void;
10
+ sensors: import("@dnd-kit/core").SensorDescriptor<import("@dnd-kit/core").SensorOptions>[];
11
+ dropAnimationConfig: import("@dnd-kit/core/dist/components/DragOverlay/hooks/useDropAnimation").DropAnimationOptions;
12
+ };
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useSortableMulti = useSortableMulti;
4
+ const sortable_1 = require("@dnd-kit/sortable");
5
+ const types_1 = require("@uxf/data-grid/hidden-columns/types");
6
+ const react_1 = require("react");
7
+ const use_sortable_core_1 = require("./use-sortable-core");
8
+ function useSortableMulti(sections, setSections, onMove) {
9
+ const { activeElement, setActiveElement, onDragStart, onDragCancel, sensors, dropAnimationConfig } = (0, use_sortable_core_1.useSortableCore)();
10
+ const activeItem = (0, react_1.useMemo)(() => {
11
+ if (!(activeElement === null || activeElement === void 0 ? void 0 : activeElement.id)) {
12
+ return null;
13
+ }
14
+ for (const items of Object.values(sections)) {
15
+ const item = items.find((i) => i.id === activeElement.id);
16
+ if (item) {
17
+ return item;
18
+ }
19
+ }
20
+ return null;
21
+ }, [activeElement, sections]);
22
+ const onDragEnd = (0, react_1.useCallback)(({ active, over }) => {
23
+ if (!over || active.id === over.id) {
24
+ setActiveElement(null);
25
+ return;
26
+ }
27
+ const isDroppingToEmptyPlaceholder = over.id === types_1.EMPTY_HIDDEN_COLUMNS_PLACEHOLDER_ID || over.id === types_1.EMPTY_VISIBLE_COLUMNS_PLACEHOLDER_ID;
28
+ let sourceKey = null;
29
+ let destKey = null;
30
+ // Determine source key by checking where the active item currently is
31
+ for (const key in sections) {
32
+ if (sections[key].find((item) => item.id === active.id)) {
33
+ sourceKey = key;
34
+ }
35
+ }
36
+ // Determine destination key based on over.id
37
+ if (isDroppingToEmptyPlaceholder) {
38
+ destKey = over.id === types_1.EMPTY_VISIBLE_COLUMNS_PLACEHOLDER_ID ? "visible" : "hidden";
39
+ }
40
+ else {
41
+ for (const key in sections) {
42
+ if (sections[key].find((item) => item.id === over.id)) {
43
+ destKey = key;
44
+ }
45
+ }
46
+ }
47
+ if (!sourceKey || !destKey) {
48
+ setActiveElement(null);
49
+ return;
50
+ }
51
+ const sourceList = [...sections[sourceKey]];
52
+ const destList = [...sections[destKey]];
53
+ const fromIndex = sourceList.findIndex((item) => item.id === active.id);
54
+ let toIndex = destList.findIndex((item) => item.id === over.id);
55
+ // If dropping onto a placeholder or not found in list, append at end
56
+ if (toIndex === -1 || isDroppingToEmptyPlaceholder) {
57
+ toIndex = destList.length;
58
+ }
59
+ const nextState = { ...sections };
60
+ if (sourceKey === destKey) {
61
+ nextState[sourceKey] = (0, sortable_1.arrayMove)(sourceList, fromIndex, toIndex);
62
+ }
63
+ else {
64
+ const [moved] = sourceList.splice(fromIndex, 1);
65
+ destList.splice(toIndex, 0, moved);
66
+ nextState[sourceKey] = sourceList;
67
+ nextState[destKey] = destList;
68
+ }
69
+ setSections(nextState);
70
+ onMove === null || onMove === void 0 ? void 0 : onMove(sourceKey, destKey, fromIndex, toIndex, nextState);
71
+ setActiveElement(null);
72
+ }, [sections, setSections, onMove, setActiveElement]);
73
+ const getSectionItemsIds = (0, react_1.useCallback)((section) => {
74
+ const ids = sections[section].map((i) => i.id);
75
+ if (ids.length === 0) {
76
+ return [
77
+ section === "visible" ? types_1.EMPTY_VISIBLE_COLUMNS_PLACEHOLDER_ID : types_1.EMPTY_HIDDEN_COLUMNS_PLACEHOLDER_ID,
78
+ ];
79
+ }
80
+ return ids;
81
+ }, [sections]);
82
+ return {
83
+ getSectionItemsIds,
84
+ activeItem,
85
+ onDragStart,
86
+ onDragEnd,
87
+ onDragCancel,
88
+ sensors,
89
+ dropAnimationConfig,
90
+ };
91
+ }
@@ -0,0 +1,13 @@
1
+ import { DragEndEvent } from "@dnd-kit/core";
2
+ export declare const useSortableSingle: <T extends {
3
+ id: string;
4
+ }>(initialItems: T[], onReorder?: (fromIndex: number, toIndex: number, updatedList: T[]) => void) => {
5
+ items: T[];
6
+ setItems: import("react").Dispatch<import("react").SetStateAction<T[]>>;
7
+ activeItem: T | null;
8
+ onDragStart: ({ active }: import("@dnd-kit/core").DragStartEvent) => void;
9
+ onDragEnd: ({ active, over }: DragEndEvent) => void;
10
+ onDragCancel: () => void;
11
+ sensors: import("@dnd-kit/core").SensorDescriptor<import("@dnd-kit/core").SensorOptions>[];
12
+ dropAnimationConfig: import("@dnd-kit/core/dist/components/DragOverlay/hooks/useDropAnimation").DropAnimationOptions;
13
+ };
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useSortableSingle = void 0;
4
+ const sortable_1 = require("@dnd-kit/sortable");
5
+ const react_1 = require("react");
6
+ const use_sortable_core_1 = require("./use-sortable-core");
7
+ const useSortableSingle = (initialItems, onReorder) => {
8
+ const [items, setItems] = (0, react_1.useState)(initialItems);
9
+ const { activeElement, setActiveElement, onDragStart, onDragCancel, sensors, dropAnimationConfig } = (0, use_sortable_core_1.useSortableCore)();
10
+ (0, react_1.useEffect)(() => {
11
+ setItems(initialItems);
12
+ }, [initialItems]);
13
+ const activeItem = (0, react_1.useMemo)(() => { var _a; return (_a = items.find((item) => item.id === (activeElement === null || activeElement === void 0 ? void 0 : activeElement.id))) !== null && _a !== void 0 ? _a : null; }, [activeElement, items]);
14
+ const onDragEnd = (0, react_1.useCallback)(({ active, over }) => {
15
+ if (!over || active.id === over.id) {
16
+ setActiveElement(null);
17
+ return;
18
+ }
19
+ const fromIndex = items.findIndex((i) => i.id === active.id);
20
+ const toIndex = items.findIndex((i) => i.id === over.id);
21
+ const reordered = (0, sortable_1.arrayMove)(items, fromIndex, toIndex);
22
+ setItems(reordered);
23
+ onReorder === null || onReorder === void 0 ? void 0 : onReorder(fromIndex, toIndex, reordered);
24
+ setActiveElement(null);
25
+ }, [items, setActiveElement, onReorder]);
26
+ return {
27
+ items,
28
+ setItems,
29
+ activeItem,
30
+ onDragStart,
31
+ onDragEnd,
32
+ onDragCancel,
33
+ sensors,
34
+ dropAnimationConfig,
35
+ };
36
+ };
37
+ exports.useSortableSingle = useSortableSingle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxf/core-react",
3
- "version": "11.91.0",
3
+ "version": "11.93.0",
4
4
  "description": "UXF Core",
5
5
  "author": "UX Fans s.r.o",
6
6
  "license": "MIT",
@@ -12,7 +12,9 @@
12
12
  "typecheck": "tsc --noEmit --skipLibCheck"
13
13
  },
14
14
  "dependencies": {
15
- "@uxf/core": "11.91.0"
15
+ "@uxf/core": "11.93.0",
16
+ "@dnd-kit/core": "6.3.1",
17
+ "@dnd-kit/sortable": "10.0.0"
16
18
  },
17
19
  "peerDependencies": {
18
20
  "react": ">=18.2.0"