@present-day/reveal-row 0.1.6 → 0.2.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 +88 -9
- package/dist/index.d.mts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +182 -80
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +184 -81
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
|
-
#
|
|
1
|
+
# RevealRow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Buttery swipe-to-reveal actions for React lists — powered by native scroll physics, not JavaScript animation.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@present-day/reveal-row)
|
|
6
|
+
[](https://github.com/present-day/reveal-row/actions/workflows/ci.yml)
|
|
7
|
+
[](https://bundlephobia.com/package/@present-day/reveal-row)
|
|
8
|
+
[](https://www.npmjs.com/package/@present-day/reveal-row)
|
|
9
|
+
[](./LICENSE)
|
|
6
10
|
|
|
11
|
+
The swipe-to-delete pattern everyone knows from iOS Mail — as a headless React component. Swipe (or drag, or scroll) a row horizontally to reveal action buttons on the **right**, the **left**, or **both** sides, with three crisp snap positions: left · center · right.
|
|
12
|
+
|
|
13
|
+
**[▶ Try the live playground](https://present-day.github.io/reveal-row/)**
|
|
14
|
+
|
|
15
|
+
<img width="295" height="640" alt="RevealRow demo — swiping a list row to reveal actions" src="https://github.com/user-attachments/assets/932021d4-5224-479f-9df9-c7045bf12afb" />
|
|
16
|
+
|
|
17
|
+
## Why RevealRow?
|
|
18
|
+
|
|
19
|
+
- 🍦 **Native scroll physics** — momentum, rubber-banding, and snap come from the browser's own scroll engine (CSS scroll-snap), not a JS animation loop. It feels right because it *is* the real thing.
|
|
20
|
+
- 🎨 **Headless & unstyled** — no bundled CSS. Every sub-element takes your class names, so it drops into Tailwind, CSS Modules, or plain CSS without a fight.
|
|
21
|
+
- 📱 **Touch, trackpad, and mouse** — one component, every input. Works inside vertically scrolling lists without gesture conflicts.
|
|
22
|
+
- 🪶 **Tiny & dependency-free** — just `react` and `react-dom` as peers. Tree-shakeable, `sideEffects: false`, ESM + CJS + types.
|
|
23
|
+
- ♿ **Accessible by default** — focus-driven reveal for keyboard users, `prefers-reduced-motion` support, configurable ARIA labels, and a click-guard so a swipe never fires an accidental row activation.
|
|
24
|
+
- 🎛️ **Fully controllable** — imperative ref API (`reveal('left')`, `close()`), settled-position callbacks, and a `disabled`/`isActive` protocol for coordinating whole lists.
|
|
7
25
|
|
|
8
26
|
## Install
|
|
9
27
|
|
|
10
28
|
```bash
|
|
29
|
+
bun add @present-day/reveal-row
|
|
30
|
+
# or
|
|
11
31
|
npm i @present-day/reveal-row
|
|
12
32
|
```
|
|
13
33
|
|
|
14
34
|
Peer dependencies: `react`, `react-dom` (v18 or v19).
|
|
15
35
|
|
|
16
|
-
##
|
|
36
|
+
## Quick start
|
|
17
37
|
|
|
18
38
|
```tsx
|
|
19
39
|
import { RevealRow } from '@present-day/reveal-row'
|
|
@@ -37,6 +57,34 @@ import { RevealRow } from '@present-day/reveal-row'
|
|
|
37
57
|
</RevealRow>
|
|
38
58
|
```
|
|
39
59
|
|
|
60
|
+
## Multiple actions per side
|
|
61
|
+
|
|
62
|
+
A side slot is a single column that **auto-sizes to its content** (with an 88px floor) — to show several buttons (iOS Mail-style Delete + Pin), lay them out with flex and give each button its own width. No math required:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<RevealRow
|
|
66
|
+
right={
|
|
67
|
+
<div style={{ display: 'flex', height: '100%' }}>
|
|
68
|
+
<button style={{ width: 88 }} onClick={handleDelete}>Delete</button>
|
|
69
|
+
<button style={{ width: 88 }} onClick={handlePin}>Pin</button>
|
|
70
|
+
</div>
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
<MyRowContent />
|
|
74
|
+
</RevealRow>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Passing `actionWidthLeft`/`actionWidthRight` a number still gives you a fixed-width column, exactly as before.
|
|
78
|
+
|
|
79
|
+
**[▶ See it live in the playground](https://present-day.github.io/reveal-row/)** under "Multiple actions".
|
|
80
|
+
|
|
81
|
+
## Accessibility
|
|
82
|
+
|
|
83
|
+
- **Focus-driven reveal** — tabbing into an off-screen action button snaps that side cleanly into view (instead of the browser's un-snapped scroll-into-view). When focus leaves the row, a focus-initiated reveal closes again — so keyboard users never leave rows stuck open. Swipe-opened rows are not affected by focus loss.
|
|
84
|
+
- **Reduced motion** — preset animations become instant under `prefers-reduced-motion: reduce`. An explicit `animationConfig` is treated as an intentional override and left untouched.
|
|
85
|
+
- **State hooks for styling and testing** — the root carries `data-reveal-position="left | center | right"` (settled position) alongside `data-reveal-mode`.
|
|
86
|
+
- **Screen readers** — the drag handle is decorative (`aria-hidden`) with a configurable sr-only description (`handleAriaLabel`).
|
|
87
|
+
|
|
40
88
|
## Modes
|
|
41
89
|
|
|
42
90
|
| `mode` | Slots used | Resting ("closed") scroll |
|
|
@@ -55,8 +103,8 @@ Omit `mode` and it's inferred: both slots → `both`, only `left` → `left`, ot
|
|
|
55
103
|
| `left` | `ReactNode` | — | Leading action column |
|
|
56
104
|
| `right` | `ReactNode` | — | Trailing action column |
|
|
57
105
|
| `mode` | `'left' \| 'right' \| 'both'` | inferred | Override mode detection |
|
|
58
|
-
| `actionWidthLeft` | `number` |
|
|
59
|
-
| `actionWidthRight` | `number` |
|
|
106
|
+
| `actionWidthLeft` | `number` | auto (min 88px) | Fixed width (px) of the left column; omit to size to content |
|
|
107
|
+
| `actionWidthRight` | `number` | auto (min 88px) | Fixed width (px) of the right column; omit to size to content |
|
|
60
108
|
| `classNames` | `RevealRowClassNames` | `{}` | Class names for each sub-element |
|
|
61
109
|
| `showHandle` | `boolean` | `true` | Render the default 6-dot drag affordance |
|
|
62
110
|
| `handle` | `ReactNode` | — | Replace the default handle with custom content |
|
|
@@ -87,9 +135,9 @@ ref.current?.reveal('right') // snap to right action
|
|
|
87
135
|
ref.current?.reveal('center') // alias for close
|
|
88
136
|
```
|
|
89
137
|
|
|
90
|
-
## classNames
|
|
138
|
+
## Styling with classNames
|
|
91
139
|
|
|
92
|
-
All sub-elements accept class names
|
|
140
|
+
All sub-elements accept class names, so styling is entirely yours:
|
|
93
141
|
|
|
94
142
|
```tsx
|
|
95
143
|
<RevealRow
|
|
@@ -113,6 +161,37 @@ All sub-elements accept class names for styling:
|
|
|
113
161
|
|
|
114
162
|
**Preventing row activation on swipe** — the component guards against triggering a click after a horizontal drag. If you wrap the row in a command palette item or similar, keep the built-in `onClickCapture` behaviour intact, or replicate it.
|
|
115
163
|
|
|
116
|
-
|
|
164
|
+
**Focus styles** — the row root is a scroll container, and CSS clips anything drawn outside the scrollport, including focus outlines (on both axes). Draw focus rings *inward* on action buttons and row content: `outline-offset: -2px`, or Tailwind's `focus-visible:ring-2 focus-visible:ring-inset`.
|
|
165
|
+
|
|
166
|
+
**Single-axis scrolling** — the root pins `overflow-y: hidden` so rows only ever scroll horizontally; vertical overflow clips instead of scrolling.
|
|
167
|
+
|
|
168
|
+
**Rounded list corners** — don't rely on a parent's `overflow: hidden` + `border-radius` to clip action buttons: scroll containers are composited on their own layers, and browsers can skip ancestor rounded clipping mid-scroll, exposing square button corners. Instead, put the matching radius on the buttons that touch the container's edges — the outermost button of the group, on the first and last rows only. Drive it from a token so the radius stays in one place:
|
|
169
|
+
|
|
170
|
+
```css
|
|
171
|
+
:root { --row-radius: 12px; }
|
|
172
|
+
.list { border-radius: var(--row-radius); }
|
|
173
|
+
.list > :first-child [data-reveal-row-right] button:last-child { border-top-right-radius: var(--row-radius); }
|
|
174
|
+
.list > :last-child [data-reveal-row-right] button:last-child { border-bottom-right-radius: var(--row-radius); }
|
|
175
|
+
.list > :first-child [data-reveal-row-left] button:first-child { border-top-left-radius: var(--row-radius); }
|
|
176
|
+
.list > :last-child [data-reveal-row-left] button:first-child { border-bottom-left-radius: var(--row-radius); }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The playground implements the same idea with an index-aware helper (`edgeCorners` in `playground/src/PlaygroundApp.tsx`).
|
|
180
|
+
|
|
181
|
+
## CSS tokens
|
|
182
|
+
|
|
183
|
+
The component is headless, but its one built-in dimension is themeable via a CSS custom property:
|
|
184
|
+
|
|
185
|
+
| Token | Default | Effect |
|
|
186
|
+
| ----- | ------- | ------ |
|
|
187
|
+
| `--reveal-row-action-min-width` | `88px` | Minimum width of an auto-sized action column (the floor under content-based sizing) |
|
|
188
|
+
|
|
189
|
+
Set it on `:root` or any ancestor: `[data-reveal-mode] { --reveal-row-action-min-width: 72px; }`. Explicit `actionWidthLeft`/`actionWidthRight` props bypass the token. The playground layers its own tokens on top (`--row-radius`, `--action-width`) in `playground/index.html` — retheme everything from one place.
|
|
190
|
+
|
|
191
|
+
## Publishing (maintainers)
|
|
117
192
|
|
|
118
193
|
Run `bun run build` to produce `dist/`. The `exports` field in `package.json` points to `dist/index.{js,mjs,d.ts}`. Push a tag to trigger the GitHub Actions publish workflow (OIDC trusted publishing — no npm token needed in secrets).
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
[MIT](./LICENSE) © [Present Day](https://presentday.io)
|
package/dist/index.d.mts
CHANGED
|
@@ -73,7 +73,15 @@ type RevealRowProps = {
|
|
|
73
73
|
* mode is derived unless overridden.
|
|
74
74
|
*/
|
|
75
75
|
mode?: RevealMode;
|
|
76
|
+
/**
|
|
77
|
+
* Fixed width (px) of the left action column. Omit to size the column to
|
|
78
|
+
* its content with an 88px floor (`minmax(88px, max-content)`).
|
|
79
|
+
*/
|
|
76
80
|
actionWidthLeft?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Fixed width (px) of the right action column. Omit to size the column to
|
|
83
|
+
* its content with an 88px floor (`minmax(88px, max-content)`).
|
|
84
|
+
*/
|
|
77
85
|
actionWidthRight?: number;
|
|
78
86
|
classNames?: RevealRowClassNames;
|
|
79
87
|
/** @default true */
|
package/dist/index.d.ts
CHANGED
|
@@ -73,7 +73,15 @@ type RevealRowProps = {
|
|
|
73
73
|
* mode is derived unless overridden.
|
|
74
74
|
*/
|
|
75
75
|
mode?: RevealMode;
|
|
76
|
+
/**
|
|
77
|
+
* Fixed width (px) of the left action column. Omit to size the column to
|
|
78
|
+
* its content with an 88px floor (`minmax(88px, max-content)`).
|
|
79
|
+
*/
|
|
76
80
|
actionWidthLeft?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Fixed width (px) of the right action column. Omit to size the column to
|
|
83
|
+
* its content with an 88px floor (`minmax(88px, max-content)`).
|
|
84
|
+
*/
|
|
77
85
|
actionWidthRight?: number;
|
|
78
86
|
classNames?: RevealRowClassNames;
|
|
79
87
|
/** @default true */
|
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
|
-
|
|
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,
|
|
@@ -154,6 +167,9 @@ var restEpsilon = 2;
|
|
|
154
167
|
var rootScrollStyle = {
|
|
155
168
|
display: "grid",
|
|
156
169
|
overflowX: "auto",
|
|
170
|
+
// `overflow-x: auto` would otherwise force `overflow-y` to compute to
|
|
171
|
+
// `auto` — pin it so rows only ever scroll on one axis.
|
|
172
|
+
overflowY: "hidden",
|
|
157
173
|
scrollSnapType: "x mandatory",
|
|
158
174
|
scrollbarWidth: "none",
|
|
159
175
|
WebkitOverflowScrolling: "touch",
|
|
@@ -195,26 +211,45 @@ function RevealRowInner({
|
|
|
195
211
|
const mode = resolveMode(left, right, modeProp);
|
|
196
212
|
const hasL = mode === REVEAL_MODE.left || mode === REVEAL_MODE.both;
|
|
197
213
|
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
214
|
const handlePosition = handlePositionProp ?? (mode === REVEAL_MODE.left ? REVEAL_HANDLE_POSITION.start : REVEAL_HANDLE_POSITION.end);
|
|
201
215
|
const containerRef = (0, import_react.useRef)(null);
|
|
202
216
|
const leftRef = (0, import_react.useRef)(null);
|
|
203
217
|
const rightRef = (0, import_react.useRef)(null);
|
|
204
218
|
const lastEmitted = (0, import_react.useRef)(null);
|
|
205
219
|
const swipedRef = (0, import_react.useRef)(false);
|
|
220
|
+
const focusRevealedRef = (0, import_react.useRef)(false);
|
|
221
|
+
const [settledPosition, setSettledPosition] = (0, import_react.useState)(
|
|
222
|
+
REVEAL_POSITION.center
|
|
223
|
+
);
|
|
224
|
+
const getWL = (0, import_react.useCallback)(
|
|
225
|
+
() => hasL ? wLIn ?? (leftRef.current?.offsetWidth || 88) : 0,
|
|
226
|
+
[hasL, wLIn]
|
|
227
|
+
);
|
|
228
|
+
const getWR = (0, import_react.useCallback)(
|
|
229
|
+
() => hasR ? wRIn ?? (rightRef.current?.offsetWidth || 88) : 0,
|
|
230
|
+
[hasR, wRIn]
|
|
231
|
+
);
|
|
206
232
|
const debounceTimer = (0, import_react.useRef)(null);
|
|
207
233
|
const hasAppliedInitialScroll = (0, import_react.useRef)(false);
|
|
208
234
|
const isAnimatingRef = (0, import_react.useRef)(false);
|
|
209
235
|
const rafIdRef = (0, import_react.useRef)(null);
|
|
210
236
|
const settleTimerRef = (0, import_react.useRef)(null);
|
|
237
|
+
const finalizeTimerRef = (0, import_react.useRef)(null);
|
|
238
|
+
const originalSnapRef = (0, import_react.useRef)("");
|
|
211
239
|
const readPosition = (0, import_react.useCallback)(() => {
|
|
212
240
|
const el = containerRef.current;
|
|
213
241
|
if (!el) return REVEAL_POSITION.center;
|
|
214
|
-
return getRevealFromScroll(
|
|
215
|
-
|
|
242
|
+
return getRevealFromScroll(
|
|
243
|
+
el.scrollLeft,
|
|
244
|
+
getMaxScroll(el),
|
|
245
|
+
getWL(),
|
|
246
|
+
getWR(),
|
|
247
|
+
mode
|
|
248
|
+
);
|
|
249
|
+
}, [getWL, getWR, mode]);
|
|
216
250
|
const emitReveal = (0, import_react.useCallback)(
|
|
217
251
|
(position) => {
|
|
252
|
+
setSettledPosition(position);
|
|
218
253
|
if (lastEmitted.current === position) return;
|
|
219
254
|
lastEmitted.current = position;
|
|
220
255
|
onRevealChange?.(position);
|
|
@@ -225,9 +260,9 @@ function RevealRowInner({
|
|
|
225
260
|
const el = containerRef.current;
|
|
226
261
|
if (!el) return;
|
|
227
262
|
const m = getMaxScroll(el);
|
|
228
|
-
const closed = getScrollClosed(
|
|
263
|
+
const closed = getScrollClosed(getWL(), getWR(), m, mode);
|
|
229
264
|
el.scrollLeft = closed;
|
|
230
|
-
}, [
|
|
265
|
+
}, [getWL, getWR, mode]);
|
|
231
266
|
const scrollToPosition = (0, import_react.useCallback)(
|
|
232
267
|
(targetScrollLeft, animationOptions) => {
|
|
233
268
|
const el = containerRef.current;
|
|
@@ -245,9 +280,29 @@ function RevealRowInner({
|
|
|
245
280
|
clearTimeout(settleTimerRef.current);
|
|
246
281
|
settleTimerRef.current = null;
|
|
247
282
|
}
|
|
283
|
+
if (finalizeTimerRef.current !== null) {
|
|
284
|
+
clearTimeout(finalizeTimerRef.current);
|
|
285
|
+
finalizeTimerRef.current = null;
|
|
286
|
+
}
|
|
287
|
+
if (!isAnimatingRef.current) {
|
|
288
|
+
originalSnapRef.current = el.style.scrollSnapType;
|
|
289
|
+
}
|
|
248
290
|
isAnimatingRef.current = true;
|
|
249
|
-
const originalScrollSnapType = el.style.scrollSnapType;
|
|
250
291
|
el.style.scrollSnapType = "none";
|
|
292
|
+
const finish = () => {
|
|
293
|
+
el.scrollLeft = targetScrollLeft;
|
|
294
|
+
el.style.scrollSnapType = originalSnapRef.current || "x mandatory";
|
|
295
|
+
isAnimatingRef.current = false;
|
|
296
|
+
if (rafIdRef.current !== null) {
|
|
297
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
298
|
+
rafIdRef.current = null;
|
|
299
|
+
}
|
|
300
|
+
if (finalizeTimerRef.current !== null) {
|
|
301
|
+
clearTimeout(finalizeTimerRef.current);
|
|
302
|
+
finalizeTimerRef.current = null;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
finalizeTimerRef.current = setTimeout(finish, duration + 100);
|
|
251
306
|
const startScrollLeft = el.scrollLeft;
|
|
252
307
|
const distance = targetScrollLeft - startScrollLeft;
|
|
253
308
|
const startTime = performance.now();
|
|
@@ -272,86 +327,117 @@ function RevealRowInner({
|
|
|
272
327
|
if (progress < 1) {
|
|
273
328
|
rafIdRef.current = requestAnimationFrame(animate);
|
|
274
329
|
} else {
|
|
275
|
-
el.scrollLeft = targetScrollLeft;
|
|
276
|
-
el.style.scrollSnapType = originalScrollSnapType || "x mandatory";
|
|
277
|
-
isAnimatingRef.current = false;
|
|
278
330
|
rafIdRef.current = null;
|
|
331
|
+
finish();
|
|
279
332
|
}
|
|
280
333
|
};
|
|
281
334
|
rafIdRef.current = requestAnimationFrame(animate);
|
|
282
335
|
},
|
|
283
336
|
[]
|
|
284
337
|
);
|
|
285
|
-
(0, import_react.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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);
|
|
338
|
+
const closeRow = (0, import_react.useCallback)(
|
|
339
|
+
(animated) => {
|
|
340
|
+
const el = containerRef.current;
|
|
341
|
+
if (!el) return;
|
|
342
|
+
const m = getMaxScroll(el);
|
|
343
|
+
const closed = getScrollClosed(getWL(), getWR(), m, mode);
|
|
344
|
+
const animConfig = resolveAnimationConfig(
|
|
345
|
+
animated,
|
|
346
|
+
animationPreset,
|
|
347
|
+
animationConfig
|
|
348
|
+
);
|
|
349
|
+
scrollToPosition(closed, animConfig);
|
|
350
|
+
const delay = animConfig.duration > 0 ? animConfig.duration : 0;
|
|
351
|
+
if (settleTimerRef.current !== null) {
|
|
352
|
+
clearTimeout(settleTimerRef.current);
|
|
342
353
|
}
|
|
343
|
-
|
|
354
|
+
settleTimerRef.current = setTimeout(() => {
|
|
355
|
+
emitReveal(readPosition());
|
|
356
|
+
settleTimerRef.current = null;
|
|
357
|
+
}, delay);
|
|
358
|
+
},
|
|
344
359
|
[
|
|
360
|
+
animationConfig,
|
|
361
|
+
animationPreset,
|
|
362
|
+
emitReveal,
|
|
363
|
+
getWL,
|
|
364
|
+
getWR,
|
|
345
365
|
mode,
|
|
346
|
-
onRevealChange,
|
|
347
366
|
readPosition,
|
|
348
|
-
scrollToPosition
|
|
349
|
-
|
|
350
|
-
|
|
367
|
+
scrollToPosition
|
|
368
|
+
]
|
|
369
|
+
);
|
|
370
|
+
const revealRow = (0, import_react.useCallback)(
|
|
371
|
+
(position, animated) => {
|
|
372
|
+
const el = containerRef.current;
|
|
373
|
+
if (!el) return;
|
|
374
|
+
const max = getMaxScroll(el);
|
|
375
|
+
const wL = getWL();
|
|
376
|
+
const wR = getWR();
|
|
377
|
+
let targetScroll;
|
|
378
|
+
if (position === REVEAL_POSITION.center) {
|
|
379
|
+
targetScroll = getScrollClosed(wL, wR, max, mode);
|
|
380
|
+
} else if (position === REVEAL_POSITION.left && (mode === REVEAL_MODE.left || mode === REVEAL_MODE.both)) {
|
|
381
|
+
targetScroll = 0;
|
|
382
|
+
} else if (position === REVEAL_POSITION.right && (mode === REVEAL_MODE.right || mode === REVEAL_MODE.both)) {
|
|
383
|
+
targetScroll = max;
|
|
384
|
+
} else {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const animConfig = resolveAnimationConfig(
|
|
388
|
+
animated,
|
|
389
|
+
animationPreset,
|
|
390
|
+
animationConfig
|
|
391
|
+
);
|
|
392
|
+
scrollToPosition(targetScroll, animConfig);
|
|
393
|
+
const delay = animConfig.duration > 0 ? animConfig.duration : 0;
|
|
394
|
+
if (settleTimerRef.current !== null) {
|
|
395
|
+
clearTimeout(settleTimerRef.current);
|
|
396
|
+
}
|
|
397
|
+
settleTimerRef.current = setTimeout(() => {
|
|
398
|
+
emitReveal(getRevealFromScroll(targetScroll, max, wL, wR, mode));
|
|
399
|
+
settleTimerRef.current = null;
|
|
400
|
+
}, delay);
|
|
401
|
+
},
|
|
402
|
+
[
|
|
403
|
+
animationConfig,
|
|
351
404
|
animationPreset,
|
|
352
|
-
|
|
405
|
+
emitReveal,
|
|
406
|
+
getWL,
|
|
407
|
+
getWR,
|
|
408
|
+
mode,
|
|
409
|
+
scrollToPosition
|
|
353
410
|
]
|
|
354
411
|
);
|
|
412
|
+
(0, import_react.useImperativeHandle)(
|
|
413
|
+
forwardedRef,
|
|
414
|
+
() => ({ close: closeRow, reveal: revealRow }),
|
|
415
|
+
[closeRow, revealRow]
|
|
416
|
+
);
|
|
417
|
+
const handleRootFocus = (0, import_react.useCallback)(
|
|
418
|
+
(e) => {
|
|
419
|
+
const t = e.target;
|
|
420
|
+
if (!t) return;
|
|
421
|
+
if (leftRef.current?.contains(t)) {
|
|
422
|
+
focusRevealedRef.current = true;
|
|
423
|
+
revealRow(REVEAL_POSITION.left);
|
|
424
|
+
} else if (rightRef.current?.contains(t)) {
|
|
425
|
+
focusRevealedRef.current = true;
|
|
426
|
+
revealRow(REVEAL_POSITION.right);
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
[revealRow]
|
|
430
|
+
);
|
|
431
|
+
const handleRootBlur = (0, import_react.useCallback)(
|
|
432
|
+
(e) => {
|
|
433
|
+
if (!focusRevealedRef.current) return;
|
|
434
|
+
const next = e.relatedTarget;
|
|
435
|
+
if (next && containerRef.current?.contains(next)) return;
|
|
436
|
+
focusRevealedRef.current = false;
|
|
437
|
+
closeRow();
|
|
438
|
+
},
|
|
439
|
+
[closeRow]
|
|
440
|
+
);
|
|
355
441
|
(0, import_react.useLayoutEffect)(() => {
|
|
356
442
|
if (mode === REVEAL_MODE.right) {
|
|
357
443
|
return;
|
|
@@ -359,10 +445,10 @@ function RevealRowInner({
|
|
|
359
445
|
const el = containerRef.current;
|
|
360
446
|
if (!el) return;
|
|
361
447
|
if (!hasAppliedInitialScroll.current) {
|
|
362
|
-
el.scrollLeft =
|
|
448
|
+
el.scrollLeft = getWL();
|
|
363
449
|
hasAppliedInitialScroll.current = true;
|
|
364
450
|
}
|
|
365
|
-
}, [mode,
|
|
451
|
+
}, [mode, getWL]);
|
|
366
452
|
(0, import_react.useEffect)(() => {
|
|
367
453
|
if (disabled && resetWhenDisabled) {
|
|
368
454
|
hasAppliedInitialScroll.current = true;
|
|
@@ -388,6 +474,9 @@ function RevealRowInner({
|
|
|
388
474
|
if (settleTimerRef.current !== null) {
|
|
389
475
|
clearTimeout(settleTimerRef.current);
|
|
390
476
|
}
|
|
477
|
+
if (finalizeTimerRef.current !== null) {
|
|
478
|
+
clearTimeout(finalizeTimerRef.current);
|
|
479
|
+
}
|
|
391
480
|
},
|
|
392
481
|
[]
|
|
393
482
|
);
|
|
@@ -397,6 +486,8 @@ function RevealRowInner({
|
|
|
397
486
|
const el = containerRef.current;
|
|
398
487
|
if (!el) return;
|
|
399
488
|
const m = getMaxScroll(el);
|
|
489
|
+
const wL = getWL();
|
|
490
|
+
const wR = getWR();
|
|
400
491
|
const pos = getRevealFromScroll(el.scrollLeft, m, wL, wR, mode);
|
|
401
492
|
const closed = getScrollClosed(wL, wR, m, mode);
|
|
402
493
|
if (Math.abs(el.scrollLeft - closed) > restEpsilon) {
|
|
@@ -410,7 +501,7 @@ function RevealRowInner({
|
|
|
410
501
|
emitReveal(pos);
|
|
411
502
|
}, SCROLL_REVEAL_DEBOUNCE_MS);
|
|
412
503
|
},
|
|
413
|
-
[emitReveal,
|
|
504
|
+
[emitReveal, getWL, getWR, mode, onScrollProp]
|
|
414
505
|
);
|
|
415
506
|
const onClickCapture = (0, import_react.useCallback)((e) => {
|
|
416
507
|
const t = e.target;
|
|
@@ -425,7 +516,9 @@ function RevealRowInner({
|
|
|
425
516
|
swipedRef.current = false;
|
|
426
517
|
}
|
|
427
518
|
}, []);
|
|
428
|
-
const
|
|
519
|
+
const trackL = wLIn != null ? `${wLIn}px` : "max-content";
|
|
520
|
+
const trackR = wRIn != null ? `${wRIn}px` : "max-content";
|
|
521
|
+
const gridTemplateColumns = mode === REVEAL_MODE.right ? `100% ${trackR}` : mode === REVEAL_MODE.left ? `${trackL} 100%` : `${trackL} 100% ${trackR}`;
|
|
429
522
|
const handleContent = showHandle ? handleOverride !== void 0 ? handleOverride : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
430
523
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
431
524
|
"span",
|
|
@@ -498,9 +591,12 @@ function RevealRowInner({
|
|
|
498
591
|
{
|
|
499
592
|
ref: containerRef,
|
|
500
593
|
"data-reveal-mode": mode,
|
|
594
|
+
"data-reveal-position": settledPosition,
|
|
501
595
|
className: cx(classNames.root, className),
|
|
502
596
|
onScroll: handleScroll,
|
|
503
597
|
onClickCapture,
|
|
598
|
+
onFocus: handleRootFocus,
|
|
599
|
+
onBlur: handleRootBlur,
|
|
504
600
|
style: {
|
|
505
601
|
...disabled ? rootScrollStyleDisabled : rootScrollStyle,
|
|
506
602
|
...style,
|
|
@@ -513,7 +609,10 @@ function RevealRowInner({
|
|
|
513
609
|
ref: leftRef,
|
|
514
610
|
className: classNames.left,
|
|
515
611
|
"data-reveal-row-left": true,
|
|
516
|
-
style: {
|
|
612
|
+
style: {
|
|
613
|
+
...snapStart,
|
|
614
|
+
...wLIn != null ? { minWidth: 0, width: wLIn } : { minWidth: "var(--reveal-row-action-min-width, 88px)" }
|
|
615
|
+
},
|
|
517
616
|
children: left
|
|
518
617
|
}
|
|
519
618
|
) : null,
|
|
@@ -524,7 +623,10 @@ function RevealRowInner({
|
|
|
524
623
|
ref: rightRef,
|
|
525
624
|
className: classNames.right,
|
|
526
625
|
"data-reveal-row-right": true,
|
|
527
|
-
style: {
|
|
626
|
+
style: {
|
|
627
|
+
...snapEnd,
|
|
628
|
+
...wRIn != null ? { minWidth: 0, width: wRIn } : { minWidth: "var(--reveal-row-action-min-width, 88px)" }
|
|
629
|
+
},
|
|
528
630
|
children: right
|
|
529
631
|
}
|
|
530
632
|
) : null
|