@waveso/ui 0.17.0 → 0.18.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 +25 -14
- package/dist/accordion.js +2 -2
- package/dist/action-bar.js +1 -1
- package/dist/alert-dialog.js +1 -1
- package/dist/alert.js +1 -1
- package/dist/autocomplete.js +4 -4
- package/dist/avatar.js +6 -6
- package/dist/badge.js +2 -2
- package/dist/button-group.js +3 -3
- package/dist/button.js +3 -3
- package/dist/card.js +1 -1
- package/dist/checkbox.js +1 -1
- package/dist/combobox.js +6 -6
- package/dist/context-menu.d.ts +1 -1
- package/dist/context-menu.js +27 -10
- package/dist/dialog.js +1 -1
- package/dist/drawer.js +2 -2
- package/dist/gradient-reveal-text.js +1 -1
- package/dist/input-group.js +1 -1
- package/dist/input-otp.js +1 -1
- package/dist/input.js +1 -1
- package/dist/item.js +2 -2
- package/dist/menu.d.ts +1 -1
- package/dist/menu.js +27 -10
- package/dist/menubar.js +2 -2
- package/dist/popover.js +1 -1
- package/dist/preview-card.js +1 -1
- package/dist/radio.js +1 -1
- package/dist/resizable.js +2 -2
- package/dist/scroll-area.js +2 -12
- package/dist/select.js +5 -5
- package/dist/separator.js +1 -1
- package/dist/sidebar.js +11 -11
- package/dist/slider.js +2 -2
- package/dist/styles.css +190 -76
- package/dist/swipe-deck.d.ts +92 -36
- package/dist/swipe-deck.js +601 -158
- package/dist/switch.js +1 -1
- package/dist/table.js +3 -3
- package/dist/tabs.js +3 -3
- package/dist/textarea.js +1 -1
- package/dist/toast.d.ts +1 -1
- package/dist/toast.js +4 -4
- package/dist/toggle.js +3 -3
- package/package.json +1 -1
package/dist/swipe-deck.js
CHANGED
|
@@ -13,8 +13,21 @@ const KEY_DIRECTIONS = {
|
|
|
13
13
|
ArrowUp: "up",
|
|
14
14
|
ArrowDown: "down"
|
|
15
15
|
};
|
|
16
|
+
/** One step each way, in screen axes (y grows downward). */
|
|
17
|
+
const DIRECTION_VECTORS = {
|
|
18
|
+
left: [-1, 0],
|
|
19
|
+
right: [1, 0],
|
|
20
|
+
up: [0, -1],
|
|
21
|
+
down: [0, 1]
|
|
22
|
+
};
|
|
16
23
|
/** How far (px) a press moves before it becomes a drag, so taps stay clicks. */
|
|
17
24
|
const DRAG_SLOP = 6;
|
|
25
|
+
/**
|
|
26
|
+
* How hard a card resists being pulled toward a side it can't leave by: the
|
|
27
|
+
* factor in iOS's rubber band, under which a pull moves it less and less,
|
|
28
|
+
* never past its own length.
|
|
29
|
+
*/
|
|
30
|
+
const RUBBER_BAND = .55;
|
|
18
31
|
/** A card leaves when it comes to rest past this share of its width (or height). */
|
|
19
32
|
const LEAVE_SHARE = .25;
|
|
20
33
|
/** Only pointer samples this recent (ms) set the release speed. */
|
|
@@ -24,7 +37,7 @@ const VELOCITY_WINDOW = 100;
|
|
|
24
37
|
* travel to where the card is, to decide whether it leaves and which way.
|
|
25
38
|
*/
|
|
26
39
|
const THROW_TIME = .25;
|
|
27
|
-
/** A leaving card's exit point is this far (px) past the
|
|
40
|
+
/** A leaving card's exit point is this far (px) past the pile's edge, shadow and all. */
|
|
28
41
|
const CLEARANCE = 16;
|
|
29
42
|
const RETURN_SPRING = {
|
|
30
43
|
type: "spring",
|
|
@@ -41,7 +54,7 @@ const EXIT_SPRING = {
|
|
|
41
54
|
stiffness: 650,
|
|
42
55
|
damping: 42
|
|
43
56
|
};
|
|
44
|
-
/** A leaving card fades once clear of the
|
|
57
|
+
/** A leaving card fades once clear of the pile and slowed below this (px/s). */
|
|
45
58
|
const CAUGHT_SPEED = 200;
|
|
46
59
|
/** Moves a card from one place in the pile to the next. */
|
|
47
60
|
const PLACE_SPRING = {
|
|
@@ -49,6 +62,12 @@ const PLACE_SPRING = {
|
|
|
49
62
|
stiffness: 260,
|
|
50
63
|
damping: 24
|
|
51
64
|
};
|
|
65
|
+
/** Brings a card your list moved to the back from its exit point under the pile. */
|
|
66
|
+
const TUCK_SPRING = {
|
|
67
|
+
type: "spring",
|
|
68
|
+
stiffness: 240,
|
|
69
|
+
damping: 28
|
|
70
|
+
};
|
|
52
71
|
/** How far (in places) the cards behind lift toward the front as it's dragged out. */
|
|
53
72
|
const LIFT = .4;
|
|
54
73
|
/** On mount, the cards behind wait this long (s) for the front card to fade in. */
|
|
@@ -57,6 +76,36 @@ const ENTER_LEAD = .15;
|
|
|
57
76
|
const ENTER_STAGGER = .06;
|
|
58
77
|
/** And grows in from this size, the zoom recipes' md step (`--scale-md`). */
|
|
59
78
|
const ENTER_SCALE = .9;
|
|
79
|
+
/** A pressed card lifts to this size, under the pointer. */
|
|
80
|
+
const PRESS_SCALE = 1.035;
|
|
81
|
+
const PRESS_SPRING = {
|
|
82
|
+
type: "spring",
|
|
83
|
+
stiffness: 420,
|
|
84
|
+
damping: 34
|
|
85
|
+
};
|
|
86
|
+
/** How far (deg) a dragged card tilts toward the viewer at most. */
|
|
87
|
+
const TILT = 30;
|
|
88
|
+
/** And how far (deg) it turns in its own plane, carried sideways. */
|
|
89
|
+
const SPIN = 6;
|
|
90
|
+
/** The pointer speed (px/s) that tilts a card all the way. */
|
|
91
|
+
const LEAN_SPEED = 1300;
|
|
92
|
+
/** The drag (px) that tilts and turns a card all the way. */
|
|
93
|
+
const LEAN_REACH = 260;
|
|
94
|
+
/** A still pointer stops tilting the card after this long (ms). */
|
|
95
|
+
const LEAN_IDLE = 70;
|
|
96
|
+
/** The depth (px) the tilt is seen from. */
|
|
97
|
+
const PERSPECTIVE = 800;
|
|
98
|
+
const LEAN_SPRING = {
|
|
99
|
+
type: "spring",
|
|
100
|
+
stiffness: 240,
|
|
101
|
+
damping: 28
|
|
102
|
+
};
|
|
103
|
+
/** Lays a card flat and lowers it once it's let go. */
|
|
104
|
+
const FACE_SPRING = {
|
|
105
|
+
type: "spring",
|
|
106
|
+
stiffness: 240,
|
|
107
|
+
damping: 30
|
|
108
|
+
};
|
|
60
109
|
/** Dash and gap (px) of `SwipeDeckEmpty`'s outline. */
|
|
61
110
|
const OUTLINE_DASH = 8;
|
|
62
111
|
const OUTLINE_GAP = 6;
|
|
@@ -64,16 +113,41 @@ const OUTLINE_GAP = 6;
|
|
|
64
113
|
const OUTLINE_CORNER_STEPS = 24;
|
|
65
114
|
/** Presses on these inside a card stay theirs: a slider's thumb, a text box. */
|
|
66
115
|
const FIELD_SELECTOR = "input, textarea, select, [contenteditable]:not([contenteditable='false']), [role=slider]";
|
|
67
|
-
|
|
116
|
+
const ITEM_SELECTOR = "[data-slot=swipe-deck-item]";
|
|
117
|
+
/**
|
|
118
|
+
* Keeps the browser's own panning on the axis the deck doesn't swipe along,
|
|
119
|
+
* and its pinch zoom everywhere.
|
|
120
|
+
*/
|
|
68
121
|
function touchAction(directions) {
|
|
69
122
|
const horizontal = directions.some(isHorizontal);
|
|
70
123
|
const vertical = directions.some((direction) => !isHorizontal(direction));
|
|
71
|
-
if (horizontal && vertical) return "touch-
|
|
72
|
-
return horizontal ? "touch-pan-y" : "touch-pan-x";
|
|
124
|
+
if (horizontal && vertical) return "touch-pinch-zoom";
|
|
125
|
+
return horizontal ? "touch-pan-y touch-pinch-zoom" : "touch-pan-x touch-pinch-zoom";
|
|
73
126
|
}
|
|
74
127
|
function isHorizontal(direction) {
|
|
75
128
|
return direction === "left" || direction === "right";
|
|
76
129
|
}
|
|
130
|
+
/** Whether `offset` on an axis is on a side the card can leave by. */
|
|
131
|
+
function isOpenSide(directions, horizontal, offset) {
|
|
132
|
+
if (offset === 0) return true;
|
|
133
|
+
const toward = horizontal ? offset > 0 ? "right" : "left" : offset > 0 ? "down" : "up";
|
|
134
|
+
return directions.includes(toward);
|
|
135
|
+
}
|
|
136
|
+
/** Where a pull of `offset` toward a closed side leaves the card. */
|
|
137
|
+
function resist(offset, length) {
|
|
138
|
+
if (length <= 0) return 0;
|
|
139
|
+
return Math.sign(offset) * (1 - 1 / (Math.abs(offset) * RUBBER_BAND / length + 1)) * length;
|
|
140
|
+
}
|
|
141
|
+
/** The pull that `resist` turns into `offset`, to catch a resisted card. */
|
|
142
|
+
function unresist(offset, length) {
|
|
143
|
+
if (length <= 0) return 0;
|
|
144
|
+
const share = Math.min(Math.abs(offset) / length, .99);
|
|
145
|
+
return Math.sign(offset) * (1 / (1 - share) - 1) * length / RUBBER_BAND;
|
|
146
|
+
}
|
|
147
|
+
/** A drag's offset on one axis, resisted when it points to a closed side. */
|
|
148
|
+
function follow(directions, horizontal, pull, length) {
|
|
149
|
+
return isOpenSide(directions, horizontal, pull) ? pull : resist(pull, length);
|
|
150
|
+
}
|
|
77
151
|
/** Lets the cards behind settle back from a lift. */
|
|
78
152
|
function releaseLift(lift, isInstant) {
|
|
79
153
|
if (isInstant) lift.jump(0);
|
|
@@ -92,6 +166,29 @@ function transitionsEnd(element) {
|
|
|
92
166
|
function within(value, min, max, fallback) {
|
|
93
167
|
return Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
|
|
94
168
|
}
|
|
169
|
+
function isThenable(value) {
|
|
170
|
+
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
171
|
+
}
|
|
172
|
+
function createSwipeEventDetails(cause) {
|
|
173
|
+
let isCanceled = false;
|
|
174
|
+
return {
|
|
175
|
+
...cause,
|
|
176
|
+
cancel: () => {
|
|
177
|
+
isCanceled = true;
|
|
178
|
+
},
|
|
179
|
+
get isCanceled() {
|
|
180
|
+
return isCanceled;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const subscribeToNothing = () => () => {};
|
|
185
|
+
/**
|
|
186
|
+
* False while React renders on the server or hydrates what the server sent,
|
|
187
|
+
* true for a render made in the browser.
|
|
188
|
+
*/
|
|
189
|
+
function useIsClientRender() {
|
|
190
|
+
return React.useSyncExternalStore(subscribeToNothing, () => true, () => false);
|
|
191
|
+
}
|
|
95
192
|
/**
|
|
96
193
|
* Three steady numbers in [-0.5, 0.5] from a card's value: where it lies in a
|
|
97
194
|
* `pile`, the same on every render and server.
|
|
@@ -118,30 +215,51 @@ function sizeOf(element) {
|
|
|
118
215
|
height: element.offsetHeight
|
|
119
216
|
};
|
|
120
217
|
}
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Where a pointer at (clientX, clientY) is on a card, in px from its transform
|
|
220
|
+
* origin, the card's own turn and size taken back out: the middle of its
|
|
221
|
+
* bounding box is where its middle is drawn, however it is turned.
|
|
222
|
+
*/
|
|
223
|
+
function grabPoint(element, clientX, clientY, size) {
|
|
224
|
+
const box = element.getBoundingClientRect();
|
|
225
|
+
const style = getComputedStyle(element);
|
|
226
|
+
const angle = (Number.parseFloat(style.rotate) || 0) * Math.PI / 180;
|
|
227
|
+
const [scaleX = 1, scaleY = scaleX] = style.scale && style.scale !== "none" ? style.scale.split(" ").map((part) => Number.parseFloat(part) || 1) : [];
|
|
228
|
+
const dx = clientX - (box.left + box.width / 2);
|
|
229
|
+
const dy = clientY - (box.top + box.height / 2);
|
|
230
|
+
const [cos, sin] = [Math.cos(-angle), Math.sin(-angle)];
|
|
231
|
+
const [originX = 0, originY = 0] = style.transformOrigin.split(" ").map((part) => Number.parseFloat(part) || 0);
|
|
232
|
+
return [within(size.width / 2 + (dx * cos - dy * sin) / scaleX, 0, size.width, 0) - originX, within(size.height / 2 + (dx * sin + dy * cos) / scaleY, 0, size.height, 0) - originY];
|
|
233
|
+
}
|
|
234
|
+
/** Whether a card's press and tilt have settled, its pose drawn as `none`. */
|
|
235
|
+
function isPoseAtRest(press, spin, tiltX, tiltY) {
|
|
236
|
+
return Math.abs(press.get() - 1) < 1e-4 && Math.abs(spin.get()) < .001 && Math.abs(tiltX.get()) < .001 && Math.abs(tiltY.get()) < .001;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Whether a card moved by (x, y) is off the pile on the side it's heading for
|
|
240
|
+
* (ux, uy): one dragged out to the right and sent left still has to cross it.
|
|
241
|
+
*/
|
|
242
|
+
function isClear(x, y, extents, [ux, uy]) {
|
|
243
|
+
return ux > 0 && x >= extents.right || ux < 0 && -x >= extents.left || uy > 0 && y >= extents.down || uy < 0 && -y >= extents.up;
|
|
124
244
|
}
|
|
125
245
|
/**
|
|
126
246
|
* Where a card at (x, y), heading along (dx, dy), first sits `CLEARANCE` past
|
|
127
|
-
* the
|
|
247
|
+
* the pile's edge: the nearer of the side edges and the top or bottom edge.
|
|
128
248
|
*/
|
|
129
|
-
function exitPoint(x, y, [dx, dy],
|
|
249
|
+
function exitPoint(x, y, [dx, dy], extents) {
|
|
130
250
|
const length = Math.hypot(dx, dy);
|
|
131
251
|
const ux = dx / length;
|
|
132
252
|
const uy = dy / length;
|
|
133
|
-
const
|
|
253
|
+
const edgeX = ux > 0 ? extents.right + CLEARANCE : -extents.left - CLEARANCE;
|
|
254
|
+
const edgeY = uy > 0 ? extents.down + CLEARANCE : -extents.up - CLEARANCE;
|
|
255
|
+
const steps = [ux === 0 ? Infinity : (edgeX - x) / ux, uy === 0 ? Infinity : (edgeY - y) / uy];
|
|
134
256
|
const step = Math.max(0, Math.min(...steps));
|
|
135
257
|
return [x + step * ux, y + step * uy];
|
|
136
258
|
}
|
|
137
259
|
/** How far a movement goes toward a direction; negative when it goes away. */
|
|
138
260
|
function reach(direction, x, y) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
case "left": return -x;
|
|
142
|
-
case "down": return y;
|
|
143
|
-
case "up": return -y;
|
|
144
|
-
}
|
|
261
|
+
const [dx, dy] = DIRECTION_VECTORS[direction];
|
|
262
|
+
return x * dx + y * dy;
|
|
145
263
|
}
|
|
146
264
|
/**
|
|
147
265
|
* The allowed direction a movement leans furthest into, and how far it goes
|
|
@@ -171,6 +289,9 @@ function releaseVelocity(samples, time) {
|
|
|
171
289
|
const elapsed = (last.time - first.time) / 1e3;
|
|
172
290
|
return [(last.x - first.x) / elapsed, (last.y - first.y) / elapsed];
|
|
173
291
|
}
|
|
292
|
+
function clampUnit(value) {
|
|
293
|
+
return Math.min(1, Math.max(-1, value));
|
|
294
|
+
}
|
|
174
295
|
const SwipeDeckContext = React.createContext(null);
|
|
175
296
|
function useSwipeDeckContext(part) {
|
|
176
297
|
const context = React.useContext(SwipeDeckContext);
|
|
@@ -180,25 +301,43 @@ function useSwipeDeckContext(part) {
|
|
|
180
301
|
/**
|
|
181
302
|
* A pile of cards people swipe away: drag or throw the front card, press an
|
|
182
303
|
* arrow key while the deck is focused, or use a `SwipeDeckAction`. A let-go
|
|
183
|
-
* card whose position and throw carry it past a quarter of its width
|
|
184
|
-
*
|
|
304
|
+
* card whose position and throw carry it past a quarter of its width (or of
|
|
305
|
+
* its height, up and down) leaves:
|
|
306
|
+
* it slides out the way it is heading until clear of the pile, held by a
|
|
185
307
|
* spring that a hard throw runs out against, then fades. Short of that, it
|
|
186
|
-
* springs back.
|
|
187
|
-
*
|
|
308
|
+
* springs back. Pressed, a card lifts under the pointer; dragged, it tilts
|
|
309
|
+
* with the throw.
|
|
310
|
+
*
|
|
311
|
+
* Your list stays the source of truth. `onSwipe` comes the moment a card is
|
|
312
|
+
* sent off: count it there, or cancel to keep it. `onSwipeComplete` comes once
|
|
313
|
+
* it has gone: remove it from your list then, not in `onSwipe`, where removing
|
|
314
|
+
* it would cut its exit short. To cycle the deck instead, move the card to
|
|
315
|
+
* the end of your list in `onSwipe`, and it slides back under the pile. To
|
|
316
|
+
* undo a swipe, put the card back at the start of your list.
|
|
317
|
+
*
|
|
318
|
+
* The cards are the deck's own children, in your list's order. A long list
|
|
319
|
+
* needn't be rendered whole: the deck shows `visible` cards, so the next few
|
|
320
|
+
* dozen are plenty.
|
|
188
321
|
*
|
|
189
322
|
* End the deck with a `SwipeDeckEmpty`: it lies under the last card, so
|
|
190
|
-
* dragging that card shows what comes next.
|
|
191
|
-
*
|
|
192
|
-
*
|
|
323
|
+
* dragging that card shows what comes next.
|
|
324
|
+
*
|
|
325
|
+
* Styling hooks: `data-layout`, `data-disabled`, and `data-empty` once no
|
|
326
|
+
* card is in front (the last one leaving, or none left). CSS variables:
|
|
327
|
+
* `--swipe-deck-card-width` / `-height`, the size of the largest card.
|
|
193
328
|
*
|
|
194
329
|
* Give the deck an `aria-label`. Screen readers hear the next card after each
|
|
195
330
|
* swipe: its `aria-label` when it has one, otherwise all of its text.
|
|
196
331
|
*/
|
|
197
|
-
function SwipeDeck({ onSwipe, directions = DEFAULT_DIRECTIONS, disabled = false, layout = "
|
|
332
|
+
function SwipeDeck({ onSwipe, onSwipeComplete, directions = DEFAULT_DIRECTIONS, disabled = false, layout = "fan", spread = .5, depth = .5, visible = 4, className, style, onKeyDown, children, ref, ...props }) {
|
|
198
333
|
const rootRef = React.useRef(null);
|
|
199
334
|
const mergedRef = React.useMemo(() => mergeRefs(rootRef, ref), [ref]);
|
|
200
335
|
const itemsRef = React.useRef(/* @__PURE__ */ new Map());
|
|
201
|
-
const
|
|
336
|
+
const valuesRef = React.useRef(/* @__PURE__ */ new WeakMap());
|
|
337
|
+
const sentRef = React.useRef(/* @__PURE__ */ new Map());
|
|
338
|
+
const leavesRef = React.useRef(0);
|
|
339
|
+
const followersRef = React.useRef(/* @__PURE__ */ new Map());
|
|
340
|
+
const movedBackRef = React.useRef(/* @__PURE__ */ new Set());
|
|
202
341
|
const statusRef = React.useRef(null);
|
|
203
342
|
const emptyRef = React.useRef(null);
|
|
204
343
|
const sizeObserverRef = React.useRef(null);
|
|
@@ -206,32 +345,60 @@ function SwipeDeck({ onSwipe, directions = DEFAULT_DIRECTIONS, disabled = false,
|
|
|
206
345
|
const [order, setOrder] = React.useState([]);
|
|
207
346
|
const [leavingList, setLeavingList] = React.useState([]);
|
|
208
347
|
const [isReady, setIsReady] = React.useState(false);
|
|
348
|
+
const visibleCount = Math.round(within(visible, 1, Infinity, 4));
|
|
209
349
|
const onSwipeRef = React.useRef(onSwipe);
|
|
210
|
-
React.
|
|
350
|
+
const onSwipeCompleteRef = React.useRef(onSwipeComplete);
|
|
351
|
+
React.useLayoutEffect(() => {
|
|
211
352
|
onSwipeRef.current = onSwipe;
|
|
353
|
+
onSwipeCompleteRef.current = onSwipeComplete;
|
|
212
354
|
});
|
|
213
|
-
const readOrder = React.useCallback(() =>
|
|
355
|
+
const readOrder = React.useCallback(() => {
|
|
356
|
+
const root = rootRef.current;
|
|
357
|
+
if (!root) return [];
|
|
358
|
+
const read = (elements) => {
|
|
359
|
+
const next = [];
|
|
360
|
+
for (const element of elements) {
|
|
361
|
+
const value = valuesRef.current.get(element);
|
|
362
|
+
if (value !== void 0) next.push(value);
|
|
363
|
+
}
|
|
364
|
+
return next;
|
|
365
|
+
};
|
|
366
|
+
const next = read(root.children);
|
|
367
|
+
return next.length === itemsRef.current.size ? next : read(root.querySelectorAll(ITEM_SELECTOR));
|
|
368
|
+
}, []);
|
|
369
|
+
const noteMovedBack = React.useCallback((current) => {
|
|
370
|
+
if (followersRef.current.size === 0) return;
|
|
371
|
+
const indexes = new Map(current.map((card, index) => [card, index]));
|
|
372
|
+
for (const [value, followers] of followersRef.current) {
|
|
373
|
+
const own = indexes.get(value);
|
|
374
|
+
if (own !== void 0 && followers.some((follower) => (indexes.get(follower) ?? Infinity) < own)) movedBackRef.current.add(value);
|
|
375
|
+
}
|
|
376
|
+
}, []);
|
|
214
377
|
const syncOrder = React.useCallback(() => {
|
|
215
|
-
|
|
378
|
+
if (!rootRef.current) return;
|
|
216
379
|
const next = readOrder();
|
|
380
|
+
noteMovedBack(next);
|
|
217
381
|
setOrder((previous) => previous.length === next.length && previous.every((value, index) => value === next[index]) ? previous : next);
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return followerIndex !== -1 && followerIndex < index;
|
|
224
|
-
});
|
|
225
|
-
if (index === -1 || hasMoved) {
|
|
226
|
-
answered.add(value);
|
|
227
|
-
finishedRef.current.delete(value);
|
|
228
|
-
}
|
|
382
|
+
for (const value of sentRef.current.keys()) {
|
|
383
|
+
if (itemsRef.current.has(value)) continue;
|
|
384
|
+
sentRef.current.delete(value);
|
|
385
|
+
followersRef.current.delete(value);
|
|
386
|
+
movedBackRef.current.delete(value);
|
|
229
387
|
}
|
|
230
388
|
setLeavingList((previous) => {
|
|
231
|
-
const kept = previous.filter((value) =>
|
|
389
|
+
const kept = previous.filter((value) => itemsRef.current.has(value));
|
|
232
390
|
return kept.length === previous.length ? previous : kept;
|
|
233
391
|
});
|
|
234
|
-
}, [readOrder]);
|
|
392
|
+
}, [noteMovedBack, readOrder]);
|
|
393
|
+
const isSyncQueuedRef = React.useRef(false);
|
|
394
|
+
const syncSoon = React.useCallback(() => {
|
|
395
|
+
if (isSyncQueuedRef.current) return;
|
|
396
|
+
isSyncQueuedRef.current = true;
|
|
397
|
+
queueMicrotask(() => {
|
|
398
|
+
isSyncQueuedRef.current = false;
|
|
399
|
+
syncOrder();
|
|
400
|
+
});
|
|
401
|
+
}, [syncOrder]);
|
|
235
402
|
React.useLayoutEffect(() => {
|
|
236
403
|
syncOrder();
|
|
237
404
|
});
|
|
@@ -259,50 +426,143 @@ function SwipeDeck({ onSwipe, directions = DEFAULT_DIRECTIONS, disabled = false,
|
|
|
259
426
|
}, []);
|
|
260
427
|
React.useEffect(() => () => sizeObserverRef.current?.disconnect(), []);
|
|
261
428
|
const register = React.useCallback((value, record) => {
|
|
262
|
-
itemsRef.current
|
|
429
|
+
const items = itemsRef.current;
|
|
430
|
+
const existing = items.get(value);
|
|
431
|
+
if (process.env.NODE_ENV === "development" && existing && existing.element !== record.element) console.warn(`SwipeDeck: more than one card has the value "${value}". Give each SwipeDeckItem its own value, the same as its key.`);
|
|
432
|
+
items.set(value, record);
|
|
433
|
+
valuesRef.current.set(record.element, value);
|
|
263
434
|
sizeObserver().observe(record.element);
|
|
264
|
-
|
|
435
|
+
syncSoon();
|
|
265
436
|
return () => {
|
|
266
|
-
|
|
267
|
-
|
|
437
|
+
valuesRef.current.delete(record.element);
|
|
438
|
+
if (items.get(value) === record) {
|
|
439
|
+
items.delete(value);
|
|
268
440
|
sizeObserverRef.current?.unobserve(record.element);
|
|
269
|
-
syncOrder();
|
|
270
441
|
}
|
|
442
|
+
syncSoon();
|
|
271
443
|
};
|
|
272
|
-
}, [sizeObserver,
|
|
444
|
+
}, [sizeObserver, syncSoon]);
|
|
273
445
|
const focusDeck = React.useCallback(() => rootRef.current?.focus(), []);
|
|
274
446
|
const [settledFront, setSettledFront] = React.useState();
|
|
275
|
-
React.
|
|
276
|
-
|
|
277
|
-
}), [lift]);
|
|
278
|
-
const leave = React.useCallback((value) => {
|
|
279
|
-
setLeavingList((previous) => previous.includes(value) ? previous : [...previous, value]);
|
|
280
|
-
}, []);
|
|
281
|
-
const finish = React.useCallback((details) => {
|
|
447
|
+
const send = React.useCallback((details, cause) => {
|
|
448
|
+
if (sentRef.current.has(details.value)) return false;
|
|
282
449
|
const current = readOrder();
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
450
|
+
const followers = current.slice(current.indexOf(details.value) + 1);
|
|
451
|
+
const eventDetails = createSwipeEventDetails(cause);
|
|
452
|
+
onSwipeRef.current?.(details, eventDetails);
|
|
453
|
+
if (eventDetails.isCanceled) return false;
|
|
454
|
+
leavesRef.current += 1;
|
|
455
|
+
sentRef.current.set(details.value, leavesRef.current);
|
|
456
|
+
followersRef.current.set(details.value, followers);
|
|
457
|
+
if ((itemsRef.current.get(details.value)?.element)?.contains(document.activeElement)) focusDeck();
|
|
458
|
+
setLeavingList((previous) => previous.includes(details.value) ? previous : [...previous, details.value]);
|
|
459
|
+
return true;
|
|
460
|
+
}, [focusDeck, readOrder]);
|
|
461
|
+
const isMovedBack = React.useCallback((value) => {
|
|
462
|
+
noteMovedBack(readOrder());
|
|
463
|
+
return movedBackRef.current.has(value);
|
|
464
|
+
}, [noteMovedBack, readOrder]);
|
|
465
|
+
const returnNow = React.useCallback((leaves) => {
|
|
466
|
+
const back = /* @__PURE__ */ new Set();
|
|
467
|
+
for (const [value, leave] of leaves) {
|
|
468
|
+
if (sentRef.current.get(value) !== leave) continue;
|
|
469
|
+
back.add(value);
|
|
470
|
+
sentRef.current.delete(value);
|
|
471
|
+
followersRef.current.delete(value);
|
|
472
|
+
movedBackRef.current.delete(value);
|
|
473
|
+
}
|
|
474
|
+
if (back.size === 0) return;
|
|
475
|
+
setLeavingList((previous) => {
|
|
476
|
+
const kept = previous.filter((value) => !back.has(value));
|
|
477
|
+
return kept.length === previous.length ? previous : kept;
|
|
478
|
+
});
|
|
479
|
+
}, []);
|
|
480
|
+
const rejoin = React.useCallback((value) => {
|
|
481
|
+
const leave = sentRef.current.get(value);
|
|
482
|
+
if (leave !== void 0) returnNow(/* @__PURE__ */ new Map([[value, leave]]));
|
|
483
|
+
}, [returnNow]);
|
|
484
|
+
const answeredRef = React.useRef(/* @__PURE__ */ new Map());
|
|
485
|
+
const [answers, setAnswers] = React.useState(0);
|
|
486
|
+
const endLeave = React.useCallback((value, leave) => {
|
|
487
|
+
answeredRef.current.set(value, leave);
|
|
488
|
+
React.startTransition(() => setAnswers((count) => count + 1));
|
|
489
|
+
}, []);
|
|
490
|
+
React.useLayoutEffect(() => {
|
|
491
|
+
if (answeredRef.current.size === 0) return;
|
|
492
|
+
const answered = answeredRef.current;
|
|
493
|
+
answeredRef.current = /* @__PURE__ */ new Map();
|
|
494
|
+
returnNow(answered);
|
|
495
|
+
}, [answers, returnNow]);
|
|
496
|
+
const complete = React.useCallback((details) => {
|
|
497
|
+
const leave = sentRef.current.get(details.value);
|
|
498
|
+
if (leave === void 0) {
|
|
499
|
+
onSwipeCompleteRef.current?.(details);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
let answer;
|
|
503
|
+
try {
|
|
504
|
+
answer = onSwipeCompleteRef.current?.(details);
|
|
505
|
+
} finally {
|
|
506
|
+
if (isThenable(answer)) Promise.resolve(answer).finally(() => endLeave(details.value, leave));
|
|
507
|
+
else endLeave(details.value, leave);
|
|
508
|
+
}
|
|
509
|
+
}, [endLeave]);
|
|
286
510
|
const leaving = React.useMemo(() => new Set(leavingList), [leavingList]);
|
|
287
511
|
const positions = React.useMemo(() => new Map(order.filter((value) => !leaving.has(value)).map((value, index) => [value, index])), [order, leaving]);
|
|
288
512
|
const front = order.find((value) => !leaving.has(value));
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
513
|
+
React.useLayoutEffect(() => {
|
|
514
|
+
setSettledFront(void 0);
|
|
515
|
+
}, [front]);
|
|
516
|
+
const positionsRef = React.useRef(positions);
|
|
517
|
+
const visibleRef = React.useRef(visibleCount);
|
|
518
|
+
React.useLayoutEffect(() => {
|
|
519
|
+
positionsRef.current = positions;
|
|
520
|
+
visibleRef.current = visibleCount;
|
|
521
|
+
});
|
|
522
|
+
const exitExtents = React.useCallback((value, element, x, y) => {
|
|
523
|
+
const own = element.getBoundingClientRect();
|
|
524
|
+
const home = {
|
|
525
|
+
left: own.left - x,
|
|
526
|
+
right: own.right - x,
|
|
527
|
+
top: own.top - y,
|
|
528
|
+
bottom: own.bottom - y
|
|
529
|
+
};
|
|
530
|
+
const pile = { ...home };
|
|
531
|
+
for (const [card, record] of itemsRef.current) {
|
|
532
|
+
const position = positionsRef.current.get(card);
|
|
533
|
+
if (card === value || position === void 0 || position >= visibleRef.current) continue;
|
|
534
|
+
const box = record.element.getBoundingClientRect();
|
|
535
|
+
pile.left = Math.min(pile.left, box.left);
|
|
536
|
+
pile.right = Math.max(pile.right, box.right);
|
|
537
|
+
pile.top = Math.min(pile.top, box.top);
|
|
538
|
+
pile.bottom = Math.max(pile.bottom, box.bottom);
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
right: pile.right - home.left,
|
|
542
|
+
left: home.right - pile.left,
|
|
543
|
+
down: pile.bottom - home.top,
|
|
544
|
+
up: home.bottom - pile.top
|
|
545
|
+
};
|
|
546
|
+
}, []);
|
|
547
|
+
const swipeFront = React.useCallback((direction, cause) => {
|
|
548
|
+
if (disabled || !directions.includes(direction)) return;
|
|
549
|
+
const next = readOrder().find((value) => !sentRef.current.has(value));
|
|
550
|
+
if (next !== void 0) itemsRef.current.get(next)?.swipe(direction, cause);
|
|
292
551
|
}, [
|
|
293
552
|
directions,
|
|
294
553
|
disabled,
|
|
295
|
-
|
|
554
|
+
readOrder
|
|
296
555
|
]);
|
|
297
|
-
const announcedRef = React.useRef(
|
|
556
|
+
const announcedRef = React.useRef(null);
|
|
298
557
|
React.useEffect(() => {
|
|
558
|
+
if (!isReady) return;
|
|
299
559
|
const previous = announcedRef.current;
|
|
300
|
-
announcedRef.current = front;
|
|
560
|
+
announcedRef.current = { front };
|
|
301
561
|
const status = statusRef.current;
|
|
302
|
-
if (previous
|
|
562
|
+
if (!previous || previous.front === front || !status) return;
|
|
303
563
|
const element = front === void 0 ? emptyRef.current : itemsRef.current.get(front)?.element;
|
|
304
564
|
status.textContent = element ? element.getAttribute("aria-label") ?? (element.innerText || element.textContent || "") : "";
|
|
305
|
-
}, [front]);
|
|
565
|
+
}, [front, isReady]);
|
|
306
566
|
const handleKeyDown = (event) => {
|
|
307
567
|
onKeyDown?.(event);
|
|
308
568
|
if (event.defaultPrevented || event.target !== event.currentTarget) return;
|
|
@@ -310,7 +570,11 @@ function SwipeDeck({ onSwipe, directions = DEFAULT_DIRECTIONS, disabled = false,
|
|
|
310
570
|
const direction = KEY_DIRECTIONS[event.key];
|
|
311
571
|
if (!direction || disabled || !directions.includes(direction)) return;
|
|
312
572
|
event.preventDefault();
|
|
313
|
-
|
|
573
|
+
if (event.repeat) return;
|
|
574
|
+
swipeFront(direction, {
|
|
575
|
+
reason: "keyboard",
|
|
576
|
+
event: event.nativeEvent
|
|
577
|
+
});
|
|
314
578
|
};
|
|
315
579
|
const context = React.useMemo(() => ({
|
|
316
580
|
directions,
|
|
@@ -319,11 +583,16 @@ function SwipeDeck({ onSwipe, directions = DEFAULT_DIRECTIONS, disabled = false,
|
|
|
319
583
|
front,
|
|
320
584
|
positions,
|
|
321
585
|
leaving,
|
|
586
|
+
leavingOrder: leavingList,
|
|
322
587
|
count: order.length,
|
|
588
|
+
visible: visibleCount,
|
|
323
589
|
lift,
|
|
324
590
|
register,
|
|
325
|
-
|
|
326
|
-
|
|
591
|
+
send,
|
|
592
|
+
isMovedBack,
|
|
593
|
+
rejoin,
|
|
594
|
+
complete,
|
|
595
|
+
exitExtents,
|
|
327
596
|
swipeFront,
|
|
328
597
|
isEmpty: isReady && front === void 0,
|
|
329
598
|
emptyRef,
|
|
@@ -337,11 +606,16 @@ function SwipeDeck({ onSwipe, directions = DEFAULT_DIRECTIONS, disabled = false,
|
|
|
337
606
|
front,
|
|
338
607
|
positions,
|
|
339
608
|
leaving,
|
|
609
|
+
leavingList,
|
|
340
610
|
order.length,
|
|
611
|
+
visibleCount,
|
|
341
612
|
lift,
|
|
342
613
|
register,
|
|
343
|
-
|
|
344
|
-
|
|
614
|
+
send,
|
|
615
|
+
isMovedBack,
|
|
616
|
+
rejoin,
|
|
617
|
+
complete,
|
|
618
|
+
exitExtents,
|
|
345
619
|
swipeFront,
|
|
346
620
|
focusDeck,
|
|
347
621
|
settledFront
|
|
@@ -357,11 +631,11 @@ function SwipeDeck({ onSwipe, directions = DEFAULT_DIRECTIONS, disabled = false,
|
|
|
357
631
|
role: "group",
|
|
358
632
|
tabIndex: 0,
|
|
359
633
|
"aria-disabled": disabled || void 0,
|
|
360
|
-
className: cn("group/swipe-deck relative isolate grid outline-hidden", "ring-focus/50 rounded-lg focus-visible:not-has-data-front:ring-3", className),
|
|
634
|
+
className: cn("group/swipe-deck relative isolate grid focus-visible:outline-hidden", "[--swipe-deck-side:1] rtl:[--swipe-deck-side:-1]", "ring-focus/50 rounded-lg focus-visible:not-has-data-front:ring-3", className),
|
|
361
635
|
style: {
|
|
362
636
|
"--swipe-deck-spread": within(spread, 0, 2, .5),
|
|
363
637
|
"--swipe-deck-depth": within(depth, 0, 1, .5),
|
|
364
|
-
"--swipe-deck-visible":
|
|
638
|
+
"--swipe-deck-visible": visibleCount,
|
|
365
639
|
...style
|
|
366
640
|
},
|
|
367
641
|
onKeyDown: handleKeyDown,
|
|
@@ -376,37 +650,46 @@ function SwipeDeck({ onSwipe, directions = DEFAULT_DIRECTIONS, disabled = false,
|
|
|
376
650
|
});
|
|
377
651
|
}
|
|
378
652
|
/**
|
|
379
|
-
* One card in the deck. It sits in the deck's first grid
|
|
380
|
-
* card, so the deck takes the size of its largest card.
|
|
653
|
+
* One card in the deck, a direct child of it. It sits in the deck's first grid
|
|
654
|
+
* cell with every other card, so the deck takes the size of its largest card.
|
|
381
655
|
*
|
|
382
656
|
* Styling hooks: `data-front` on the top card, `data-swiping` while it is
|
|
383
|
-
* dragged, `data-leaving` from the moment it leaves, `data-
|
|
384
|
-
* it
|
|
385
|
-
* `data-swipe-direction` for the
|
|
386
|
-
*
|
|
387
|
-
* `--swipe-deck-
|
|
388
|
-
*
|
|
657
|
+
* dragged, `data-leaving` from the moment it leaves, `data-starting-style`
|
|
658
|
+
* while it comes in and `data-ending-style` once it is clear of the pile and
|
|
659
|
+
* fading (the `motion-fade-lg` recipe), and `data-swipe-direction` for the
|
|
660
|
+
* way it leans or leaves. CSS variables, which what's inside the card can
|
|
661
|
+
* read too: `--swipe-deck-index` (its place in the pile, 0 at the front),
|
|
662
|
+
* `--swipe-deck-place` (that place as it springs from one to the next),
|
|
389
663
|
* `--swipe-deck-movement-x` / `-y` in px, and `--swipe-deck-progress` from 0
|
|
390
664
|
* at rest to 1 where letting go makes the card leave.
|
|
391
665
|
*
|
|
392
|
-
* The card owns its translate, rotate and
|
|
393
|
-
* be a `motion-fade-*` one: a recipe that transitions those
|
|
394
|
-
* drag.
|
|
666
|
+
* The card owns its translate, rotate, scale and transform, so its motion
|
|
667
|
+
* recipe can only be a `motion-fade-*` one: a recipe that transitions those
|
|
668
|
+
* would lag every drag.
|
|
395
669
|
*/
|
|
396
670
|
function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCapture, onDragStart, ...props }) {
|
|
397
671
|
const deck = useSwipeDeckContext("SwipeDeckItem");
|
|
398
|
-
const { directions, register,
|
|
672
|
+
const { directions, register, send, complete, rejoin, isMovedBack, exitExtents, focusDeck, settle } = deck;
|
|
399
673
|
const prefersReducedMotion = useReducedMotion();
|
|
674
|
+
const isHydrating = !useIsClientRender();
|
|
675
|
+
const wasHydratedRef = React.useRef(isHydrating);
|
|
400
676
|
const elementRef = React.useRef(null);
|
|
401
677
|
const mergedRef = React.useMemo(() => mergeRefs(elementRef, ref), [ref]);
|
|
402
678
|
const x = useMotionValue(0);
|
|
403
679
|
const y = useMotionValue(0);
|
|
404
680
|
const place = useMotionValue(0);
|
|
405
681
|
const enterScale = useMotionValue(1);
|
|
682
|
+
const press = useMotionValue(1);
|
|
683
|
+
const tiltX = useMotionValue(0);
|
|
684
|
+
const tiltY = useMotionValue(0);
|
|
685
|
+
const spin = useMotionValue(0);
|
|
686
|
+
const grabRef = React.useRef([0, 0]);
|
|
406
687
|
const drivesLiftRef = React.useRef(false);
|
|
407
688
|
const scatter = React.useMemo(() => scatterOf(value), [value]);
|
|
408
689
|
const leftTowardRef = React.useRef(null);
|
|
409
690
|
const stopWatchingRef = React.useRef(null);
|
|
691
|
+
const exitRef = React.useRef(null);
|
|
692
|
+
const isTuckingRef = React.useRef(false);
|
|
410
693
|
const dragRef = React.useRef(null);
|
|
411
694
|
const endDragRef = React.useRef(null);
|
|
412
695
|
const sizeRef = React.useRef({
|
|
@@ -416,15 +699,16 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
416
699
|
const draggedRef = React.useRef(false);
|
|
417
700
|
const [isSwiping, setIsSwiping] = React.useState(false);
|
|
418
701
|
const [isFading, setIsFading] = React.useState(false);
|
|
419
|
-
const [isStarting, setIsStarting] = React.useState(
|
|
702
|
+
const [isStarting, setIsStarting] = React.useState(!isHydrating);
|
|
420
703
|
const [direction, setDirection] = React.useState();
|
|
421
704
|
const position = deck.positions.get(value);
|
|
422
705
|
const isLeaving = deck.leaving.has(value);
|
|
423
706
|
const isFront = deck.front === value;
|
|
707
|
+
const isInert = isLeaving || position !== void 0 && position > 0;
|
|
424
708
|
const directionsRef = React.useRef(directions);
|
|
425
709
|
const disabledRef = React.useRef(deck.disabled);
|
|
426
710
|
const reducedMotionRef = React.useRef(prefersReducedMotion);
|
|
427
|
-
React.
|
|
711
|
+
React.useLayoutEffect(() => {
|
|
428
712
|
directionsRef.current = directions;
|
|
429
713
|
disabledRef.current = deck.disabled;
|
|
430
714
|
reducedMotionRef.current = prefersReducedMotion;
|
|
@@ -440,34 +724,74 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
440
724
|
element.style.setProperty("--swipe-deck-progress", String(Math.min(share, 1)));
|
|
441
725
|
if (drivesLiftRef.current) deck.lift.set(Math.min(share, 1) * LIFT);
|
|
442
726
|
};
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
727
|
+
const writePose = () => {
|
|
728
|
+
const element = elementRef.current;
|
|
729
|
+
if (!element) return;
|
|
730
|
+
const [grabX, grabY] = grabRef.current;
|
|
731
|
+
element.style.setProperty("--swipe-deck-pose", isPoseAtRest(press, spin, tiltX, tiltY) ? "none" : `translate(${grabX}px, ${grabY}px) perspective(${PERSPECTIVE}px) scale(${press.get()}) rotate(${spin.get()}deg) rotateX(${tiltX.get()}deg) rotateY(${tiltY.get()}deg) translate(${-grabX}px, ${-grabY}px)`);
|
|
732
|
+
};
|
|
733
|
+
const writeNumber = (name) => (latest) => {
|
|
734
|
+
elementRef.current?.style.setProperty(name, String(latest));
|
|
735
|
+
};
|
|
736
|
+
const stops = [
|
|
737
|
+
x.on("change", write),
|
|
738
|
+
y.on("change", write),
|
|
739
|
+
place.on("change", writeNumber("--swipe-deck-place")),
|
|
740
|
+
enterScale.on("change", writeNumber("--swipe-deck-enter-scale")),
|
|
741
|
+
press.on("change", writePose),
|
|
742
|
+
spin.on("change", writePose),
|
|
743
|
+
tiltX.on("change", writePose),
|
|
744
|
+
tiltY.on("change", writePose)
|
|
745
|
+
];
|
|
746
|
+
const exit = exitRef.current;
|
|
747
|
+
if (exit) {
|
|
748
|
+
exitRef.current = null;
|
|
749
|
+
x.jump(exit[0]);
|
|
750
|
+
y.jump(exit[1]);
|
|
751
|
+
setIsFading(true);
|
|
752
|
+
}
|
|
451
753
|
return () => {
|
|
452
|
-
|
|
453
|
-
stopY();
|
|
454
|
-
stopPlace();
|
|
455
|
-
stopEnterScale();
|
|
754
|
+
for (const stop of stops) stop();
|
|
456
755
|
if (dragRef.current) releaseLift(deck.lift, reducedMotionRef.current);
|
|
457
756
|
endDragRef.current?.();
|
|
458
757
|
stopWatchingRef.current?.();
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
758
|
+
for (const motionValue of [
|
|
759
|
+
x,
|
|
760
|
+
y,
|
|
761
|
+
place,
|
|
762
|
+
enterScale,
|
|
763
|
+
press,
|
|
764
|
+
spin,
|
|
765
|
+
tiltX,
|
|
766
|
+
tiltY
|
|
767
|
+
]) motionValue.stop();
|
|
463
768
|
};
|
|
464
769
|
}, [
|
|
465
770
|
deck.lift,
|
|
466
771
|
enterScale,
|
|
467
772
|
place,
|
|
773
|
+
press,
|
|
774
|
+
spin,
|
|
775
|
+
tiltX,
|
|
776
|
+
tiltY,
|
|
468
777
|
x,
|
|
469
778
|
y
|
|
470
779
|
]);
|
|
780
|
+
React.useEffect(() => {
|
|
781
|
+
const element = elementRef.current;
|
|
782
|
+
if (!element || position === void 0 || position > deck.visible) return;
|
|
783
|
+
const writeLift = (latest) => element.style.setProperty("--swipe-deck-lift", String(latest));
|
|
784
|
+
writeLift(deck.lift.get());
|
|
785
|
+
const stop = deck.lift.on("change", writeLift);
|
|
786
|
+
return () => {
|
|
787
|
+
stop();
|
|
788
|
+
element.style.removeProperty("--swipe-deck-lift");
|
|
789
|
+
};
|
|
790
|
+
}, [
|
|
791
|
+
deck.lift,
|
|
792
|
+
deck.visible,
|
|
793
|
+
position
|
|
794
|
+
]);
|
|
471
795
|
const snapBack = React.useCallback(([vx, vy] = [0, 0]) => {
|
|
472
796
|
if (prefersReducedMotion) {
|
|
473
797
|
x.set(0);
|
|
@@ -487,35 +811,72 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
487
811
|
x,
|
|
488
812
|
y
|
|
489
813
|
]);
|
|
490
|
-
const
|
|
814
|
+
const settleFace = React.useCallback(() => {
|
|
815
|
+
for (const [motionValue, rest] of [
|
|
816
|
+
[press, 1],
|
|
817
|
+
[spin, 0],
|
|
818
|
+
[tiltX, 0],
|
|
819
|
+
[tiltY, 0]
|
|
820
|
+
]) if (reducedMotionRef.current) motionValue.jump(rest);
|
|
821
|
+
else animate(motionValue, rest, FACE_SPRING);
|
|
822
|
+
}, [
|
|
823
|
+
press,
|
|
824
|
+
spin,
|
|
825
|
+
tiltX,
|
|
826
|
+
tiltY
|
|
827
|
+
]);
|
|
828
|
+
const tilt = React.useCallback((vx, vy) => {
|
|
829
|
+
if (reducedMotionRef.current) return;
|
|
830
|
+
const sideways = clampUnit(vx / LEAN_SPEED) * .7 + clampUnit(x.get() / LEAN_REACH) * .3;
|
|
831
|
+
const upward = clampUnit(-vy / LEAN_SPEED) * .7 + clampUnit(-y.get() / LEAN_REACH) * .3;
|
|
832
|
+
animate(tiltY, sideways * TILT, LEAN_SPRING);
|
|
833
|
+
animate(tiltX, upward * TILT, LEAN_SPRING);
|
|
834
|
+
animate(spin, clampUnit(x.get() / LEAN_REACH) * SPIN, LEAN_SPRING);
|
|
835
|
+
}, [
|
|
836
|
+
spin,
|
|
837
|
+
tiltX,
|
|
838
|
+
tiltY,
|
|
839
|
+
x,
|
|
840
|
+
y
|
|
841
|
+
]);
|
|
842
|
+
const fling = React.useCallback((to, velocity) => {
|
|
843
|
+
const [vx, vy] = velocity ?? [0, 0];
|
|
491
844
|
endDragRef.current?.();
|
|
492
845
|
setIsSwiping(false);
|
|
493
846
|
leftTowardRef.current = to;
|
|
494
|
-
leave(value);
|
|
495
847
|
setDirection(to);
|
|
496
848
|
drivesLiftRef.current = false;
|
|
497
849
|
releaseLift(deck.lift, prefersReducedMotion);
|
|
850
|
+
settleFace();
|
|
498
851
|
setIsStarting(false);
|
|
499
|
-
|
|
852
|
+
const element = elementRef.current;
|
|
853
|
+
if (prefersReducedMotion || !element) {
|
|
500
854
|
setIsFading(true);
|
|
501
855
|
return;
|
|
502
856
|
}
|
|
503
|
-
const
|
|
857
|
+
const extents = exitExtents(value, element, x.get(), y.get());
|
|
504
858
|
const aheadX = x.get() + vx * THROW_TIME;
|
|
505
859
|
const aheadY = y.get() + vy * THROW_TIME;
|
|
506
|
-
const
|
|
507
|
-
const
|
|
508
|
-
|
|
860
|
+
const heading = velocity && Math.hypot(aheadX, aheadY) >= 1 ? [aheadX, aheadY] : DIRECTION_VECTORS[to];
|
|
861
|
+
const [targetX, targetY] = exitPoint(x.get(), y.get(), heading, extents);
|
|
862
|
+
exitRef.current = [targetX, targetY];
|
|
509
863
|
const headingLength = Math.hypot(...heading);
|
|
510
864
|
const [ux, uy] = [heading[0] / headingLength, heading[1] / headingLength];
|
|
511
|
-
|
|
865
|
+
let isOut = false;
|
|
866
|
+
const leaveSight = () => {
|
|
867
|
+
if (isOut) return;
|
|
868
|
+
isOut = true;
|
|
869
|
+
exitRef.current = null;
|
|
512
870
|
stopWatchingRef.current?.();
|
|
513
871
|
stopWatchingRef.current = null;
|
|
514
|
-
|
|
872
|
+
if (isMovedBack(value)) {
|
|
873
|
+
isTuckingRef.current = true;
|
|
874
|
+
rejoin(value);
|
|
875
|
+
} else setIsFading(true);
|
|
515
876
|
};
|
|
516
877
|
const check = () => {
|
|
517
878
|
const outward = x.getVelocity() * ux + y.getVelocity() * uy;
|
|
518
|
-
if (isClear(x.get(), y.get(),
|
|
879
|
+
if (isClear(x.get(), y.get(), extents, [ux, uy]) && outward <= CAUGHT_SPEED) leaveSight();
|
|
519
880
|
};
|
|
520
881
|
stopWatchingRef.current?.();
|
|
521
882
|
const stopX = x.on("change", check);
|
|
@@ -528,36 +889,47 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
528
889
|
animate(x, targetX, {
|
|
529
890
|
...EXIT_SPRING,
|
|
530
891
|
velocity: vx,
|
|
531
|
-
onComplete: isMainX ?
|
|
892
|
+
onComplete: isMainX ? leaveSight : void 0
|
|
532
893
|
});
|
|
533
894
|
animate(y, targetY, {
|
|
534
895
|
...EXIT_SPRING,
|
|
535
896
|
velocity: vy,
|
|
536
|
-
onComplete: isMainX ? void 0 :
|
|
897
|
+
onComplete: isMainX ? void 0 : leaveSight
|
|
537
898
|
});
|
|
538
899
|
}, [
|
|
539
900
|
deck.lift,
|
|
540
|
-
|
|
901
|
+
exitExtents,
|
|
902
|
+
isMovedBack,
|
|
541
903
|
prefersReducedMotion,
|
|
904
|
+
rejoin,
|
|
905
|
+
settleFace,
|
|
542
906
|
value,
|
|
543
907
|
x,
|
|
544
908
|
y
|
|
545
909
|
]);
|
|
910
|
+
const swipe = (to, cause, velocity) => {
|
|
911
|
+
if (!send({
|
|
912
|
+
value,
|
|
913
|
+
direction: to
|
|
914
|
+
}, cause)) return false;
|
|
915
|
+
fling(to, velocity);
|
|
916
|
+
return true;
|
|
917
|
+
};
|
|
918
|
+
const swipeRef = React.useRef(swipe);
|
|
919
|
+
React.useEffect(() => {
|
|
920
|
+
swipeRef.current = swipe;
|
|
921
|
+
});
|
|
546
922
|
React.useLayoutEffect(() => {
|
|
547
923
|
const element = elementRef.current;
|
|
548
924
|
if (!element) return;
|
|
549
925
|
return register(value, {
|
|
550
926
|
element,
|
|
551
|
-
swipe: (to) => {
|
|
927
|
+
swipe: (to, cause) => {
|
|
552
928
|
sizeRef.current = sizeOf(element);
|
|
553
|
-
|
|
929
|
+
swipeRef.current(to, cause);
|
|
554
930
|
}
|
|
555
931
|
});
|
|
556
|
-
}, [
|
|
557
|
-
fling,
|
|
558
|
-
register,
|
|
559
|
-
value
|
|
560
|
-
]);
|
|
932
|
+
}, [register, value]);
|
|
561
933
|
React.useEffect(() => {
|
|
562
934
|
const element = elementRef.current;
|
|
563
935
|
const to = leftTowardRef.current;
|
|
@@ -568,7 +940,7 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
568
940
|
if (isCancelled) return;
|
|
569
941
|
x.stop();
|
|
570
942
|
y.stop();
|
|
571
|
-
|
|
943
|
+
complete({
|
|
572
944
|
value,
|
|
573
945
|
direction: to
|
|
574
946
|
});
|
|
@@ -579,7 +951,7 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
579
951
|
cancelAnimationFrame(frame);
|
|
580
952
|
};
|
|
581
953
|
}, [
|
|
582
|
-
|
|
954
|
+
complete,
|
|
583
955
|
isFading,
|
|
584
956
|
value,
|
|
585
957
|
x,
|
|
@@ -587,33 +959,68 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
587
959
|
]);
|
|
588
960
|
const wasLeavingRef = React.useRef(false);
|
|
589
961
|
const isPlacedRef = React.useRef(false);
|
|
962
|
+
const springPlace = React.useCallback((target, spring) => {
|
|
963
|
+
const shown = Math.min(target, deck.visible);
|
|
964
|
+
animate(place, shown, {
|
|
965
|
+
...spring,
|
|
966
|
+
onComplete: shown === target ? void 0 : () => place.jump(target)
|
|
967
|
+
});
|
|
968
|
+
}, [deck.visible, place]);
|
|
969
|
+
const enterAtRef = React.useRef(null);
|
|
970
|
+
const isReenteringRef = React.useRef(false);
|
|
590
971
|
React.useEffect(() => {
|
|
591
972
|
const target = isLeaving ? 0 : position;
|
|
592
973
|
if (target === void 0) return;
|
|
593
974
|
if (wasLeavingRef.current && !isLeaving) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
975
|
+
stopWatchingRef.current?.();
|
|
976
|
+
stopWatchingRef.current = null;
|
|
977
|
+
exitRef.current = null;
|
|
978
|
+
if (isTuckingRef.current) {
|
|
979
|
+
isTuckingRef.current = false;
|
|
980
|
+
animate(x, 0, TUCK_SPRING);
|
|
981
|
+
animate(y, 0, TUCK_SPRING);
|
|
982
|
+
springPlace(target, TUCK_SPRING);
|
|
983
|
+
const to = leftTowardRef.current;
|
|
984
|
+
if (to) complete({
|
|
985
|
+
value,
|
|
986
|
+
direction: to
|
|
987
|
+
});
|
|
988
|
+
} else {
|
|
989
|
+
x.jump(0);
|
|
990
|
+
y.jump(0);
|
|
991
|
+
place.jump(target);
|
|
992
|
+
setIsFading(false);
|
|
993
|
+
enterAtRef.current = null;
|
|
994
|
+
isReenteringRef.current = true;
|
|
995
|
+
setIsStarting(true);
|
|
996
|
+
}
|
|
997
|
+
} else if (prefersReducedMotion || !isPlacedRef.current || place.get() > deck.visible && target > deck.visible) place.jump(target);
|
|
998
|
+
else springPlace(target, PLACE_SPRING);
|
|
600
999
|
isPlacedRef.current = true;
|
|
601
1000
|
wasLeavingRef.current = isLeaving;
|
|
602
1001
|
}, [
|
|
1002
|
+
complete,
|
|
1003
|
+
deck.visible,
|
|
603
1004
|
isLeaving,
|
|
604
1005
|
place,
|
|
605
1006
|
position,
|
|
606
1007
|
prefersReducedMotion,
|
|
1008
|
+
springPlace,
|
|
1009
|
+
value,
|
|
607
1010
|
x,
|
|
608
1011
|
y
|
|
609
1012
|
]);
|
|
610
|
-
|
|
1013
|
+
React.useLayoutEffect(() => {
|
|
1014
|
+
if (wasHydratedRef.current && elementRef.current?.matches(`${ITEM_SELECTOR} ~ *`)) setIsStarting(true);
|
|
1015
|
+
}, []);
|
|
611
1016
|
React.useEffect(() => {
|
|
612
1017
|
if (!isStarting || position === void 0) return;
|
|
613
1018
|
if (enterAtRef.current === null) {
|
|
614
|
-
const
|
|
1019
|
+
const isBehind = !prefersReducedMotion && position > 0 && position < deck.visible;
|
|
1020
|
+
const delay = isBehind && !isReenteringRef.current ? ENTER_LEAD + (position - 1) * ENTER_STAGGER : 0;
|
|
1021
|
+
isReenteringRef.current = false;
|
|
615
1022
|
enterAtRef.current = performance.now() + delay * 1e3;
|
|
616
|
-
if (
|
|
1023
|
+
if (isBehind) {
|
|
617
1024
|
enterScale.jump(ENTER_SCALE);
|
|
618
1025
|
animate(enterScale, 1, {
|
|
619
1026
|
...PLACE_SPRING,
|
|
@@ -631,12 +1038,12 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
631
1038
|
cancelAnimationFrame(frame);
|
|
632
1039
|
};
|
|
633
1040
|
}, [
|
|
1041
|
+
deck.visible,
|
|
634
1042
|
enterScale,
|
|
635
1043
|
isStarting,
|
|
636
1044
|
position,
|
|
637
1045
|
prefersReducedMotion
|
|
638
1046
|
]);
|
|
639
|
-
const { settle } = deck;
|
|
640
1047
|
React.useEffect(() => {
|
|
641
1048
|
const element = elementRef.current;
|
|
642
1049
|
if (!isFront || isStarting || !element) return;
|
|
@@ -659,10 +1066,14 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
659
1066
|
settle,
|
|
660
1067
|
value
|
|
661
1068
|
]);
|
|
1069
|
+
React.useLayoutEffect(() => {
|
|
1070
|
+
if (isInert && elementRef.current?.contains(document.activeElement)) focusDeck();
|
|
1071
|
+
}, [focusDeck, isInert]);
|
|
662
1072
|
React.useEffect(() => {
|
|
663
1073
|
if (isFront && !deck.disabled || !dragRef.current) return;
|
|
664
1074
|
endDragRef.current?.();
|
|
665
1075
|
setIsSwiping(false);
|
|
1076
|
+
settleFace();
|
|
666
1077
|
snapBack();
|
|
667
1078
|
if (!isFront) releaseLift(deck.lift, prefersReducedMotion);
|
|
668
1079
|
}, [
|
|
@@ -670,9 +1081,11 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
670
1081
|
deck.lift,
|
|
671
1082
|
isFront,
|
|
672
1083
|
prefersReducedMotion,
|
|
1084
|
+
settleFace,
|
|
673
1085
|
snapBack
|
|
674
1086
|
]);
|
|
675
1087
|
const release = (drag, event) => {
|
|
1088
|
+
settleFace();
|
|
676
1089
|
if (!drag.moved) {
|
|
677
1090
|
snapBack();
|
|
678
1091
|
return;
|
|
@@ -687,13 +1100,14 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
687
1100
|
y: event.clientY,
|
|
688
1101
|
time: event.timeStamp
|
|
689
1102
|
});
|
|
690
|
-
const
|
|
1103
|
+
const [pointerVx, pointerVy] = releaseVelocity(drag.samples, event.timeStamp);
|
|
1104
|
+
const velocity = [isOpenSide(directionsRef.current, true, x.get()) ? pointerVx : 0, isOpenSide(directionsRef.current, false, y.get()) ? pointerVy : 0];
|
|
691
1105
|
if (event.type === "pointerup" && !disabledRef.current) {
|
|
692
1106
|
const ahead = lean(directionsRef.current, x.get() + velocity[0] * THROW_TIME, y.get() + velocity[1] * THROW_TIME, sizeRef.current);
|
|
693
|
-
if (ahead.direction && ahead.share >= 1
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
}
|
|
1107
|
+
if (ahead.direction && ahead.share >= 1 && swipe(ahead.direction, {
|
|
1108
|
+
reason: "swipe",
|
|
1109
|
+
event
|
|
1110
|
+
}, velocity)) return;
|
|
697
1111
|
}
|
|
698
1112
|
snapBack(velocity);
|
|
699
1113
|
};
|
|
@@ -706,40 +1120,60 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
706
1120
|
deck.lift.stop();
|
|
707
1121
|
x.stop();
|
|
708
1122
|
y.stop();
|
|
709
|
-
|
|
1123
|
+
const size = sizeOf(element);
|
|
1124
|
+
sizeRef.current = size;
|
|
1125
|
+
if (isPoseAtRest(press, spin, tiltX, tiltY)) grabRef.current = grabPoint(element, event.clientX, event.clientY, size);
|
|
1126
|
+
if (!prefersReducedMotion) animate(press, PRESS_SCALE, PRESS_SPRING);
|
|
1127
|
+
const openAtPress = directionsRef.current;
|
|
710
1128
|
const drag = {
|
|
711
1129
|
pointerId: event.pointerId,
|
|
712
1130
|
startX: event.clientX,
|
|
713
1131
|
startY: event.clientY,
|
|
714
|
-
originX: x.get(),
|
|
715
|
-
originY: y.get(),
|
|
1132
|
+
originX: isOpenSide(openAtPress, true, x.get()) ? x.get() : unresist(x.get(), size.width),
|
|
1133
|
+
originY: isOpenSide(openAtPress, false, y.get()) ? y.get() : unresist(y.get(), size.height),
|
|
716
1134
|
moved: false,
|
|
717
1135
|
samples: [{
|
|
718
1136
|
x: event.clientX,
|
|
719
1137
|
y: event.clientY,
|
|
720
1138
|
time: event.timeStamp
|
|
721
|
-
}]
|
|
1139
|
+
}],
|
|
1140
|
+
idle: void 0
|
|
722
1141
|
};
|
|
723
1142
|
dragRef.current = drag;
|
|
724
1143
|
const handleMove = (moveEvent) => {
|
|
725
1144
|
if (moveEvent.pointerId !== drag.pointerId) return;
|
|
1145
|
+
if (moveEvent.buttons === 0) {
|
|
1146
|
+
endDrag();
|
|
1147
|
+
release(drag, moveEvent);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
726
1150
|
const dx = moveEvent.clientX - drag.startX;
|
|
727
1151
|
const dy = moveEvent.clientY - drag.startY;
|
|
728
1152
|
if (!drag.moved) {
|
|
729
1153
|
if (Math.hypot(dx, dy) < DRAG_SLOP) return;
|
|
730
1154
|
drag.moved = true;
|
|
731
|
-
|
|
1155
|
+
try {
|
|
1156
|
+
element.setPointerCapture(drag.pointerId);
|
|
1157
|
+
} catch {}
|
|
732
1158
|
setIsSwiping(true);
|
|
733
1159
|
}
|
|
734
|
-
|
|
735
|
-
|
|
1160
|
+
const allowed = directionsRef.current;
|
|
1161
|
+
const { width, height } = sizeRef.current;
|
|
1162
|
+
x.set(follow(allowed, true, drag.originX + dx, width));
|
|
1163
|
+
y.set(follow(allowed, false, drag.originY + dy, height));
|
|
736
1164
|
drag.samples.push({
|
|
737
1165
|
x: moveEvent.clientX,
|
|
738
1166
|
y: moveEvent.clientY,
|
|
739
1167
|
time: moveEvent.timeStamp
|
|
740
1168
|
});
|
|
741
1169
|
if (drag.samples.length > 20) drag.samples.shift();
|
|
742
|
-
setDirection(lean(
|
|
1170
|
+
setDirection(lean(allowed, x.get(), y.get(), sizeRef.current).direction);
|
|
1171
|
+
const [pointerVx, pointerVy] = releaseVelocity(drag.samples, moveEvent.timeStamp);
|
|
1172
|
+
tilt(isOpenSide(allowed, true, x.get()) ? pointerVx : 0, isOpenSide(allowed, false, y.get()) ? pointerVy : 0);
|
|
1173
|
+
window.clearTimeout(drag.idle);
|
|
1174
|
+
drag.idle = window.setTimeout(() => {
|
|
1175
|
+
if (dragRef.current === drag) tilt(0, 0);
|
|
1176
|
+
}, LEAN_IDLE);
|
|
743
1177
|
};
|
|
744
1178
|
const handleEnd = (endEvent) => {
|
|
745
1179
|
if (endEvent.pointerId !== drag.pointerId) return;
|
|
@@ -747,6 +1181,7 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
747
1181
|
release(drag, endEvent);
|
|
748
1182
|
};
|
|
749
1183
|
const endDrag = () => {
|
|
1184
|
+
window.clearTimeout(drag.idle);
|
|
750
1185
|
window.removeEventListener("pointermove", handleMove);
|
|
751
1186
|
window.removeEventListener("pointerup", handleEnd);
|
|
752
1187
|
window.removeEventListener("pointercancel", handleEnd);
|
|
@@ -767,7 +1202,7 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
767
1202
|
}
|
|
768
1203
|
onClickCapture?.(event);
|
|
769
1204
|
};
|
|
770
|
-
const zIndex = isLeaving ? deck.count +
|
|
1205
|
+
const zIndex = isLeaving ? deck.count + deck.leavingOrder.length - deck.leavingOrder.indexOf(value) : position === void 0 ? void 0 : deck.count - position;
|
|
771
1206
|
return /* @__PURE__ */ jsx("div", {
|
|
772
1207
|
ref: mergedRef,
|
|
773
1208
|
"data-slot": "swipe-deck-item",
|
|
@@ -778,8 +1213,8 @@ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCap
|
|
|
778
1213
|
"data-starting-style": isStarting ? "" : void 0,
|
|
779
1214
|
"data-ending-style": isFading ? "" : void 0,
|
|
780
1215
|
"data-swipe-direction": isSwiping || isLeaving ? direction : void 0,
|
|
781
|
-
inert:
|
|
782
|
-
className: cn("motion-fade-lg relative col-start-1 row-start-1 rounded-lg select-none", "[--swipe-deck-slot:calc(var(--swipe-deck-place,0)_-_min(var(--swipe-deck-place,0),1)_*_var(--swipe-deck-lift,0))]", "[--swipe-deck-layout-slot:min(var(--swipe-deck-slot),calc(var(--swipe-deck-visible,4)_-_1))]", "[translate:calc(var(--swipe-deck-movement-x,0px)_+_var(--swipe-deck-layout-x,0px))_calc(var(--swipe-deck-movement-y,0px)_+_var(--swipe-deck-layout-y,0px))]", "[rotate:var(--swipe-deck-layout-rotate,0deg)] [scale:calc(var(--swipe-deck-layout-scale,1)_*_var(--swipe-deck-enter-scale,1))]", "[opacity:clamp(0,calc(var(--swipe-deck-visible,4)_-_var(--swipe-deck-slot)),1)]", "group-data-[layout=stack]/swipe-deck:origin-top", "group-data-[layout=stack]/swipe-deck:[--swipe-deck-layout-y:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_-32px)]", "group-data-[layout=stack]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.1))]", "group-data-[layout=fan]/swipe-deck:origin-[90%_90%]", "group-data-[layout=fan]/swipe-deck:[--swipe-deck-layout-rotate:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*
|
|
1216
|
+
inert: isInert,
|
|
1217
|
+
className: cn("motion-fade-lg relative col-start-1 row-start-1 rounded-lg select-none", "[--swipe-deck-slot:calc(var(--swipe-deck-place,0)_-_min(var(--swipe-deck-place,0),1)_*_var(--swipe-deck-lift,0))]", "[--swipe-deck-layout-slot:min(var(--swipe-deck-slot),calc(var(--swipe-deck-visible,4)_-_1))]", "[translate:calc(var(--swipe-deck-movement-x,0px)_+_var(--swipe-deck-layout-x,0px))_calc(var(--swipe-deck-movement-y,0px)_+_var(--swipe-deck-layout-y,0px))]", "[rotate:var(--swipe-deck-layout-rotate,0deg)] [scale:calc(var(--swipe-deck-layout-scale,1)_*_var(--swipe-deck-enter-scale,1))]", "[transform:var(--swipe-deck-pose,none)]", "[opacity:clamp(0,calc(var(--swipe-deck-visible,4)_-_var(--swipe-deck-slot)),1)]", "group-data-[layout=stack]/swipe-deck:origin-top", "group-data-[layout=stack]/swipe-deck:[--swipe-deck-layout-y:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_-32px)]", "group-data-[layout=stack]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.1))]", "group-data-[layout=fan]/swipe-deck:origin-[90%_90%] group-data-[layout=fan]/swipe-deck:rtl:origin-[10%_90%]", "group-data-[layout=fan]/swipe-deck:[--swipe-deck-layout-rotate:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_8deg_*_var(--swipe-deck-side,1))]", "group-data-[layout=fan]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.12))]", "group-data-[layout=cascade]/swipe-deck:[--swipe-deck-layout-x:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_28px_*_var(--swipe-deck-side,1))]", "group-data-[layout=cascade]/swipe-deck:[--swipe-deck-layout-y:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_-28px)]", "group-data-[layout=cascade]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.07))]", "group-data-[layout=pile]/swipe-deck:[--swipe-deck-layout-x:calc(var(--swipe-deck-scatter-x)_*_var(--swipe-deck-spread,0.5)_*_56px)]", "group-data-[layout=pile]/swipe-deck:[--swipe-deck-layout-y:calc(var(--swipe-deck-scatter-y)_*_var(--swipe-deck-spread,0.5)_*_44px)]", "group-data-[layout=pile]/swipe-deck:[--swipe-deck-layout-rotate:calc(var(--swipe-deck-scatter-rotate)_*_var(--swipe-deck-spread,0.5)_*_40deg)]", "group-data-[layout=pile]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.03))]", "group-focus-visible/swipe-deck:data-front:ring-focus/50 group-focus-visible/swipe-deck:data-front:ring-3", "data-front:cursor-grab data-swiping:cursor-grabbing group-data-disabled/swipe-deck:data-front:cursor-auto", "[-webkit-tap-highlight-color:transparent] [-webkit-touch-callout:none]", !deck.isReady && "[&:is([data-slot=swipe-deck-item]~*)]:invisible [&:is([data-slot=swipe-deck-item]~*)]:opacity-0", touchAction(directions), className),
|
|
783
1218
|
style: {
|
|
784
1219
|
zIndex,
|
|
785
1220
|
"--swipe-deck-index": isLeaving ? 0 : position,
|
|
@@ -873,10 +1308,11 @@ function DashedOutline() {
|
|
|
873
1308
|
className: "pointer-events-none absolute inset-0 size-full overflow-visible",
|
|
874
1309
|
"aria-hidden": true,
|
|
875
1310
|
children: /* @__PURE__ */ jsx("path", {
|
|
1311
|
+
"data-slot": "swipe-deck-empty-outline-path",
|
|
876
1312
|
d: outline.d,
|
|
877
1313
|
pathLength,
|
|
878
1314
|
strokeDasharray: `${OUTLINE_DASH} ${OUTLINE_GAP}`,
|
|
879
|
-
className: "stroke-
|
|
1315
|
+
className: "stroke-contour fill-none"
|
|
880
1316
|
})
|
|
881
1317
|
});
|
|
882
1318
|
}
|
|
@@ -886,9 +1322,8 @@ function DashedOutline() {
|
|
|
886
1322
|
* their size, inside a dashed outline of their shape (`data-slot`
|
|
887
1323
|
* `swipe-deck-empty-outline`, to restyle or hide). It is drawn as soon as one
|
|
888
1324
|
* card is left and has come to rest, under that card, so dragging it shows
|
|
889
|
-
* what comes next; it
|
|
890
|
-
*
|
|
891
|
-
* or its text) once that last card has gone too.
|
|
1325
|
+
* what comes next; it can be reached (Tab, clicks) and screen readers hear it
|
|
1326
|
+
* (its `aria-label`, or its text) once that last card has gone too.
|
|
892
1327
|
*/
|
|
893
1328
|
function SwipeDeckEmpty({ className, children, ref, ...props }) {
|
|
894
1329
|
const deck = useSwipeDeckContext("SwipeDeckEmpty");
|
|
@@ -910,9 +1345,10 @@ function SwipeDeckEmpty({ className, children, ref, ...props }) {
|
|
|
910
1345
|
}
|
|
911
1346
|
/**
|
|
912
1347
|
* A button that sends the front card off in `direction`: the way to swipe
|
|
913
|
-
* without dragging. Disabled when the deck is
|
|
1348
|
+
* without dragging. Disabled (and still focusable) when the deck is disabled,
|
|
1349
|
+
* has no card in front, or can't go that way.
|
|
914
1350
|
*/
|
|
915
|
-
function SwipeDeckAction({ direction, disabled, onClick, ...props }) {
|
|
1351
|
+
function SwipeDeckAction({ direction, disabled, onClick, onKeyDown, ...props }) {
|
|
916
1352
|
const deck = useSwipeDeckContext("SwipeDeckAction");
|
|
917
1353
|
const unavailable = deck.disabled || deck.isReady && deck.front === void 0 || !deck.directions.includes(direction);
|
|
918
1354
|
return /* @__PURE__ */ jsx(Button, {
|
|
@@ -922,7 +1358,14 @@ function SwipeDeckAction({ direction, disabled, onClick, ...props }) {
|
|
|
922
1358
|
focusableWhenDisabled: true,
|
|
923
1359
|
onClick: (event) => {
|
|
924
1360
|
onClick?.(event);
|
|
925
|
-
if (!event.defaultPrevented) deck.swipeFront(direction
|
|
1361
|
+
if (!event.defaultPrevented) deck.swipeFront(direction, {
|
|
1362
|
+
reason: "action-press",
|
|
1363
|
+
event: event.nativeEvent
|
|
1364
|
+
});
|
|
1365
|
+
},
|
|
1366
|
+
onKeyDown: (event) => {
|
|
1367
|
+
onKeyDown?.(event);
|
|
1368
|
+
if (event.repeat && event.key === "Enter") event.preventDefault();
|
|
926
1369
|
},
|
|
927
1370
|
...props
|
|
928
1371
|
});
|