@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.js CHANGED
@@ -130,16 +130,29 @@ function resolveMode(left, right, mode) {
130
130
  if (left != null) return REVEAL_MODE.left;
131
131
  return REVEAL_MODE.right;
132
132
  }
133
+ function prefersReducedMotion() {
134
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
135
+ }
133
136
  function resolveAnimationConfig(animated, defaultPreset, customConfig) {
134
137
  let config;
138
+ let explicit = false;
135
139
  if (animated === false) {
136
140
  config = ANIMATION_PRESETS[ANIMATION_PRESET.none];
137
141
  } else if (animated === true || animated === void 0) {
138
- config = customConfig || ANIMATION_PRESETS[defaultPreset];
142
+ if (customConfig) {
143
+ config = customConfig;
144
+ explicit = true;
145
+ } else {
146
+ config = ANIMATION_PRESETS[defaultPreset];
147
+ }
139
148
  } else if (typeof animated === "string") {
140
149
  config = ANIMATION_PRESETS[animated];
141
150
  } else {
142
151
  config = animated;
152
+ explicit = true;
153
+ }
154
+ if (!explicit && prefersReducedMotion()) {
155
+ return { ...config, duration: 0 };
143
156
  }
144
157
  return {
145
158
  ...config,
@@ -151,9 +164,22 @@ function resolveAnimationConfig(animated, defaultPreset, customConfig) {
151
164
  }
152
165
  var SCROLL_REVEAL_DEBOUNCE_MS = 64;
153
166
  var restEpsilon = 2;
167
+ var PEEK_FRACTION = 0.35;
168
+ var PEEK_OUT_ANIMATION = {
169
+ duration: 120,
170
+ easing: "ease-out"
171
+ };
172
+ var PEEK_RETURN_ANIMATION = {
173
+ duration: 180,
174
+ easing: "ease-out"
175
+ };
176
+ var PEEK_SETTLE_BUFFER_MS = 120;
154
177
  var rootScrollStyle = {
155
178
  display: "grid",
156
179
  overflowX: "auto",
180
+ // `overflow-x: auto` would otherwise force `overflow-y` to compute to
181
+ // `auto` — pin it so rows only ever scroll on one axis.
182
+ overflowY: "hidden",
157
183
  scrollSnapType: "x mandatory",
158
184
  scrollbarWidth: "none",
159
185
  WebkitOverflowScrolling: "touch",
@@ -181,6 +207,7 @@ function RevealRowInner({
181
207
  handlePosition: handlePositionProp,
182
208
  handleTitle = "Drag horizontally to show actions",
183
209
  handleAriaLabel = "Drag horizontally to show actions",
210
+ peekOnHandleTap = true,
184
211
  onRevealChange,
185
212
  onScroll: onScrollProp,
186
213
  resetWhenDisabled = true,
@@ -195,26 +222,47 @@ function RevealRowInner({
195
222
  const mode = resolveMode(left, right, modeProp);
196
223
  const hasL = mode === REVEAL_MODE.left || mode === REVEAL_MODE.both;
197
224
  const hasR = mode === REVEAL_MODE.right || mode === REVEAL_MODE.both;
198
- const wL = hasL ? wLIn ?? 88 : 0;
199
- const wR = hasR ? wRIn ?? 88 : 0;
200
225
  const handlePosition = handlePositionProp ?? (mode === REVEAL_MODE.left ? REVEAL_HANDLE_POSITION.start : REVEAL_HANDLE_POSITION.end);
201
226
  const containerRef = (0, import_react.useRef)(null);
202
227
  const leftRef = (0, import_react.useRef)(null);
203
228
  const rightRef = (0, import_react.useRef)(null);
229
+ const handleStripRef = (0, import_react.useRef)(null);
230
+ const peekTimerRef = (0, import_react.useRef)(null);
204
231
  const lastEmitted = (0, import_react.useRef)(null);
205
232
  const swipedRef = (0, import_react.useRef)(false);
233
+ const focusRevealedRef = (0, import_react.useRef)(false);
234
+ const [settledPosition, setSettledPosition] = (0, import_react.useState)(
235
+ REVEAL_POSITION.center
236
+ );
237
+ const getWL = (0, import_react.useCallback)(
238
+ () => hasL ? wLIn ?? (leftRef.current?.offsetWidth || 88) : 0,
239
+ [hasL, wLIn]
240
+ );
241
+ const getWR = (0, import_react.useCallback)(
242
+ () => hasR ? wRIn ?? (rightRef.current?.offsetWidth || 88) : 0,
243
+ [hasR, wRIn]
244
+ );
206
245
  const debounceTimer = (0, import_react.useRef)(null);
207
246
  const hasAppliedInitialScroll = (0, import_react.useRef)(false);
208
247
  const isAnimatingRef = (0, import_react.useRef)(false);
209
248
  const rafIdRef = (0, import_react.useRef)(null);
210
249
  const settleTimerRef = (0, import_react.useRef)(null);
250
+ const finalizeTimerRef = (0, import_react.useRef)(null);
251
+ const originalSnapRef = (0, import_react.useRef)("");
211
252
  const readPosition = (0, import_react.useCallback)(() => {
212
253
  const el = containerRef.current;
213
254
  if (!el) return REVEAL_POSITION.center;
214
- return getRevealFromScroll(el.scrollLeft, getMaxScroll(el), wL, wR, mode);
215
- }, [wL, wR, mode]);
255
+ return getRevealFromScroll(
256
+ el.scrollLeft,
257
+ getMaxScroll(el),
258
+ getWL(),
259
+ getWR(),
260
+ mode
261
+ );
262
+ }, [getWL, getWR, mode]);
216
263
  const emitReveal = (0, import_react.useCallback)(
217
264
  (position) => {
265
+ setSettledPosition(position);
218
266
  if (lastEmitted.current === position) return;
219
267
  lastEmitted.current = position;
220
268
  onRevealChange?.(position);
@@ -225,9 +273,9 @@ function RevealRowInner({
225
273
  const el = containerRef.current;
226
274
  if (!el) return;
227
275
  const m = getMaxScroll(el);
228
- const closed = getScrollClosed(wL, wR, m, mode);
276
+ const closed = getScrollClosed(getWL(), getWR(), m, mode);
229
277
  el.scrollLeft = closed;
230
- }, [wL, wR, mode]);
278
+ }, [getWL, getWR, mode]);
231
279
  const scrollToPosition = (0, import_react.useCallback)(
232
280
  (targetScrollLeft, animationOptions) => {
233
281
  const el = containerRef.current;
@@ -245,9 +293,29 @@ function RevealRowInner({
245
293
  clearTimeout(settleTimerRef.current);
246
294
  settleTimerRef.current = null;
247
295
  }
296
+ if (finalizeTimerRef.current !== null) {
297
+ clearTimeout(finalizeTimerRef.current);
298
+ finalizeTimerRef.current = null;
299
+ }
300
+ if (!isAnimatingRef.current) {
301
+ originalSnapRef.current = el.style.scrollSnapType;
302
+ }
248
303
  isAnimatingRef.current = true;
249
- const originalScrollSnapType = el.style.scrollSnapType;
250
304
  el.style.scrollSnapType = "none";
305
+ const finish = () => {
306
+ el.scrollLeft = targetScrollLeft;
307
+ el.style.scrollSnapType = originalSnapRef.current || "x mandatory";
308
+ isAnimatingRef.current = false;
309
+ if (rafIdRef.current !== null) {
310
+ cancelAnimationFrame(rafIdRef.current);
311
+ rafIdRef.current = null;
312
+ }
313
+ if (finalizeTimerRef.current !== null) {
314
+ clearTimeout(finalizeTimerRef.current);
315
+ finalizeTimerRef.current = null;
316
+ }
317
+ };
318
+ finalizeTimerRef.current = setTimeout(finish, duration + 100);
251
319
  const startScrollLeft = el.scrollLeft;
252
320
  const distance = targetScrollLeft - startScrollLeft;
253
321
  const startTime = performance.now();
@@ -272,86 +340,156 @@ function RevealRowInner({
272
340
  if (progress < 1) {
273
341
  rafIdRef.current = requestAnimationFrame(animate);
274
342
  } else {
275
- el.scrollLeft = targetScrollLeft;
276
- el.style.scrollSnapType = originalScrollSnapType || "x mandatory";
277
- isAnimatingRef.current = false;
278
343
  rafIdRef.current = null;
344
+ finish();
279
345
  }
280
346
  };
281
347
  rafIdRef.current = requestAnimationFrame(animate);
282
348
  },
283
349
  []
284
350
  );
285
- (0, import_react.useImperativeHandle)(
286
- forwardedRef,
287
- () => ({
288
- close: (animated) => {
289
- const el = containerRef.current;
290
- if (!el) return;
291
- const m = getMaxScroll(el);
292
- const closed = getScrollClosed(wL, wR, m, mode);
293
- const animConfig = resolveAnimationConfig(
294
- animated,
295
- animationPreset,
296
- animationConfig
297
- );
298
- scrollToPosition(closed, animConfig);
299
- const delay = animConfig.duration > 0 ? animConfig.duration : 0;
300
- if (settleTimerRef.current !== null) {
301
- clearTimeout(settleTimerRef.current);
302
- }
303
- settleTimerRef.current = setTimeout(() => {
304
- const p = readPosition();
305
- if (lastEmitted.current !== p) {
306
- lastEmitted.current = p;
307
- onRevealChange?.(p);
308
- }
309
- settleTimerRef.current = null;
310
- }, delay);
311
- },
312
- reveal: (position, animated) => {
313
- const el = containerRef.current;
314
- if (!el) return;
315
- const max = getMaxScroll(el);
316
- let targetScroll;
317
- if (position === REVEAL_POSITION.center) {
318
- targetScroll = getScrollClosed(wL, wR, max, mode);
319
- } else if (position === REVEAL_POSITION.left && (mode === REVEAL_MODE.left || mode === REVEAL_MODE.both)) {
320
- targetScroll = 0;
321
- } else if (position === REVEAL_POSITION.right && (mode === REVEAL_MODE.right || mode === REVEAL_MODE.both)) {
322
- targetScroll = max;
323
- } else {
324
- return;
325
- }
326
- const animConfig = resolveAnimationConfig(
327
- animated,
328
- animationPreset,
329
- animationConfig
330
- );
331
- scrollToPosition(targetScroll, animConfig);
332
- const delay = animConfig.duration > 0 ? animConfig.duration : 0;
333
- if (settleTimerRef.current !== null) {
334
- clearTimeout(settleTimerRef.current);
335
- }
336
- settleTimerRef.current = setTimeout(() => {
337
- const p = getRevealFromScroll(targetScroll, max, wL, wR, mode);
338
- lastEmitted.current = p;
339
- onRevealChange?.(p);
340
- settleTimerRef.current = null;
341
- }, delay);
351
+ const peekActions = (0, import_react.useCallback)(() => {
352
+ const el = containerRef.current;
353
+ if (!el || disabled) return false;
354
+ if (isAnimatingRef.current || peekTimerRef.current !== null) return false;
355
+ const defaultConfig = resolveAnimationConfig(
356
+ void 0,
357
+ animationPreset,
358
+ animationConfig
359
+ );
360
+ if (defaultConfig.duration === 0) return false;
361
+ const m = getMaxScroll(el);
362
+ const wL = getWL();
363
+ const wR = getWR();
364
+ const closed = getScrollClosed(wL, wR, m, mode);
365
+ if (Math.abs(el.scrollLeft - closed) > restEpsilon) return false;
366
+ const peekRight = handlePosition === REVEAL_HANDLE_POSITION.end ? hasR : !hasL;
367
+ const target = peekRight ? Math.min(m, closed + wR * PEEK_FRACTION) : Math.max(0, closed - wL * PEEK_FRACTION);
368
+ if (target === closed) return false;
369
+ scrollToPosition(target, PEEK_OUT_ANIMATION);
370
+ peekTimerRef.current = setTimeout(() => {
371
+ scrollToPosition(closed, PEEK_RETURN_ANIMATION);
372
+ peekTimerRef.current = setTimeout(() => {
373
+ peekTimerRef.current = null;
374
+ swipedRef.current = false;
375
+ }, PEEK_RETURN_ANIMATION.duration + PEEK_SETTLE_BUFFER_MS);
376
+ }, PEEK_OUT_ANIMATION.duration);
377
+ return true;
378
+ }, [
379
+ animationConfig,
380
+ animationPreset,
381
+ disabled,
382
+ getWL,
383
+ getWR,
384
+ handlePosition,
385
+ hasL,
386
+ hasR,
387
+ mode,
388
+ scrollToPosition
389
+ ]);
390
+ const closeRow = (0, import_react.useCallback)(
391
+ (animated) => {
392
+ const el = containerRef.current;
393
+ if (!el) return;
394
+ const m = getMaxScroll(el);
395
+ const closed = getScrollClosed(getWL(), getWR(), m, mode);
396
+ const animConfig = resolveAnimationConfig(
397
+ animated,
398
+ animationPreset,
399
+ animationConfig
400
+ );
401
+ scrollToPosition(closed, animConfig);
402
+ const delay = animConfig.duration > 0 ? animConfig.duration : 0;
403
+ if (settleTimerRef.current !== null) {
404
+ clearTimeout(settleTimerRef.current);
342
405
  }
343
- }),
406
+ settleTimerRef.current = setTimeout(() => {
407
+ emitReveal(readPosition());
408
+ settleTimerRef.current = null;
409
+ }, delay);
410
+ },
344
411
  [
412
+ animationConfig,
413
+ animationPreset,
414
+ emitReveal,
415
+ getWL,
416
+ getWR,
345
417
  mode,
346
- onRevealChange,
347
418
  readPosition,
348
- scrollToPosition,
349
- wL,
350
- wR,
419
+ scrollToPosition
420
+ ]
421
+ );
422
+ const revealRow = (0, import_react.useCallback)(
423
+ (position, animated) => {
424
+ const el = containerRef.current;
425
+ if (!el) return;
426
+ const max = getMaxScroll(el);
427
+ const wL = getWL();
428
+ const wR = getWR();
429
+ let targetScroll;
430
+ if (position === REVEAL_POSITION.center) {
431
+ targetScroll = getScrollClosed(wL, wR, max, mode);
432
+ } else if (position === REVEAL_POSITION.left && (mode === REVEAL_MODE.left || mode === REVEAL_MODE.both)) {
433
+ targetScroll = 0;
434
+ } else if (position === REVEAL_POSITION.right && (mode === REVEAL_MODE.right || mode === REVEAL_MODE.both)) {
435
+ targetScroll = max;
436
+ } else {
437
+ return;
438
+ }
439
+ const animConfig = resolveAnimationConfig(
440
+ animated,
441
+ animationPreset,
442
+ animationConfig
443
+ );
444
+ scrollToPosition(targetScroll, animConfig);
445
+ const delay = animConfig.duration > 0 ? animConfig.duration : 0;
446
+ if (settleTimerRef.current !== null) {
447
+ clearTimeout(settleTimerRef.current);
448
+ }
449
+ settleTimerRef.current = setTimeout(() => {
450
+ emitReveal(getRevealFromScroll(targetScroll, max, wL, wR, mode));
451
+ settleTimerRef.current = null;
452
+ }, delay);
453
+ },
454
+ [
455
+ animationConfig,
351
456
  animationPreset,
352
- animationConfig
457
+ emitReveal,
458
+ getWL,
459
+ getWR,
460
+ mode,
461
+ scrollToPosition
353
462
  ]
354
463
  );
464
+ (0, import_react.useImperativeHandle)(
465
+ forwardedRef,
466
+ () => ({ close: closeRow, reveal: revealRow }),
467
+ [closeRow, revealRow]
468
+ );
469
+ const handleRootFocus = (0, import_react.useCallback)(
470
+ (e) => {
471
+ const t = e.target;
472
+ if (!t) return;
473
+ if (leftRef.current?.contains(t)) {
474
+ focusRevealedRef.current = true;
475
+ revealRow(REVEAL_POSITION.left);
476
+ } else if (rightRef.current?.contains(t)) {
477
+ focusRevealedRef.current = true;
478
+ revealRow(REVEAL_POSITION.right);
479
+ }
480
+ },
481
+ [revealRow]
482
+ );
483
+ const handleRootBlur = (0, import_react.useCallback)(
484
+ (e) => {
485
+ if (!focusRevealedRef.current) return;
486
+ const next = e.relatedTarget;
487
+ if (next && containerRef.current?.contains(next)) return;
488
+ focusRevealedRef.current = false;
489
+ closeRow();
490
+ },
491
+ [closeRow]
492
+ );
355
493
  (0, import_react.useLayoutEffect)(() => {
356
494
  if (mode === REVEAL_MODE.right) {
357
495
  return;
@@ -359,10 +497,10 @@ function RevealRowInner({
359
497
  const el = containerRef.current;
360
498
  if (!el) return;
361
499
  if (!hasAppliedInitialScroll.current) {
362
- el.scrollLeft = wL;
500
+ el.scrollLeft = getWL();
363
501
  hasAppliedInitialScroll.current = true;
364
502
  }
365
- }, [mode, wL]);
503
+ }, [mode, getWL]);
366
504
  (0, import_react.useEffect)(() => {
367
505
  if (disabled && resetWhenDisabled) {
368
506
  hasAppliedInitialScroll.current = true;
@@ -388,6 +526,12 @@ function RevealRowInner({
388
526
  if (settleTimerRef.current !== null) {
389
527
  clearTimeout(settleTimerRef.current);
390
528
  }
529
+ if (finalizeTimerRef.current !== null) {
530
+ clearTimeout(finalizeTimerRef.current);
531
+ }
532
+ if (peekTimerRef.current !== null) {
533
+ clearTimeout(peekTimerRef.current);
534
+ }
391
535
  },
392
536
  []
393
537
  );
@@ -397,6 +541,8 @@ function RevealRowInner({
397
541
  const el = containerRef.current;
398
542
  if (!el) return;
399
543
  const m = getMaxScroll(el);
544
+ const wL = getWL();
545
+ const wR = getWR();
400
546
  const pos = getRevealFromScroll(el.scrollLeft, m, wL, wR, mode);
401
547
  const closed = getScrollClosed(wL, wR, m, mode);
402
548
  if (Math.abs(el.scrollLeft - closed) > restEpsilon) {
@@ -410,22 +556,32 @@ function RevealRowInner({
410
556
  emitReveal(pos);
411
557
  }, SCROLL_REVEAL_DEBOUNCE_MS);
412
558
  },
413
- [emitReveal, mode, onScrollProp, wL, wR]
559
+ [emitReveal, getWL, getWR, mode, onScrollProp]
414
560
  );
415
- const onClickCapture = (0, import_react.useCallback)((e) => {
416
- const t = e.target;
417
- if (t) {
418
- if (leftRef.current?.contains(t) || rightRef.current?.contains(t)) {
561
+ const onClickCapture = (0, import_react.useCallback)(
562
+ (e) => {
563
+ const t = e.target;
564
+ if (t) {
565
+ if (leftRef.current?.contains(t) || rightRef.current?.contains(t)) {
566
+ return;
567
+ }
568
+ }
569
+ if (swipedRef.current) {
570
+ e.preventDefault();
571
+ e.stopPropagation();
572
+ swipedRef.current = false;
419
573
  return;
420
574
  }
421
- }
422
- if (swipedRef.current) {
423
- e.preventDefault();
424
- e.stopPropagation();
425
- swipedRef.current = false;
426
- }
427
- }, []);
428
- const gridTemplateColumns = mode === REVEAL_MODE.right ? `100% ${wR}px` : mode === REVEAL_MODE.left ? `${wL}px 100%` : `${wL}px 100% ${wR}px`;
575
+ if (peekOnHandleTap && t && handleStripRef.current?.contains(t) && peekActions()) {
576
+ e.preventDefault();
577
+ e.stopPropagation();
578
+ }
579
+ },
580
+ [peekActions, peekOnHandleTap]
581
+ );
582
+ const trackL = wLIn != null ? `${wLIn}px` : "max-content";
583
+ const trackR = wRIn != null ? `${wRIn}px` : "max-content";
584
+ const gridTemplateColumns = mode === REVEAL_MODE.right ? `100% ${trackR}` : mode === REVEAL_MODE.left ? `${trackL} 100%` : `${trackL} 100% ${trackR}`;
429
585
  const handleContent = showHandle ? handleOverride !== void 0 ? handleOverride : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
430
586
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
431
587
  "span",
@@ -449,6 +605,7 @@ function RevealRowInner({
449
605
  const handleStrip = showHandle ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
450
606
  "div",
451
607
  {
608
+ ref: handleStripRef,
452
609
  role: "presentation",
453
610
  className: classNames.handleContainer,
454
611
  "data-reveal-row-handle": true,
@@ -498,9 +655,12 @@ function RevealRowInner({
498
655
  {
499
656
  ref: containerRef,
500
657
  "data-reveal-mode": mode,
658
+ "data-reveal-position": settledPosition,
501
659
  className: cx(classNames.root, className),
502
660
  onScroll: handleScroll,
503
661
  onClickCapture,
662
+ onFocus: handleRootFocus,
663
+ onBlur: handleRootBlur,
504
664
  style: {
505
665
  ...disabled ? rootScrollStyleDisabled : rootScrollStyle,
506
666
  ...style,
@@ -513,7 +673,10 @@ function RevealRowInner({
513
673
  ref: leftRef,
514
674
  className: classNames.left,
515
675
  "data-reveal-row-left": true,
516
- style: { ...snapStart, minWidth: 0, width: wL },
676
+ style: {
677
+ ...snapStart,
678
+ ...wLIn != null ? { minWidth: 0, width: wLIn } : { minWidth: "var(--reveal-row-action-min-width, 88px)" }
679
+ },
517
680
  children: left
518
681
  }
519
682
  ) : null,
@@ -524,7 +687,10 @@ function RevealRowInner({
524
687
  ref: rightRef,
525
688
  className: classNames.right,
526
689
  "data-reveal-row-right": true,
527
- style: { ...snapEnd, minWidth: 0, width: wR },
690
+ style: {
691
+ ...snapEnd,
692
+ ...wRIn != null ? { minWidth: 0, width: wRIn } : { minWidth: "var(--reveal-row-action-min-width, 88px)" }
693
+ },
528
694
  children: right
529
695
  }
530
696
  ) : null