@nexub/react 7.6.0 → 7.8.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.
@@ -11,18 +11,69 @@ function moveItem(lists, [fc, fi], [tc, ti]) {
11
11
  next[tc].splice(Math.min(ti, next[tc].length), 0, item);
12
12
  return next;
13
13
  }
14
+ const THRESHOLD = 4;
15
+ const EDGE = 48;
16
+ const MAX_SPEED = 16;
17
+ const itemOf = (el) => el.closest("[data-nx-reorder-item]");
18
+ function scrollParent(el, axis) {
19
+ for (let p = el.parentElement; p; p = p.parentElement) {
20
+ const s = getComputedStyle(p);
21
+ const scrolls = axis === "y" ? /auto|scroll/.test(s.overflowY) && p.scrollHeight > p.clientHeight : /auto|scroll/.test(s.overflowX) && p.scrollWidth > p.clientWidth;
22
+ if (scrolls) return p;
23
+ }
24
+ return document.scrollingElement;
25
+ }
26
+ function edgeSpeed(at, start, end) {
27
+ if (at < start + EDGE) return -MAX_SPEED * Math.min(1, (start + EDGE - at) / EDGE);
28
+ if (at > end - EDGE) return MAX_SPEED * Math.min(1, (at - end + EDGE) / EDGE);
29
+ return 0;
30
+ }
31
+ const sameMark = (a, b) => a === b || !!a && !!b && a.edge === b.edge && ("id" in a ? "id" in b && a.id === b.id : "list" in b && a.list === b.list);
14
32
  function useReorder({ lists, getId, getLabel, listLabels, onMove }) {
15
33
  const t = useNexubMessages();
16
34
  const { dir } = useLocale();
17
- const dragged = React.useRef(null);
18
35
  const handles = React.useRef(/* @__PURE__ */ new Map());
36
+ const listEls = React.useRef(/* @__PURE__ */ new Map());
19
37
  const refocus = React.useRef(null);
38
+ const drag = React.useRef(null);
39
+ const flipFrom = React.useRef(null);
20
40
  const [announcement, setAnnouncement] = React.useState("");
41
+ const [dragging, setDragging] = React.useState(null);
42
+ const [mark, setMark] = React.useState(null);
43
+ const itemCache = React.useRef(/* @__PURE__ */ new Map());
21
44
  const hintId = React.useId();
22
45
  React.useEffect(() => {
23
46
  if (refocus.current) handles.current.get(refocus.current)?.focus();
24
47
  refocus.current = null;
25
48
  });
49
+ React.useLayoutEffect(() => {
50
+ const from = flipFrom.current;
51
+ flipFrom.current = null;
52
+ if (!from || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
53
+ for (const [id, h] of handles.current) {
54
+ const el = itemOf(h);
55
+ const old = from.get(id);
56
+ if (!el || !old) continue;
57
+ const now = el.getBoundingClientRect();
58
+ const dx = old.left - now.left;
59
+ const dy = old.top - now.top;
60
+ if (!dx && !dy) continue;
61
+ el.style.transition = "none";
62
+ el.style.transform = `translate(${dx}px, ${dy}px)`;
63
+ el.getBoundingClientRect();
64
+ el.style.transition = "transform var(--motion-spring-spatial-default-duration) var(--motion-spring-spatial-default-easing)";
65
+ el.style.transform = "";
66
+ el.addEventListener("transitionend", () => el.style.transition = "", { once: true });
67
+ }
68
+ });
69
+ const snapshot = () => {
70
+ const rects = /* @__PURE__ */ new Map();
71
+ for (const [id, h] of handles.current) {
72
+ const el = itemOf(h);
73
+ if (el) rects.set(id, el.getBoundingClientRect());
74
+ }
75
+ return rects;
76
+ };
26
77
  const move = (from, to, keepFocus) => {
27
78
  if (from[0] === to[0] && from[1] === to[1]) return;
28
79
  const item = lists[from[0]][from[1]];
@@ -30,40 +81,102 @@ function useReorder({ lists, getId, getLabel, listLabels, onMove }) {
30
81
  const [tc] = to;
31
82
  const ti = next[tc].indexOf(item);
32
83
  if (keepFocus) refocus.current = getId(item);
84
+ flipFrom.current ??= snapshot();
33
85
  onMove(next);
34
86
  const total = next[tc].length;
35
87
  setAnnouncement(
36
88
  from[0] !== tc && listLabels?.[tc] ? t.itemMovedToColumn(getLabel(item), listLabels[tc], ti + 1, total) : t.itemMoved(getLabel(item), ti + 1, total)
37
89
  );
38
90
  };
39
- const itemProps = (pos) => ({
40
- draggable: true,
41
- onDragStart: (e) => {
42
- dragged.current = pos;
43
- e.dataTransfer.effectAllowed = "move";
44
- },
45
- onDragOver: (e) => {
46
- if (dragged.current) e.preventDefault();
47
- },
48
- onDrop: (e) => {
49
- e.preventDefault();
50
- e.stopPropagation();
51
- if (dragged.current) move(dragged.current, pos, false);
52
- dragged.current = null;
53
- },
54
- onDragEnd: () => {
55
- dragged.current = null;
91
+ const targetAt = (d) => {
92
+ let list = d.from[0];
93
+ if (lists.length > 1) {
94
+ let best = Infinity;
95
+ for (const [l, el] of listEls.current) {
96
+ const r = el.getBoundingClientRect();
97
+ const dist = Math.hypot(Math.max(r.left - d.x, 0, d.x - r.right), Math.max(r.top - d.y, 0, d.y - r.bottom));
98
+ if (dist < best) [best, list] = [dist, l];
99
+ }
56
100
  }
57
- });
101
+ const others = lists[list].filter((it) => getId(it) !== d.id);
102
+ let k = others.findIndex((it) => {
103
+ const h = handles.current.get(getId(it));
104
+ const r = h && itemOf(h)?.getBoundingClientRect();
105
+ return r && d.y < r.top + r.height / 2;
106
+ });
107
+ if (k < 0) k = others.length;
108
+ const to = [list, k];
109
+ if (list === d.from[0] && k === d.from[1]) return { to, mark: null };
110
+ const m = k < others.length ? { id: getId(others[k]), edge: "before" } : others.length ? { id: getId(others[others.length - 1]), edge: "after" } : { list, edge: "inside" };
111
+ return { to, mark: m };
112
+ };
113
+ const update = (d) => {
114
+ if (d.el && d.rect0) {
115
+ const r = d.el.getBoundingClientRect();
116
+ d.tx = d.x - d.x0 - (r.left - d.tx - d.rect0.left);
117
+ d.ty = d.y - d.y0 - (r.top - d.ty - d.rect0.top);
118
+ d.el.style.translate = `${d.tx}px ${d.ty}px`;
119
+ }
120
+ const { to, mark: m } = targetAt(d);
121
+ d.target = m ? to : null;
122
+ if (!sameMark(d.mark, m)) setMark(d.mark = m);
123
+ };
124
+ const tick = () => {
125
+ const d = drag.current;
126
+ if (!d?.active) return;
127
+ let scrolled = false;
128
+ d.scrollers.forEach((s, i) => {
129
+ if (!s) return;
130
+ const page = s === document.scrollingElement;
131
+ const r = page ? { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight } : s.getBoundingClientRect();
132
+ const v = i === 0 ? edgeSpeed(d.x, r.left, r.right) : edgeSpeed(d.y, r.top, r.bottom);
133
+ if (!v) return;
134
+ const before = i === 0 ? s.scrollLeft : s.scrollTop;
135
+ if (i === 0) s.scrollLeft += v;
136
+ else s.scrollTop += v;
137
+ scrolled ||= (i === 0 ? s.scrollLeft : s.scrollTop) !== before;
138
+ });
139
+ if (scrolled) update(d);
140
+ d.raf = requestAnimationFrame(tick);
141
+ };
142
+ const finish = (commit) => {
143
+ const d = drag.current;
144
+ if (!d) return;
145
+ drag.current = null;
146
+ window.removeEventListener("keydown", d.onKey, true);
147
+ if (!d.active) return;
148
+ cancelAnimationFrame(d.raf);
149
+ flipFrom.current = snapshot();
150
+ if (d.el) d.el.style.translate = "";
151
+ if (commit && d.target) move(d.from, d.target, false);
152
+ setDragging(null);
153
+ setMark(null);
154
+ };
155
+ React.useEffect(() => () => finish(false), []);
156
+ const begin = (d) => {
157
+ d.active = true;
158
+ d.el = itemOf(d.handle) ?? d.handle;
159
+ d.rect0 = d.el.getBoundingClientRect();
160
+ d.scrollers = [scrollParent(d.el, "x"), scrollParent(d.el, "y")];
161
+ setDragging(d.id);
162
+ d.raf = requestAnimationFrame(tick);
163
+ };
164
+ const itemProps = (pos) => {
165
+ const id = getId(lists[pos[0]][pos[1]]);
166
+ const lifted = dragging === id ? "" : void 0;
167
+ const drop = mark && "id" in mark && mark.id === id ? mark.edge : void 0;
168
+ const prev = itemCache.current.get(id);
169
+ if (prev && prev["data-nx-dragging"] === lifted && prev["data-nx-drop"] === drop) return prev;
170
+ const next = { "data-nx-reorder-item": "", "data-nx-dragging": lifted, "data-nx-drop": drop };
171
+ itemCache.current.set(id, next);
172
+ return next;
173
+ };
58
174
  const listProps = (list) => ({
59
- onDragOver: (e) => {
60
- if (dragged.current) e.preventDefault();
175
+ ref: (el) => {
176
+ if (el) listEls.current.set(list, el);
177
+ else listEls.current.delete(list);
61
178
  },
62
- onDrop: (e) => {
63
- e.preventDefault();
64
- if (dragged.current) move(dragged.current, [list, lists[list].length], false);
65
- dragged.current = null;
66
- }
179
+ "data-nx-drop": mark && "list" in mark && mark.list === list ? "inside" : void 0
67
180
  });
68
181
  const handle = (pos, className) => {
69
182
  const item = lists[pos[0]][pos[1]];
@@ -78,6 +191,51 @@ function useReorder({ lists, getId, getLabel, listLabels, onMove }) {
78
191
  },
79
192
  "aria-label": t.moveItem(getLabel(item)),
80
193
  "aria-describedby": hintId,
194
+ onPointerDown: (e) => {
195
+ if (e.button !== 0 || drag.current) return;
196
+ const { clientX: x, clientY: y } = e;
197
+ const d = {
198
+ from: pos,
199
+ id,
200
+ handle: e.currentTarget,
201
+ x0: x,
202
+ y0: y,
203
+ x,
204
+ y,
205
+ active: false,
206
+ tx: 0,
207
+ ty: 0,
208
+ target: null,
209
+ mark: null,
210
+ scrollers: [null, null],
211
+ raf: 0,
212
+ onKey: (k) => {
213
+ if (k.key !== "Escape") return;
214
+ k.preventDefault();
215
+ k.stopPropagation();
216
+ finish(false);
217
+ }
218
+ };
219
+ drag.current = d;
220
+ window.addEventListener("keydown", d.onKey, true);
221
+ try {
222
+ e.currentTarget.setPointerCapture(e.pointerId);
223
+ } catch {
224
+ }
225
+ },
226
+ onPointerMove: (e) => {
227
+ const d = drag.current;
228
+ if (!d || d.id !== id) return;
229
+ d.x = e.clientX;
230
+ d.y = e.clientY;
231
+ if (!d.active) {
232
+ if (Math.hypot(d.x - d.x0, d.y - d.y0) < THRESHOLD) return;
233
+ begin(d);
234
+ }
235
+ update(d);
236
+ },
237
+ onPointerUp: () => finish(true),
238
+ onPointerCancel: () => finish(false),
81
239
  onKeyDown: (e) => {
82
240
  if (!e.altKey) return;
83
241
  const [c, i] = pos;
@@ -94,7 +252,7 @@ function useReorder({ lists, getId, getLabel, listLabels, onMove }) {
94
252
  move(pos, to, true);
95
253
  },
96
254
  className: cn(
97
- "inline-flex size-7 shrink-0 cursor-grab items-center justify-center rounded-shape-min text-[var(--color-on-surface-variant)]",
255
+ "inline-flex size-7 shrink-0 cursor-grab touch-none select-none items-center justify-center rounded-shape-min text-[var(--color-on-surface-variant)]",
98
256
  "hover:bg-[var(--color-surface-container-high)] active:cursor-grabbing",
99
257
  className
100
258
  ),