@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/README.md +100 -9
- package/dist/index.d.mts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +257 -91
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +259 -92
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -16
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-reveal pattern every mobile inbox and to-do list has made second nature — 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 it in Storybook](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,35 @@ 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 side by side (say, 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 Storybook](https://present-day.github.io/reveal-row/?path=/story/revealrow--multiple-actions)** 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
|
+
- **Discoverable gesture** — tapping or clicking the handle briefly peeks the actions and springs back (released before the snap point), teaching the swipe without committing to a revealed state. Disable with `peekOnHandleTap={false}`; it's skipped automatically under reduced motion or `animationPreset="none"`.
|
|
88
|
+
|
|
40
89
|
## Modes
|
|
41
90
|
|
|
42
91
|
| `mode` | Slots used | Resting ("closed") scroll |
|
|
@@ -55,14 +104,15 @@ Omit `mode` and it's inferred: both slots → `both`, only `left` → `left`, ot
|
|
|
55
104
|
| `left` | `ReactNode` | — | Leading action column |
|
|
56
105
|
| `right` | `ReactNode` | — | Trailing action column |
|
|
57
106
|
| `mode` | `'left' \| 'right' \| 'both'` | inferred | Override mode detection |
|
|
58
|
-
| `actionWidthLeft` | `number` |
|
|
59
|
-
| `actionWidthRight` | `number` |
|
|
107
|
+
| `actionWidthLeft` | `number` | auto (min 88px) | Fixed width (px) of the left column; omit to size to content |
|
|
108
|
+
| `actionWidthRight` | `number` | auto (min 88px) | Fixed width (px) of the right column; omit to size to content |
|
|
60
109
|
| `classNames` | `RevealRowClassNames` | `{}` | Class names for each sub-element |
|
|
61
110
|
| `showHandle` | `boolean` | `true` | Render the default 6-dot drag affordance |
|
|
62
111
|
| `handle` | `ReactNode` | — | Replace the default handle with custom content |
|
|
63
112
|
| `handlePosition` | `'start' \| 'end'` | `'start'` in left mode, `'end'` otherwise | Where the handle strip sits in the row |
|
|
64
113
|
| `handleTitle` | `string` | `'Drag horizontally…'` | Tooltip on the default handle |
|
|
65
114
|
| `handleAriaLabel` | `string` | `'Drag horizontally…'` | Screen-reader text on the default handle |
|
|
115
|
+
| `peekOnHandleTap` | `boolean` | `true` | Tapping the handle briefly peeks the actions and springs back, hinting at the swipe gesture |
|
|
66
116
|
| `onRevealChange` | `(pos: RevealPosition) => void` | — | Fires when the settled position changes (debounced) |
|
|
67
117
|
| `onScroll` | `UIEventHandler` | — | Raw scroll events |
|
|
68
118
|
| `disabled` | `boolean` | `false` | Disables swiping |
|
|
@@ -87,9 +137,9 @@ ref.current?.reveal('right') // snap to right action
|
|
|
87
137
|
ref.current?.reveal('center') // alias for close
|
|
88
138
|
```
|
|
89
139
|
|
|
90
|
-
## classNames
|
|
140
|
+
## Styling with classNames
|
|
91
141
|
|
|
92
|
-
All sub-elements accept class names
|
|
142
|
+
All sub-elements accept class names, so styling is entirely yours:
|
|
93
143
|
|
|
94
144
|
```tsx
|
|
95
145
|
<RevealRow
|
|
@@ -113,6 +163,47 @@ All sub-elements accept class names for styling:
|
|
|
113
163
|
|
|
114
164
|
**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
165
|
|
|
116
|
-
|
|
166
|
+
**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`.
|
|
167
|
+
|
|
168
|
+
**Single-axis scrolling** — the root pins `overflow-y: hidden` so rows only ever scroll horizontally; vertical overflow clips instead of scrolling.
|
|
169
|
+
|
|
170
|
+
**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:
|
|
171
|
+
|
|
172
|
+
```css
|
|
173
|
+
:root { --row-radius: 12px; }
|
|
174
|
+
.list { border-radius: var(--row-radius); }
|
|
175
|
+
.list > :first-child [data-reveal-row-right] button:last-child { border-top-right-radius: var(--row-radius); }
|
|
176
|
+
.list > :last-child [data-reveal-row-right] button:last-child { border-bottom-right-radius: var(--row-radius); }
|
|
177
|
+
.list > :first-child [data-reveal-row-left] button:first-child { border-top-left-radius: var(--row-radius); }
|
|
178
|
+
.list > :last-child [data-reveal-row-left] button:first-child { border-bottom-left-radius: var(--row-radius); }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The stories implement the same idea with an index-aware helper (`edgeCorners` in `stories/demo.tsx`).
|
|
182
|
+
|
|
183
|
+
## CSS tokens
|
|
184
|
+
|
|
185
|
+
The component is headless, but its one built-in dimension is themeable via a CSS custom property:
|
|
186
|
+
|
|
187
|
+
| Token | Default | Effect |
|
|
188
|
+
| ----- | ------- | ------ |
|
|
189
|
+
| `--reveal-row-action-min-width` | `88px` | Minimum width of an auto-sized action column (the floor under content-based sizing) |
|
|
190
|
+
|
|
191
|
+
Set it on `:root` or any ancestor: `[data-reveal-mode] { --reveal-row-action-min-width: 72px; }`. Explicit `actionWidthLeft`/`actionWidthRight` props bypass the token. The stories layer their own tokens on top (`--row-radius`, `--action-width`) in `stories/demo.css` — retheme everything from one place.
|
|
192
|
+
|
|
193
|
+
## Development
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
bun run storybook # http://localhost:6006 (toolbar: Theme)
|
|
197
|
+
bun run test
|
|
198
|
+
bun run build
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Testing on a phone** — swipe gestures are easiest to judge without the Storybook manager UI around the story. Open a story's standalone iframe instead, e.g. `http://<your-lan-ip>:6006/iframe.html?id=revealrow--multiple-actions` locally, or [the deployed one](https://present-day.github.io/reveal-row/iframe.html?id=revealrow--multiple-actions). Every story shows its own event log at the bottom of the screen.
|
|
202
|
+
|
|
203
|
+
## Publishing (maintainers)
|
|
117
204
|
|
|
118
205
|
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).
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
[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 */
|
|
@@ -88,6 +96,14 @@ type RevealRowProps = {
|
|
|
88
96
|
handleTitle?: string;
|
|
89
97
|
/** Shown in screen-reader text when using the default drag handle. */
|
|
90
98
|
handleAriaLabel?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Tapping/clicking the drag handle briefly peeks the actions and springs
|
|
101
|
+
* back before the snap point, hinting at the swipe gesture. Skipped when
|
|
102
|
+
* the row's default animation resolves to instant (`animationPreset="none"`
|
|
103
|
+
* or the OS prefers-reduced-motion setting).
|
|
104
|
+
* @default true
|
|
105
|
+
*/
|
|
106
|
+
peekOnHandleTap?: boolean;
|
|
91
107
|
/** Fires when the settled position changes. */
|
|
92
108
|
onRevealChange?: (position: RevealPosition) => void;
|
|
93
109
|
onScroll?: (event: UIEvent<HTMLElement>) => void;
|
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 */
|
|
@@ -88,6 +96,14 @@ type RevealRowProps = {
|
|
|
88
96
|
handleTitle?: string;
|
|
89
97
|
/** Shown in screen-reader text when using the default drag handle. */
|
|
90
98
|
handleAriaLabel?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Tapping/clicking the drag handle briefly peeks the actions and springs
|
|
101
|
+
* back before the snap point, hinting at the swipe gesture. Skipped when
|
|
102
|
+
* the row's default animation resolves to instant (`animationPreset="none"`
|
|
103
|
+
* or the OS prefers-reduced-motion setting).
|
|
104
|
+
* @default true
|
|
105
|
+
*/
|
|
106
|
+
peekOnHandleTap?: boolean;
|
|
91
107
|
/** Fires when the settled position changes. */
|
|
92
108
|
onRevealChange?: (position: RevealPosition) => void;
|
|
93
109
|
onScroll?: (event: UIEvent<HTMLElement>) => void;
|