@present-day/reveal-row 0.1.6 → 0.3.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/dist/index.mjs CHANGED
@@ -62,7 +62,8 @@ import {
62
62
  useEffect,
63
63
  useImperativeHandle,
64
64
  useLayoutEffect,
65
- useRef
65
+ useRef,
66
+ useState
66
67
  } from "react";
67
68
 
68
69
  // src/DefaultHandleIcon.tsx
@@ -103,16 +104,29 @@ function resolveMode(left, right, mode) {
103
104
  if (left != null) return REVEAL_MODE.left;
104
105
  return REVEAL_MODE.right;
105
106
  }
107
+ function prefersReducedMotion() {
108
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
109
+ }
106
110
  function resolveAnimationConfig(animated, defaultPreset, customConfig) {
107
111
  let config;
112
+ let explicit = false;
108
113
  if (animated === false) {
109
114
  config = ANIMATION_PRESETS[ANIMATION_PRESET.none];
110
115
  } else if (animated === true || animated === void 0) {
111
- config = customConfig || ANIMATION_PRESETS[defaultPreset];
116
+ if (customConfig) {
117
+ config = customConfig;
118
+ explicit = true;
119
+ } else {
120
+ config = ANIMATION_PRESETS[defaultPreset];
121
+ }
112
122
  } else if (typeof animated === "string") {
113
123
  config = ANIMATION_PRESETS[animated];
114
124
  } else {
115
125
  config = animated;
126
+ explicit = true;
127
+ }
128
+ if (!explicit && prefersReducedMotion()) {
129
+ return { ...config, duration: 0 };
116
130
  }
117
131
  return {
118
132
  ...config,
@@ -124,9 +138,22 @@ function resolveAnimationConfig(animated, defaultPreset, customConfig) {
124
138
  }
125
139
  var SCROLL_REVEAL_DEBOUNCE_MS = 64;
126
140
  var restEpsilon = 2;
141
+ var PEEK_FRACTION = 0.35;
142
+ var PEEK_OUT_ANIMATION = {
143
+ duration: 120,
144
+ easing: "ease-out"
145
+ };
146
+ var PEEK_RETURN_ANIMATION = {
147
+ duration: 180,
148
+ easing: "ease-out"
149
+ };
150
+ var PEEK_SETTLE_BUFFER_MS = 120;
127
151
  var rootScrollStyle = {
128
152
  display: "grid",
129
153
  overflowX: "auto",
154
+ // `overflow-x: auto` would otherwise force `overflow-y` to compute to
155
+ // `auto` — pin it so rows only ever scroll on one axis.
156
+ overflowY: "hidden",
130
157
  scrollSnapType: "x mandatory",
131
158
  scrollbarWidth: "none",
132
159
  WebkitOverflowScrolling: "touch",
@@ -154,6 +181,7 @@ function RevealRowInner({
154
181
  handlePosition: handlePositionProp,
155
182
  handleTitle = "Drag horizontally to show actions",
156
183
  handleAriaLabel = "Drag horizontally to show actions",
184
+ peekOnHandleTap = true,
157
185
  onRevealChange,
158
186
  onScroll: onScrollProp,
159
187
  resetWhenDisabled = true,
@@ -168,26 +196,47 @@ function RevealRowInner({
168
196
  const mode = resolveMode(left, right, modeProp);
169
197
  const hasL = mode === REVEAL_MODE.left || mode === REVEAL_MODE.both;
170
198
  const hasR = mode === REVEAL_MODE.right || mode === REVEAL_MODE.both;
171
- const wL = hasL ? wLIn ?? 88 : 0;
172
- const wR = hasR ? wRIn ?? 88 : 0;
173
199
  const handlePosition = handlePositionProp ?? (mode === REVEAL_MODE.left ? REVEAL_HANDLE_POSITION.start : REVEAL_HANDLE_POSITION.end);
174
200
  const containerRef = useRef(null);
175
201
  const leftRef = useRef(null);
176
202
  const rightRef = useRef(null);
203
+ const handleStripRef = useRef(null);
204
+ const peekTimerRef = useRef(null);
177
205
  const lastEmitted = useRef(null);
178
206
  const swipedRef = useRef(false);
207
+ const focusRevealedRef = useRef(false);
208
+ const [settledPosition, setSettledPosition] = useState(
209
+ REVEAL_POSITION.center
210
+ );
211
+ const getWL = useCallback(
212
+ () => hasL ? wLIn ?? (leftRef.current?.offsetWidth || 88) : 0,
213
+ [hasL, wLIn]
214
+ );
215
+ const getWR = useCallback(
216
+ () => hasR ? wRIn ?? (rightRef.current?.offsetWidth || 88) : 0,
217
+ [hasR, wRIn]
218
+ );
179
219
  const debounceTimer = useRef(null);
180
220
  const hasAppliedInitialScroll = useRef(false);
181
221
  const isAnimatingRef = useRef(false);
182
222
  const rafIdRef = useRef(null);
183
223
  const settleTimerRef = useRef(null);
224
+ const finalizeTimerRef = useRef(null);
225
+ const originalSnapRef = useRef("");
184
226
  const readPosition = useCallback(() => {
185
227
  const el = containerRef.current;
186
228
  if (!el) return REVEAL_POSITION.center;
187
- return getRevealFromScroll(el.scrollLeft, getMaxScroll(el), wL, wR, mode);
188
- }, [wL, wR, mode]);
229
+ return getRevealFromScroll(
230
+ el.scrollLeft,
231
+ getMaxScroll(el),
232
+ getWL(),
233
+ getWR(),
234
+ mode
235
+ );
236
+ }, [getWL, getWR, mode]);
189
237
  const emitReveal = useCallback(
190
238
  (position) => {
239
+ setSettledPosition(position);
191
240
  if (lastEmitted.current === position) return;
192
241
  lastEmitted.current = position;
193
242
  onRevealChange?.(position);
@@ -198,9 +247,9 @@ function RevealRowInner({
198
247
  const el = containerRef.current;
199
248
  if (!el) return;
200
249
  const m = getMaxScroll(el);
201
- const closed = getScrollClosed(wL, wR, m, mode);
250
+ const closed = getScrollClosed(getWL(), getWR(), m, mode);
202
251
  el.scrollLeft = closed;
203
- }, [wL, wR, mode]);
252
+ }, [getWL, getWR, mode]);
204
253
  const scrollToPosition = useCallback(
205
254
  (targetScrollLeft, animationOptions) => {
206
255
  const el = containerRef.current;
@@ -218,9 +267,29 @@ function RevealRowInner({
218
267
  clearTimeout(settleTimerRef.current);
219
268
  settleTimerRef.current = null;
220
269
  }
270
+ if (finalizeTimerRef.current !== null) {
271
+ clearTimeout(finalizeTimerRef.current);
272
+ finalizeTimerRef.current = null;
273
+ }
274
+ if (!isAnimatingRef.current) {
275
+ originalSnapRef.current = el.style.scrollSnapType;
276
+ }
221
277
  isAnimatingRef.current = true;
222
- const originalScrollSnapType = el.style.scrollSnapType;
223
278
  el.style.scrollSnapType = "none";
279
+ const finish = () => {
280
+ el.scrollLeft = targetScrollLeft;
281
+ el.style.scrollSnapType = originalSnapRef.current || "x mandatory";
282
+ isAnimatingRef.current = false;
283
+ if (rafIdRef.current !== null) {
284
+ cancelAnimationFrame(rafIdRef.current);
285
+ rafIdRef.current = null;
286
+ }
287
+ if (finalizeTimerRef.current !== null) {
288
+ clearTimeout(finalizeTimerRef.current);
289
+ finalizeTimerRef.current = null;
290
+ }
291
+ };
292
+ finalizeTimerRef.current = setTimeout(finish, duration + 100);
224
293
  const startScrollLeft = el.scrollLeft;
225
294
  const distance = targetScrollLeft - startScrollLeft;
226
295
  const startTime = performance.now();
@@ -245,86 +314,156 @@ function RevealRowInner({
245
314
  if (progress < 1) {
246
315
  rafIdRef.current = requestAnimationFrame(animate);
247
316
  } else {
248
- el.scrollLeft = targetScrollLeft;
249
- el.style.scrollSnapType = originalScrollSnapType || "x mandatory";
250
- isAnimatingRef.current = false;
251
317
  rafIdRef.current = null;
318
+ finish();
252
319
  }
253
320
  };
254
321
  rafIdRef.current = requestAnimationFrame(animate);
255
322
  },
256
323
  []
257
324
  );
258
- useImperativeHandle(
259
- forwardedRef,
260
- () => ({
261
- close: (animated) => {
262
- const el = containerRef.current;
263
- if (!el) return;
264
- const m = getMaxScroll(el);
265
- const closed = getScrollClosed(wL, wR, m, mode);
266
- const animConfig = resolveAnimationConfig(
267
- animated,
268
- animationPreset,
269
- animationConfig
270
- );
271
- scrollToPosition(closed, animConfig);
272
- const delay = animConfig.duration > 0 ? animConfig.duration : 0;
273
- if (settleTimerRef.current !== null) {
274
- clearTimeout(settleTimerRef.current);
275
- }
276
- settleTimerRef.current = setTimeout(() => {
277
- const p = readPosition();
278
- if (lastEmitted.current !== p) {
279
- lastEmitted.current = p;
280
- onRevealChange?.(p);
281
- }
282
- settleTimerRef.current = null;
283
- }, delay);
284
- },
285
- reveal: (position, animated) => {
286
- const el = containerRef.current;
287
- if (!el) return;
288
- const max = getMaxScroll(el);
289
- let targetScroll;
290
- if (position === REVEAL_POSITION.center) {
291
- targetScroll = getScrollClosed(wL, wR, max, mode);
292
- } else if (position === REVEAL_POSITION.left && (mode === REVEAL_MODE.left || mode === REVEAL_MODE.both)) {
293
- targetScroll = 0;
294
- } else if (position === REVEAL_POSITION.right && (mode === REVEAL_MODE.right || mode === REVEAL_MODE.both)) {
295
- targetScroll = max;
296
- } else {
297
- return;
298
- }
299
- const animConfig = resolveAnimationConfig(
300
- animated,
301
- animationPreset,
302
- animationConfig
303
- );
304
- scrollToPosition(targetScroll, animConfig);
305
- const delay = animConfig.duration > 0 ? animConfig.duration : 0;
306
- if (settleTimerRef.current !== null) {
307
- clearTimeout(settleTimerRef.current);
308
- }
309
- settleTimerRef.current = setTimeout(() => {
310
- const p = getRevealFromScroll(targetScroll, max, wL, wR, mode);
311
- lastEmitted.current = p;
312
- onRevealChange?.(p);
313
- settleTimerRef.current = null;
314
- }, delay);
325
+ const peekActions = useCallback(() => {
326
+ const el = containerRef.current;
327
+ if (!el || disabled) return false;
328
+ if (isAnimatingRef.current || peekTimerRef.current !== null) return false;
329
+ const defaultConfig = resolveAnimationConfig(
330
+ void 0,
331
+ animationPreset,
332
+ animationConfig
333
+ );
334
+ if (defaultConfig.duration === 0) return false;
335
+ const m = getMaxScroll(el);
336
+ const wL = getWL();
337
+ const wR = getWR();
338
+ const closed = getScrollClosed(wL, wR, m, mode);
339
+ if (Math.abs(el.scrollLeft - closed) > restEpsilon) return false;
340
+ const peekRight = handlePosition === REVEAL_HANDLE_POSITION.end ? hasR : !hasL;
341
+ const target = peekRight ? Math.min(m, closed + wR * PEEK_FRACTION) : Math.max(0, closed - wL * PEEK_FRACTION);
342
+ if (target === closed) return false;
343
+ scrollToPosition(target, PEEK_OUT_ANIMATION);
344
+ peekTimerRef.current = setTimeout(() => {
345
+ scrollToPosition(closed, PEEK_RETURN_ANIMATION);
346
+ peekTimerRef.current = setTimeout(() => {
347
+ peekTimerRef.current = null;
348
+ swipedRef.current = false;
349
+ }, PEEK_RETURN_ANIMATION.duration + PEEK_SETTLE_BUFFER_MS);
350
+ }, PEEK_OUT_ANIMATION.duration);
351
+ return true;
352
+ }, [
353
+ animationConfig,
354
+ animationPreset,
355
+ disabled,
356
+ getWL,
357
+ getWR,
358
+ handlePosition,
359
+ hasL,
360
+ hasR,
361
+ mode,
362
+ scrollToPosition
363
+ ]);
364
+ const closeRow = useCallback(
365
+ (animated) => {
366
+ const el = containerRef.current;
367
+ if (!el) return;
368
+ const m = getMaxScroll(el);
369
+ const closed = getScrollClosed(getWL(), getWR(), m, mode);
370
+ const animConfig = resolveAnimationConfig(
371
+ animated,
372
+ animationPreset,
373
+ animationConfig
374
+ );
375
+ scrollToPosition(closed, animConfig);
376
+ const delay = animConfig.duration > 0 ? animConfig.duration : 0;
377
+ if (settleTimerRef.current !== null) {
378
+ clearTimeout(settleTimerRef.current);
315
379
  }
316
- }),
380
+ settleTimerRef.current = setTimeout(() => {
381
+ emitReveal(readPosition());
382
+ settleTimerRef.current = null;
383
+ }, delay);
384
+ },
317
385
  [
386
+ animationConfig,
387
+ animationPreset,
388
+ emitReveal,
389
+ getWL,
390
+ getWR,
318
391
  mode,
319
- onRevealChange,
320
392
  readPosition,
321
- scrollToPosition,
322
- wL,
323
- wR,
393
+ scrollToPosition
394
+ ]
395
+ );
396
+ const revealRow = useCallback(
397
+ (position, animated) => {
398
+ const el = containerRef.current;
399
+ if (!el) return;
400
+ const max = getMaxScroll(el);
401
+ const wL = getWL();
402
+ const wR = getWR();
403
+ let targetScroll;
404
+ if (position === REVEAL_POSITION.center) {
405
+ targetScroll = getScrollClosed(wL, wR, max, mode);
406
+ } else if (position === REVEAL_POSITION.left && (mode === REVEAL_MODE.left || mode === REVEAL_MODE.both)) {
407
+ targetScroll = 0;
408
+ } else if (position === REVEAL_POSITION.right && (mode === REVEAL_MODE.right || mode === REVEAL_MODE.both)) {
409
+ targetScroll = max;
410
+ } else {
411
+ return;
412
+ }
413
+ const animConfig = resolveAnimationConfig(
414
+ animated,
415
+ animationPreset,
416
+ animationConfig
417
+ );
418
+ scrollToPosition(targetScroll, animConfig);
419
+ const delay = animConfig.duration > 0 ? animConfig.duration : 0;
420
+ if (settleTimerRef.current !== null) {
421
+ clearTimeout(settleTimerRef.current);
422
+ }
423
+ settleTimerRef.current = setTimeout(() => {
424
+ emitReveal(getRevealFromScroll(targetScroll, max, wL, wR, mode));
425
+ settleTimerRef.current = null;
426
+ }, delay);
427
+ },
428
+ [
429
+ animationConfig,
324
430
  animationPreset,
325
- animationConfig
431
+ emitReveal,
432
+ getWL,
433
+ getWR,
434
+ mode,
435
+ scrollToPosition
326
436
  ]
327
437
  );
438
+ useImperativeHandle(
439
+ forwardedRef,
440
+ () => ({ close: closeRow, reveal: revealRow }),
441
+ [closeRow, revealRow]
442
+ );
443
+ const handleRootFocus = useCallback(
444
+ (e) => {
445
+ const t = e.target;
446
+ if (!t) return;
447
+ if (leftRef.current?.contains(t)) {
448
+ focusRevealedRef.current = true;
449
+ revealRow(REVEAL_POSITION.left);
450
+ } else if (rightRef.current?.contains(t)) {
451
+ focusRevealedRef.current = true;
452
+ revealRow(REVEAL_POSITION.right);
453
+ }
454
+ },
455
+ [revealRow]
456
+ );
457
+ const handleRootBlur = useCallback(
458
+ (e) => {
459
+ if (!focusRevealedRef.current) return;
460
+ const next = e.relatedTarget;
461
+ if (next && containerRef.current?.contains(next)) return;
462
+ focusRevealedRef.current = false;
463
+ closeRow();
464
+ },
465
+ [closeRow]
466
+ );
328
467
  useLayoutEffect(() => {
329
468
  if (mode === REVEAL_MODE.right) {
330
469
  return;
@@ -332,10 +471,10 @@ function RevealRowInner({
332
471
  const el = containerRef.current;
333
472
  if (!el) return;
334
473
  if (!hasAppliedInitialScroll.current) {
335
- el.scrollLeft = wL;
474
+ el.scrollLeft = getWL();
336
475
  hasAppliedInitialScroll.current = true;
337
476
  }
338
- }, [mode, wL]);
477
+ }, [mode, getWL]);
339
478
  useEffect(() => {
340
479
  if (disabled && resetWhenDisabled) {
341
480
  hasAppliedInitialScroll.current = true;
@@ -361,6 +500,12 @@ function RevealRowInner({
361
500
  if (settleTimerRef.current !== null) {
362
501
  clearTimeout(settleTimerRef.current);
363
502
  }
503
+ if (finalizeTimerRef.current !== null) {
504
+ clearTimeout(finalizeTimerRef.current);
505
+ }
506
+ if (peekTimerRef.current !== null) {
507
+ clearTimeout(peekTimerRef.current);
508
+ }
364
509
  },
365
510
  []
366
511
  );
@@ -370,6 +515,8 @@ function RevealRowInner({
370
515
  const el = containerRef.current;
371
516
  if (!el) return;
372
517
  const m = getMaxScroll(el);
518
+ const wL = getWL();
519
+ const wR = getWR();
373
520
  const pos = getRevealFromScroll(el.scrollLeft, m, wL, wR, mode);
374
521
  const closed = getScrollClosed(wL, wR, m, mode);
375
522
  if (Math.abs(el.scrollLeft - closed) > restEpsilon) {
@@ -383,22 +530,32 @@ function RevealRowInner({
383
530
  emitReveal(pos);
384
531
  }, SCROLL_REVEAL_DEBOUNCE_MS);
385
532
  },
386
- [emitReveal, mode, onScrollProp, wL, wR]
533
+ [emitReveal, getWL, getWR, mode, onScrollProp]
387
534
  );
388
- const onClickCapture = useCallback((e) => {
389
- const t = e.target;
390
- if (t) {
391
- if (leftRef.current?.contains(t) || rightRef.current?.contains(t)) {
535
+ const onClickCapture = useCallback(
536
+ (e) => {
537
+ const t = e.target;
538
+ if (t) {
539
+ if (leftRef.current?.contains(t) || rightRef.current?.contains(t)) {
540
+ return;
541
+ }
542
+ }
543
+ if (swipedRef.current) {
544
+ e.preventDefault();
545
+ e.stopPropagation();
546
+ swipedRef.current = false;
392
547
  return;
393
548
  }
394
- }
395
- if (swipedRef.current) {
396
- e.preventDefault();
397
- e.stopPropagation();
398
- swipedRef.current = false;
399
- }
400
- }, []);
401
- const gridTemplateColumns = mode === REVEAL_MODE.right ? `100% ${wR}px` : mode === REVEAL_MODE.left ? `${wL}px 100%` : `${wL}px 100% ${wR}px`;
549
+ if (peekOnHandleTap && t && handleStripRef.current?.contains(t) && peekActions()) {
550
+ e.preventDefault();
551
+ e.stopPropagation();
552
+ }
553
+ },
554
+ [peekActions, peekOnHandleTap]
555
+ );
556
+ const trackL = wLIn != null ? `${wLIn}px` : "max-content";
557
+ const trackR = wRIn != null ? `${wRIn}px` : "max-content";
558
+ const gridTemplateColumns = mode === REVEAL_MODE.right ? `100% ${trackR}` : mode === REVEAL_MODE.left ? `${trackL} 100%` : `${trackL} 100% ${trackR}`;
402
559
  const handleContent = showHandle ? handleOverride !== void 0 ? handleOverride : /* @__PURE__ */ jsxs2(Fragment, { children: [
403
560
  /* @__PURE__ */ jsx2(
404
561
  "span",
@@ -422,6 +579,7 @@ function RevealRowInner({
422
579
  const handleStrip = showHandle ? /* @__PURE__ */ jsx2(
423
580
  "div",
424
581
  {
582
+ ref: handleStripRef,
425
583
  role: "presentation",
426
584
  className: classNames.handleContainer,
427
585
  "data-reveal-row-handle": true,
@@ -471,9 +629,12 @@ function RevealRowInner({
471
629
  {
472
630
  ref: containerRef,
473
631
  "data-reveal-mode": mode,
632
+ "data-reveal-position": settledPosition,
474
633
  className: cx(classNames.root, className),
475
634
  onScroll: handleScroll,
476
635
  onClickCapture,
636
+ onFocus: handleRootFocus,
637
+ onBlur: handleRootBlur,
477
638
  style: {
478
639
  ...disabled ? rootScrollStyleDisabled : rootScrollStyle,
479
640
  ...style,
@@ -486,7 +647,10 @@ function RevealRowInner({
486
647
  ref: leftRef,
487
648
  className: classNames.left,
488
649
  "data-reveal-row-left": true,
489
- style: { ...snapStart, minWidth: 0, width: wL },
650
+ style: {
651
+ ...snapStart,
652
+ ...wLIn != null ? { minWidth: 0, width: wLIn } : { minWidth: "var(--reveal-row-action-min-width, 88px)" }
653
+ },
490
654
  children: left
491
655
  }
492
656
  ) : null,
@@ -497,7 +661,10 @@ function RevealRowInner({
497
661
  ref: rightRef,
498
662
  className: classNames.right,
499
663
  "data-reveal-row-right": true,
500
- style: { ...snapEnd, minWidth: 0, width: wR },
664
+ style: {
665
+ ...snapEnd,
666
+ ...wRIn != null ? { minWidth: 0, width: wRIn } : { minWidth: "var(--reveal-row-action-min-width, 88px)" }
667
+ },
501
668
  children: right
502
669
  }
503
670
  ) : null