@delacour/react-native-bottom-sheet 0.1.0-alpha.0 → 0.1.0-alpha.20261006140348
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 +2 -2
- package/package.json +1 -1
- package/src/animation/AGENTS.md +1 -1
- package/src/animation/animation.types.ts +1 -1
- package/src/animation/use-animate-to.ts +4 -4
- package/src/animation/use-settle-callbacks.ts +3 -3
- package/src/components/AGENTS.md +2 -2
- package/src/components/bottom-sheet-background.tsx +1 -1
- package/src/components/bottom-sheet-content.tsx +3 -3
- package/src/components/bottom-sheet-footer.tsx +3 -3
- package/src/components/bottom-sheet-handle.tsx +5 -5
- package/src/components/bottom-sheet-overlay.tsx +1 -1
- package/src/components/bottom-sheet.context.tsx +1 -1
- package/src/components/bottom-sheet.tsx +13 -11
- package/src/components/bottom-sheet.types.ts +15 -15
- package/src/core/AGENTS.md +17 -17
- package/src/core/backdrop/backdrop-opacity.test.ts +2 -2
- package/src/core/backdrop/backdrop-opacity.ts +1 -1
- package/src/core/footer/body-inset.test.ts +3 -3
- package/src/core/footer/body-inset.ts +3 -3
- package/src/core/geometry/position.ts +1 -1
- package/src/core/geometry/surface-height.test.ts +5 -5
- package/src/core/geometry/surface-height.ts +7 -7
- package/src/core/haptic/crossed-snap-point.test.ts +43 -0
- package/src/core/haptic/crossed-snap-point.ts +33 -0
- package/src/core/index.ts +14 -9
- package/src/core/intent/layout-ready.ts +1 -1
- package/src/core/intent/resolve-intent.test.ts +9 -9
- package/src/core/intent/resolve-intent.ts +10 -10
- package/src/core/keyboard/container-layout-guard.ts +1 -1
- package/src/core/keyboard/keyboard-step.ts +2 -2
- package/src/core/machine/machine.types.ts +2 -2
- package/src/core/machine/step-transition.ts +2 -2
- package/src/core/scroll/scroll-lock.test.ts +2 -2
- package/src/core/scroll/scroll-lock.ts +2 -2
- package/src/core/scroll/scroll-pan.test.ts +14 -14
- package/src/core/scroll/scroll-pan.ts +14 -14
- package/src/core/sheet.types.ts +5 -5
- package/src/core/snap-points/dynamic-snap-point.test.ts +44 -0
- package/src/core/{detents/dynamic-detent.ts → snap-points/dynamic-snap-point.ts} +5 -5
- package/src/core/snap-points/index-for-height.test.ts +65 -0
- package/src/core/{detents → snap-points}/index-for-height.ts +11 -11
- package/src/core/snap-points/normalize-snap-points.test.ts +72 -0
- package/src/core/{detents/normalize-detents.ts → snap-points/normalize-snap-points.ts} +9 -9
- package/src/core/{detents → snap-points}/select-snap-height.test.ts +10 -8
- package/src/core/{detents → snap-points}/select-snap-height.ts +5 -5
- package/src/core/snap-points/sheet-state.test.ts +42 -0
- package/src/core/{detents → snap-points}/sheet-state.ts +6 -6
- package/src/gesture/AGENTS.md +11 -11
- package/src/gesture/gesture.types.ts +3 -3
- package/src/gesture/use-scroll-lock.ts +3 -3
- package/src/gesture/use-sheet-pan.ts +43 -32
- package/src/keyboard/AGENTS.md +2 -2
- package/src/keyboard/use-sheet-keyboard.ts +6 -6
- package/src/layout/AGENTS.md +5 -5
- package/src/portal/AGENTS.md +14 -5
- package/src/portal/bottom-sheet-provider.tsx +19 -7
- package/src/scrollable/AGENTS.md +5 -5
- package/src/scrollable/bottom-sheet-scroll-view.tsx +1 -1
- package/src/scrollable/create-bottom-sheet-scrollable.tsx +3 -3
- package/src/state/AGENTS.md +4 -4
- package/src/state/state.types.ts +11 -11
- package/src/state/use-sheet-geometry.ts +11 -11
- package/src/state/use-sheet-intents.ts +12 -9
- package/src/state/use-sheet-state.ts +9 -9
- package/src/state/{use-detents.ts → use-snap-points.ts} +13 -13
- package/src/steps/AGENTS.md +7 -7
- package/src/steps/bottom-sheet-steps.tsx +9 -9
- package/src/core/detents/dynamic-detent.test.ts +0 -40
- package/src/core/detents/index-for-height.test.ts +0 -65
- package/src/core/detents/normalize-detents.test.ts +0 -72
- package/src/core/detents/sheet-state.test.ts +0 -42
- package/src/core/haptic/crossed-detent.test.ts +0 -43
- package/src/core/haptic/crossed-detent.ts +0 -33
- /package/src/core/{detents → snap-points}/over-drag.test.ts +0 -0
- /package/src/core/{detents → snap-points}/over-drag.ts +0 -0
|
@@ -5,16 +5,16 @@ import { cancelAnimation, useSharedValue } from "react-native-reanimated";
|
|
|
5
5
|
import { scheduleOnRN } from "react-native-worklets";
|
|
6
6
|
import type { AnimateTo, SettleAt } from "../animation/animation.types";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
detentUnder,
|
|
8
|
+
crossedSnapPoint,
|
|
10
9
|
GESTURE_SOURCE,
|
|
11
10
|
type GestureSource,
|
|
12
11
|
listDragHeight,
|
|
13
12
|
listOwnsRelease,
|
|
14
13
|
resistOverDrag,
|
|
15
|
-
|
|
14
|
+
restingSnapPoint,
|
|
16
15
|
SCROLLABLE_TYPE,
|
|
17
16
|
selectSnapHeight,
|
|
17
|
+
snapPointUnder,
|
|
18
18
|
} from "../core";
|
|
19
19
|
import { ANIM_STATUS, type SheetGeometry, type SheetSharedState } from "../state/state.types";
|
|
20
20
|
import {
|
|
@@ -27,8 +27,8 @@ import {
|
|
|
27
27
|
|
|
28
28
|
type PanEvent = { translationY: number; velocityY: number };
|
|
29
29
|
|
|
30
|
-
/** Settle tolerance: a sheet within this of a
|
|
31
|
-
const
|
|
30
|
+
/** Settle tolerance: a sheet within this of a snap point is on it. */
|
|
31
|
+
const AT_SNAP_POINT = 0.5;
|
|
32
32
|
|
|
33
33
|
/** JS-thread, so `scheduleOnRN` has a plain function to call; `dismiss` returns a promise nobody awaits. */
|
|
34
34
|
function dismissKeyboard(): void {
|
|
@@ -40,16 +40,16 @@ function dismissKeyboard(): void {
|
|
|
40
40
|
*
|
|
41
41
|
* Everything runs on the UI thread. `onStart` cancels whatever animation owns
|
|
42
42
|
* `base` and remembers where the drag began; `onChange` turns the finger's
|
|
43
|
-
* travel into a height, rubber-banded past the
|
|
44
|
-
* and never above the container; `onFinalize` picks the
|
|
43
|
+
* travel into a height, rubber-banded past the snap points by `resistOverDrag`
|
|
44
|
+
* and never above the container; `onFinalize` picks the snap point the finger was
|
|
45
45
|
* heading for with `selectSnapHeight` and hands it to `animateTo` with the
|
|
46
46
|
* release velocity. The close is a candidate only when `enablePanDownToClose`.
|
|
47
47
|
*
|
|
48
48
|
* A content pan the list owns at release — the sheet at its top, the list
|
|
49
49
|
* scrolled — does not snap, but it still settles: the finger moved `base` by
|
|
50
50
|
* hand, so no animation ran and nothing else writes `currentIndex`. `settleAt`
|
|
51
|
-
* on the
|
|
52
|
-
* without the motion; between
|
|
51
|
+
* on the snap point under `base` is the bookkeeping of a finished animation
|
|
52
|
+
* without the motion; between snap points (which the list ownership test should
|
|
53
53
|
* rule out) the sheet snaps to the nearest one with no velocity instead, since
|
|
54
54
|
* the release velocity is the list's momentum.
|
|
55
55
|
*
|
|
@@ -62,7 +62,7 @@ function dismissKeyboard(): void {
|
|
|
62
62
|
* worklets in the ordinary way. See the package `AGENTS.md`: that is legal
|
|
63
63
|
* precisely because these are not module-scope worklets.
|
|
64
64
|
*
|
|
65
|
-
* Per-gesture memory — where the drag began, the
|
|
65
|
+
* Per-gesture memory — where the drag began, the snap point last under it,
|
|
66
66
|
* whether it is over-dragging — lives in shared values rather than closure
|
|
67
67
|
* variables. Each worklet gets its own copy of a captured `let`, so a write in
|
|
68
68
|
* `onStart` would never be seen by `onChange`.
|
|
@@ -75,12 +75,17 @@ export function useSheetPan(
|
|
|
75
75
|
options: SheetPanOptions
|
|
76
76
|
): SheetPans {
|
|
77
77
|
const startBase = useSharedValue(0);
|
|
78
|
-
const
|
|
78
|
+
const lastSnapPoint = useSharedValue(-1);
|
|
79
79
|
const overDragging = useSharedValue(false);
|
|
80
80
|
const startScroll = useSharedValue(0);
|
|
81
81
|
const listHeld = useSharedValue(false);
|
|
82
|
-
const {
|
|
83
|
-
|
|
82
|
+
const {
|
|
83
|
+
enableHandlePanningGesture,
|
|
84
|
+
enableContentPanningGesture,
|
|
85
|
+
onSnapPointHaptic,
|
|
86
|
+
onCloseHaptic,
|
|
87
|
+
onOverDragHaptic,
|
|
88
|
+
} = options;
|
|
84
89
|
|
|
85
90
|
return useMemo(() => {
|
|
86
91
|
const begin = (source: GestureSource): void => {
|
|
@@ -91,9 +96,9 @@ export function useSheetPan(
|
|
|
91
96
|
startBase.value = state.base.value;
|
|
92
97
|
startScroll.value = state.scrollOffsetY.value;
|
|
93
98
|
// A list scrolled and below the top is held by the lock: it has no travel
|
|
94
|
-
// to spend until the sheet reaches the highest
|
|
95
|
-
listHeld.value = state.scrollOffsetY.value > 0 && state.base.value < geometry.highest.value -
|
|
96
|
-
|
|
99
|
+
// to spend until the sheet reaches the highest snap point.
|
|
100
|
+
listHeld.value = state.scrollOffsetY.value > 0 && state.base.value < geometry.highest.value - AT_SNAP_POINT;
|
|
101
|
+
lastSnapPoint.value = snapPointUnder(state.base.value, geometry.snapPoints.value);
|
|
97
102
|
overDragging.value = false;
|
|
98
103
|
// A finger on the sheet is a reason to put the keyboard away; the lift
|
|
99
104
|
// comes off as it goes and the drag continues from wherever `base` is.
|
|
@@ -102,15 +107,21 @@ export function useSheetPan(
|
|
|
102
107
|
}
|
|
103
108
|
};
|
|
104
109
|
|
|
105
|
-
const haptics = (
|
|
110
|
+
const haptics = (
|
|
111
|
+
raw: number,
|
|
112
|
+
next: number,
|
|
113
|
+
lowest: number,
|
|
114
|
+
highest: number,
|
|
115
|
+
snapPoints: readonly number[]
|
|
116
|
+
): void => {
|
|
106
117
|
"worklet";
|
|
107
118
|
const over = raw > highest || raw < lowest;
|
|
108
119
|
if (over && !overDragging.value && onOverDragHaptic) onOverDragHaptic();
|
|
109
120
|
overDragging.value = over;
|
|
110
121
|
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
if (
|
|
122
|
+
if (crossedSnapPoint(lastSnapPoint.value, next, snapPoints)) {
|
|
123
|
+
lastSnapPoint.value = snapPointUnder(next, snapPoints);
|
|
124
|
+
if (onSnapPointHaptic) onSnapPointHaptic();
|
|
114
125
|
}
|
|
115
126
|
};
|
|
116
127
|
|
|
@@ -138,18 +149,18 @@ export function useSheetPan(
|
|
|
138
149
|
"worklet";
|
|
139
150
|
if (state.gestureSource.value === GESTURE_SOURCE.NONE) return;
|
|
140
151
|
const config = state.config.value;
|
|
141
|
-
const
|
|
152
|
+
const snapPoints = geometry.snapPoints.value;
|
|
142
153
|
const closed = geometry.closedHeight.value;
|
|
143
154
|
const highest = geometry.highest.value;
|
|
144
155
|
const raw = dragged(event.translationY, highest);
|
|
145
156
|
|
|
146
|
-
const lowest = config.enablePanDownToClose ||
|
|
157
|
+
const lowest = config.enablePanDownToClose || snapPoints.length === 0 ? closed : (snapPoints[0] as number);
|
|
147
158
|
const factor = config.enableOverDrag ? config.overDragResistanceFactor : 0;
|
|
148
159
|
const next = Math.min(resistOverDrag(raw, lowest, highest, factor), geometry.maxHeight.value);
|
|
149
160
|
|
|
150
|
-
haptics(raw, next, lowest, highest,
|
|
161
|
+
haptics(raw, next, lowest, highest, snapPoints);
|
|
151
162
|
state.base.value = next;
|
|
152
|
-
if (listHeld.value && next >= highest -
|
|
163
|
+
if (listHeld.value && next >= highest - AT_SNAP_POINT) listHeld.value = false;
|
|
153
164
|
};
|
|
154
165
|
|
|
155
166
|
// The haptic for a close, the keyboard for a downward release — a release
|
|
@@ -162,11 +173,11 @@ export function useSheetPan(
|
|
|
162
173
|
animateTo(target, "gesture", velocity / 2);
|
|
163
174
|
};
|
|
164
175
|
|
|
165
|
-
// A release the list owns settles the sheet on the
|
|
166
|
-
// with no motion; `false` when it sits between
|
|
176
|
+
// A release the list owns settles the sheet on the snap point under `base`
|
|
177
|
+
// with no motion; `false` when it sits between snap points and has to snap.
|
|
167
178
|
const settleResting = (): boolean => {
|
|
168
179
|
"worklet";
|
|
169
|
-
const resting =
|
|
180
|
+
const resting = restingSnapPoint(state.base.value, geometry.snapPoints.value);
|
|
170
181
|
if (resting === null) return false;
|
|
171
182
|
settleAt(resting, "gesture");
|
|
172
183
|
return true;
|
|
@@ -180,7 +191,7 @@ export function useSheetPan(
|
|
|
180
191
|
const config = state.config.value;
|
|
181
192
|
const closed = geometry.closedHeight.value;
|
|
182
193
|
|
|
183
|
-
// The finger was scrolling rows at the top
|
|
194
|
+
// The finger was scrolling rows at the top snap point: the release is the
|
|
184
195
|
// list's momentum, not a snap — but the sheet still settles where it is.
|
|
185
196
|
const listOwns = listOwnsRelease({
|
|
186
197
|
scrollable: overList(source),
|
|
@@ -190,13 +201,13 @@ export function useSheetPan(
|
|
|
190
201
|
});
|
|
191
202
|
if (listOwns && settleResting()) return;
|
|
192
203
|
|
|
193
|
-
// A list-owned release between
|
|
204
|
+
// A list-owned release between snap points (which `listOwnsRelease` should
|
|
194
205
|
// rule out) snaps to the nearest with no velocity and never closes.
|
|
195
206
|
const velocity = listOwns ? 0 : -event.velocityY;
|
|
196
207
|
const target = selectSnapHeight({
|
|
197
208
|
height: state.base.value,
|
|
198
209
|
velocity,
|
|
199
|
-
|
|
210
|
+
snapPoints: geometry.snapPoints.value,
|
|
200
211
|
closedHeight: config.enablePanDownToClose && !listOwns ? closed : null,
|
|
201
212
|
projection: SNAP_PROJECTION,
|
|
202
213
|
});
|
|
@@ -239,11 +250,11 @@ export function useSheetPan(
|
|
|
239
250
|
startBase,
|
|
240
251
|
startScroll,
|
|
241
252
|
listHeld,
|
|
242
|
-
|
|
253
|
+
lastSnapPoint,
|
|
243
254
|
overDragging,
|
|
244
255
|
enableHandlePanningGesture,
|
|
245
256
|
enableContentPanningGesture,
|
|
246
|
-
|
|
257
|
+
onSnapPointHaptic,
|
|
247
258
|
onCloseHaptic,
|
|
248
259
|
onOverDragHaptic,
|
|
249
260
|
]);
|
package/src/keyboard/AGENTS.md
CHANGED
|
@@ -55,8 +55,8 @@ stops a tap from one field to the next reading as a close and a reopen.
|
|
|
55
55
|
| `keyboardBehavior` | On show | `keyboardBlurBehavior: "restore"` on hide |
|
|
56
56
|
| --- | --- | --- |
|
|
57
57
|
| `interactive` (default) | Nothing — `keyboardLift` is a derivation over `progress`, so the sheet rides the keyboard up and follows an interactive dismiss down | Nothing to restore; `base` never moved |
|
|
58
|
-
| `extend` | `animateTo(highest, "keyboard")` | Back to the
|
|
59
|
-
| `fillParent` | `animateTo(maxHeight, "keyboard")`; no lift; the body shrinks by the keyboard | Back to the
|
|
58
|
+
| `extend` | `animateTo(highest, "keyboard")` | Back to the snap point held before |
|
|
59
|
+
| `fillParent` | `animateTo(maxHeight, "keyboard")`; no lift; the body shrinks by the keyboard | Back to the snap point held before |
|
|
60
60
|
| `none` | Nothing; the keyboard is not the sheet's problem | Nothing |
|
|
61
61
|
|
|
62
62
|
The behaviour reaction samples `progress` and `keyboardOwned` and hands two
|
|
@@ -48,9 +48,9 @@ export type UseSheetKeyboardOptions = {
|
|
|
48
48
|
*
|
|
49
49
|
* The second is the behaviour. `interactive` and `none` need none: one lifts
|
|
50
50
|
* by derivation, the other ignores the keyboard. `extend` and `fillParent`
|
|
51
|
-
* snap — to the highest
|
|
51
|
+
* snap — to the highest snap point or the container — when an owned keyboard
|
|
52
52
|
* rises, and `keyboardBlurBehavior: "restore"` sends the sheet back to the
|
|
53
|
-
*
|
|
53
|
+
* snap point it held when the keyboard leaves. `keyboardStep` in the core makes
|
|
54
54
|
* that call from two consecutive samples; this reaction only carries it out,
|
|
55
55
|
* and stays out of the way of a gesture that owns `base`.
|
|
56
56
|
*/
|
|
@@ -139,7 +139,7 @@ export function useSheetKeyboard({
|
|
|
139
139
|
if (dragging) return;
|
|
140
140
|
applied.value = true;
|
|
141
141
|
indexBefore.value = running
|
|
142
|
-
? Math.round(indexForHeight(state.animTarget.value, geometry.
|
|
142
|
+
? Math.round(indexForHeight(state.animTarget.value, geometry.snapPoints.value, closed))
|
|
143
143
|
: state.currentIndex.value;
|
|
144
144
|
const target = config.keyboardBehavior === "fillParent" ? geometry.maxHeight.value : geometry.highest.value;
|
|
145
145
|
if (running || Math.abs(target - state.base.value) > 0.5) animateTo(target, "keyboard", 0);
|
|
@@ -151,9 +151,9 @@ export function useSheetKeyboard({
|
|
|
151
151
|
indexBefore.value = CLOSED_INDEX;
|
|
152
152
|
if (step !== "restore" || dragging || !sheetOpen || index < 0) return;
|
|
153
153
|
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
const target = heightForIndex(Math.min(index,
|
|
154
|
+
const snapPoints = geometry.snapPoints.value;
|
|
155
|
+
if (snapPoints.length === 0) return;
|
|
156
|
+
const target = heightForIndex(Math.min(index, snapPoints.length - 1), snapPoints, closed);
|
|
157
157
|
if (Math.abs(target - state.base.value) > 0.5) animateTo(target, "keyboard", 0);
|
|
158
158
|
},
|
|
159
159
|
[state, geometry, animateTo, progress, applied, indexBefore]
|
package/src/layout/AGENTS.md
CHANGED
|
@@ -13,11 +13,11 @@ How the sheet learns the sizes the geometry needs.
|
|
|
13
13
|
|
|
14
14
|
| Value | By | Read for |
|
|
15
15
|
| --- | --- | --- |
|
|
16
|
-
| `containerHeight`, `containerWidth` | the frame `Portal` renders | every
|
|
16
|
+
| `containerHeight`, `containerWidth` | the frame `Portal` renders | every snap point, `position`, the detached frame |
|
|
17
17
|
| `containerBottomOffset` | the same frame, via `measureInWindow` | how much of a keyboard overlaps the sheet at all (BSHEET-3) |
|
|
18
|
-
| `handleHeight` | `Handle` | the dynamic
|
|
19
|
-
| `contentHeight` | `Content`'s inner box | the dynamic
|
|
20
|
-
| `footerContentHeight` | `Footer`'s inner box (BSHEET-3) | the dynamic
|
|
18
|
+
| `handleHeight` | `Handle` | the dynamic snap point, `contentArea` |
|
|
19
|
+
| `contentHeight` | `Content`'s inner box | the dynamic snap point |
|
|
20
|
+
| `footerContentHeight` | `Footer`'s inner box (BSHEET-3) | the dynamic snap point, `footerTop` |
|
|
21
21
|
|
|
22
22
|
Scrollables (BSHEET-4) feed `contentHeight` from `onContentSizeChange`
|
|
23
23
|
instead of a layout.
|
|
@@ -40,7 +40,7 @@ The frame's height goes through `acceptContainerLayout`, which refuses
|
|
|
40
40
|
Android's adjustResize double-count while a keyboard is up. Inert until
|
|
41
41
|
BSHEET-3 writes `keyboardHeight`, and correct once it does. A height that is
|
|
42
42
|
accepted while the sheet is open makes the intent reaction jump the sheet to
|
|
43
|
-
the same index in the re-derived
|
|
43
|
+
the same index in the re-derived snap points (`state/`).
|
|
44
44
|
|
|
45
45
|
## No synchronous first read
|
|
46
46
|
|
package/src/portal/AGENTS.md
CHANGED
|
@@ -7,7 +7,7 @@ context and the registry that `Portal` reads.
|
|
|
7
7
|
|
|
8
8
|
| Path | What |
|
|
9
9
|
| --- | --- |
|
|
10
|
-
| `bottom-sheet-provider.tsx` | `BottomSheetProvider` — teleport's `PortalProvider`, the registry provider and the host-name context set to `"root"`. The app mounts it once |
|
|
10
|
+
| `bottom-sheet-provider.tsx` | `BottomSheetProvider` — teleport's `PortalProvider`, the registry provider and the host-name context set to `"root"`. The app mounts it once. `hasPortalProvider` skips the teleport provider when one is already above |
|
|
11
11
|
| `bottom-sheet-host.tsx` | `BottomSheet.Host` — a teleport `PortalHost` (absolute fill, `box-none`) that provides its own name as the nearest one. The recipe for a native modal |
|
|
12
12
|
| `host.context.tsx` | `BottomSheetHostNameContext` — the nearest host's name, `null` with no provider; `useBottomSheetHostName` / `useOptionalBottomSheetHostName` |
|
|
13
13
|
| `sheet-registry.ts` | The pure reducer: `reduceRegistry`, `topOf`, `zIndexOf`, `isTop`, `INITIAL_REGISTRY`. Tested without a renderer |
|
|
@@ -86,16 +86,25 @@ thread, turns `overflow` visible so a card's shadow is not clipped, and is
|
|
|
86
86
|
overlay. The frame is `overflow: visible` too. The overlay is unchanged — it
|
|
87
87
|
fills the frame, margins and gap included, so a tap there closes. The rest is
|
|
88
88
|
`core/`: `closedHeight` is `−restingBottom`, so closed is `translateY =
|
|
89
|
-
containerHeight` and the card is fully off-screen; `%`
|
|
89
|
+
containerHeight` and the card is fully off-screen; `%` snap points resolve against
|
|
90
90
|
the height above the resting line; `keyboardLift` subtracts the gap. The
|
|
91
91
|
corners are the consumer's `Background` style.
|
|
92
92
|
|
|
93
93
|
**The card moves as one rigid body.** The panel is as tall as the frame, so a
|
|
94
94
|
detached `Background` is sized by the geometry's `surfaceHeight`
|
|
95
|
-
(`core/geometry/surface-height.ts`): the sheet's `height` between its
|
|
96
|
-
the first
|
|
97
|
-
close, the close animation, a rubber-band under the lowest
|
|
95
|
+
(`core/geometry/surface-height.ts`): the sheet's `height` between its snap points,
|
|
96
|
+
the first snap point's height below them and the last's above. A drag down to
|
|
97
|
+
close, the close animation, a rubber-band under the lowest snap point and an
|
|
98
98
|
over-drag past the highest therefore leave the surface, the body and the
|
|
99
99
|
footer exactly as they were and let `translateY` alone move the card, bottom
|
|
100
100
|
corners included, through the gap and off the screen. `Content` clamps its
|
|
101
101
|
body to `surfaceHeight − handleHeight` for the same reason.
|
|
102
|
+
|
|
103
|
+
## Sharing teleport's provider
|
|
104
|
+
|
|
105
|
+
There must be one teleport `PortalProvider` per app: teleport registers hosts natively by name,
|
|
106
|
+
and two providers would mean two hosts called `"root"`. `hasPortalProvider` lets an app that
|
|
107
|
+
already mounts one — `@delacour/react-native-ui`'s `OverlayProvider` is the case it was added for
|
|
108
|
+
— mount the sheet registry and host name alone. Sheets then teleport into the outer provider's
|
|
109
|
+
`"root"`, and their registry `z` (from 1) still orders them among themselves; the skin's overlays
|
|
110
|
+
draw in bands above 1000. The skin's `BottomSheetProvider` passes the flag for you.
|
|
@@ -5,6 +5,13 @@ import { SheetRegistryProvider } from "./sheet-registry.context";
|
|
|
5
5
|
|
|
6
6
|
export type BottomSheetProviderProps = {
|
|
7
7
|
children?: ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* A teleport `PortalProvider` is already mounted above, so do not mount a
|
|
10
|
+
* second. Teleport registers hosts natively by name, and two providers
|
|
11
|
+
* would mean two hosts called `"root"`. Sheets then draw into the outer
|
|
12
|
+
* provider's `"root"` host. Default `false`.
|
|
13
|
+
*/
|
|
14
|
+
hasPortalProvider?: boolean;
|
|
8
15
|
};
|
|
9
16
|
|
|
10
17
|
/**
|
|
@@ -18,16 +25,21 @@ export type BottomSheetProviderProps = {
|
|
|
18
25
|
* without a ref; and the host-name context, set to `"root"`, which is what
|
|
19
26
|
* makes a `Portal` written anywhere below teleport there without being told.
|
|
20
27
|
*
|
|
28
|
+
* `hasPortalProvider` drops the first of the three, for an app that already
|
|
29
|
+
* mounts teleport's provider for something else — `@delacour/react-native-ui`'s
|
|
30
|
+
* `OverlayProvider` is the case it exists for.
|
|
31
|
+
*
|
|
21
32
|
* Nothing else. `GestureHandlerRootView`, `SafeAreaProvider` and
|
|
22
33
|
* `KeyboardProvider` are the app's — see the package `AGENTS.md`.
|
|
23
34
|
*/
|
|
24
|
-
export function BottomSheetProvider({ children }: BottomSheetProviderProps): ReactElement {
|
|
25
|
-
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
</SheetRegistryProvider>
|
|
30
|
-
</PortalProvider>
|
|
35
|
+
export function BottomSheetProvider({ children, hasPortalProvider = false }: BottomSheetProviderProps): ReactElement {
|
|
36
|
+
const registry = (
|
|
37
|
+
<SheetRegistryProvider>
|
|
38
|
+
<BottomSheetHostNameContext.Provider value="root">{children}</BottomSheetHostNameContext.Provider>
|
|
39
|
+
</SheetRegistryProvider>
|
|
31
40
|
);
|
|
41
|
+
if (hasPortalProvider) return registry;
|
|
42
|
+
|
|
43
|
+
return <PortalProvider>{registry}</PortalProvider>;
|
|
32
44
|
}
|
|
33
45
|
BottomSheetProvider.displayName = "DelacourBottomSheet.BottomSheet.Provider";
|
package/src/scrollable/AGENTS.md
CHANGED
|
@@ -24,12 +24,12 @@ The content pan and the list's native scroll are **simultaneous**: the list's
|
|
|
24
24
|
`Native` gesture is declared `simultaneousWithExternalGesture(pans.content)`,
|
|
25
25
|
so both see every touch. The rule for who moves is the sheet's state:
|
|
26
26
|
|
|
27
|
-
- **Below the highest
|
|
27
|
+
- **Below the highest snap point — locked.** Every scroll event is answered with
|
|
28
28
|
`scrollTo(ref, 0, scrollLockedAt, false)` and the offset the pan reads is
|
|
29
29
|
pinned there, so the list consumes nothing and the pan moves the sheet. The
|
|
30
30
|
indicator is hidden, `bounces` is off and `decelerationRate` is `0`, so a
|
|
31
31
|
fling has no momentum for the lock to fight.
|
|
32
|
-
- **At the highest
|
|
32
|
+
- **At the highest snap point — unlocked.** The list scrolls. The pan treats the
|
|
33
33
|
offset the list *began* the gesture with as a budget the finger spends
|
|
34
34
|
downward before the sheet moves (`listDragHeight`): the list scrolls
|
|
35
35
|
one-for-one under the same finger, so it reaches its top exactly as the
|
|
@@ -38,7 +38,7 @@ so both see every touch. The rule for who moves is the sheet's state:
|
|
|
38
38
|
over-dragging.
|
|
39
39
|
- **A release while scrolled at the top** belongs to the list
|
|
40
40
|
(`listOwnsRelease`): the velocity is the list's momentum, not a snap. The
|
|
41
|
-
library this replaces snapped here and hopped a
|
|
41
|
+
library this replaces snapped here and hopped a snap point under interrupted
|
|
42
42
|
scrolls.
|
|
43
43
|
|
|
44
44
|
The budget is the offset at the *start* of the gesture, never the live one.
|
|
@@ -78,14 +78,14 @@ earlier answer and is gone: it ended the list above the footer with the rows
|
|
|
78
78
|
cut at that edge and an empty band under them.
|
|
79
79
|
|
|
80
80
|
`onContentSizeChange`, less the spacer's own measured height
|
|
81
|
-
(`scrollContentHeight`), writes `contentHeight`, which is the dynamic
|
|
81
|
+
(`scrollContentHeight`), writes `contentHeight`, which is the dynamic snap point's
|
|
82
82
|
measurement. A `BottomSheet.ScrollView` of forty rows therefore needs neither
|
|
83
83
|
`snapPoints` nor `dynamicSizing={false}`: it sizes to its rows, capped by
|
|
84
84
|
`maxDynamicContentSize`, and scrolls inside that. The content size and the
|
|
85
85
|
spacer's layout arrive as two events from one commit; both are held in refs
|
|
86
86
|
and folded into a single write after the batch, because the first open
|
|
87
87
|
resolves on whatever `contentHeight` says first and the mount animation, once
|
|
88
|
-
running, ignores a corrected
|
|
88
|
+
running, ignores a corrected snap point list — a frame of rows-plus-spacer would
|
|
89
89
|
open the sheet a footer too tall. `contentHeight` goes back to `UNMEASURED` on
|
|
90
90
|
unmount for the same reason `useMeasureHeight` resets it.
|
|
91
91
|
|
|
@@ -4,7 +4,7 @@ import { createBottomSheetScrollable } from "./create-bottom-sheet-scrollable";
|
|
|
4
4
|
import type { BottomSheetScrollViewComponent } from "./scrollable.types";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* A `ScrollView` as the sheet's body: locked below the highest
|
|
7
|
+
* A `ScrollView` as the sheet's body: locked below the highest snap point, sized
|
|
8
8
|
* to its content when the sheet is dynamic. `Animated.ScrollView` is
|
|
9
9
|
* Reanimated's own, made once at module scope.
|
|
10
10
|
*/
|
|
@@ -58,7 +58,7 @@ function footerElement(footer: ScrollableListFooter | undefined): ReactElement |
|
|
|
58
58
|
* object `Content` uses; the inner is a `Native` gesture on the list itself,
|
|
59
59
|
* declared simultaneous with that pan and kept alive when the finger leaves
|
|
60
60
|
* the view. Both run at once, and the pan subtracts what the list consumed.
|
|
61
|
-
* - The scroll lock (`useScrollLock`). Below the highest
|
|
61
|
+
* - The scroll lock (`useScrollLock`). Below the highest snap point the list is
|
|
62
62
|
* held where it was, so the pan moves the sheet; at the highest it scrolls,
|
|
63
63
|
* and a drag down at the top hands back to the sheet. The indicator hides,
|
|
64
64
|
* bounce switches off and momentum is cut while locked.
|
|
@@ -72,11 +72,11 @@ function footerElement(footer: ScrollableListFooter | undefined): ReactElement |
|
|
|
72
72
|
* shows below the footer's top edge, at rest or while the sheet is dragged
|
|
73
73
|
* down; the list itself does not shrink to that clip (see `styles.list`).
|
|
74
74
|
* - Content size. `onContentSizeChange` less the spacer's own measured height
|
|
75
|
-
* feeds `contentHeight`, which is the dynamic
|
|
75
|
+
* feeds `contentHeight`, which is the dynamic snap point's measurement: a
|
|
76
76
|
* `ScrollView` of forty rows needs no `snapPoints` and no
|
|
77
77
|
* `dynamicSizing={false}` — it sizes to its rows up to
|
|
78
78
|
* `maxDynamicContentSize`, then scrolls. The two layout events land in one
|
|
79
|
-
* batch and are folded into one write, so the
|
|
79
|
+
* batch and are folded into one write, so the snap point never sees the rows
|
|
80
80
|
* plus the spacer for a frame — a first open resolved on that sum would
|
|
81
81
|
* animate to it and ignore the correction.
|
|
82
82
|
* - Registration. On focus the list tells the sheet what kind it is, and on
|
package/src/state/AGENTS.md
CHANGED
|
@@ -9,9 +9,9 @@ queue that moves them.
|
|
|
9
9
|
| --- | --- |
|
|
10
10
|
| `state.types.ts` | `SheetSharedState` (the raw shared values), `SheetGeometry` (the derived ones), `SheetWorkletConfig` (the props a worklet reads), `ANIM_STATUS` |
|
|
11
11
|
| `use-sheet-state.ts` | Allocates the raw values once per sheet; mirrors `snapPoints` and the config into them, keyed on serialisation rather than identity |
|
|
12
|
-
| `use-
|
|
12
|
+
| `use-snap-points.ts` | `snapPoints` — `normalizeSnapPoints` plus the dynamic snap point once handle and content have measured |
|
|
13
13
|
| `use-sheet-geometry.ts` | Every formula in `core/AGENTS.md` as a `useDerivedValue`, in dependency order |
|
|
14
|
-
| `use-sheet-intents.ts` | The JS-thread `dispatch`, the reaction that resolves an intent on the UI thread, and the reaction that follows a
|
|
14
|
+
| `use-sheet-intents.ts` | The JS-thread `dispatch`, the reaction that resolves an intent on the UI thread, and the reaction that follows a snap point change or a container resize |
|
|
15
15
|
|
|
16
16
|
## The model
|
|
17
17
|
|
|
@@ -47,9 +47,9 @@ would see a closed sheet, resolve to nothing, and let the sheet finish opening.
|
|
|
47
47
|
The first open a sheet resolves is reported as `"mount"` and, with
|
|
48
48
|
`animateOnMount: false`, jumps. `mountPending` is that flag.
|
|
49
49
|
|
|
50
|
-
##
|
|
50
|
+
## Snap points moving under a settled sheet
|
|
51
51
|
|
|
52
|
-
The second reaction watches the
|
|
52
|
+
The second reaction watches the snap point list and the container height. When
|
|
53
53
|
the list changes with the sheet idle and open, the sheet animates to the same
|
|
54
54
|
index in the new list — dynamic content growing, a `snapPoints` change. When
|
|
55
55
|
the container changed, it jumps there instead: a rotation is not something to
|
package/src/state/state.types.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { SharedValue } from "react-native-reanimated";
|
|
|
2
2
|
import type {
|
|
3
3
|
AnimationSource,
|
|
4
4
|
DetachedOptions,
|
|
5
|
-
DetentSpec,
|
|
6
5
|
GestureSource,
|
|
7
6
|
KeyboardBehavior,
|
|
8
7
|
KeyboardBlurBehavior,
|
|
@@ -10,6 +9,7 @@ import type {
|
|
|
10
9
|
ScrollableType,
|
|
11
10
|
SheetIntent,
|
|
12
11
|
SheetState,
|
|
12
|
+
SnapPointSpec,
|
|
13
13
|
} from "../core";
|
|
14
14
|
|
|
15
15
|
/** Whether an animation is running. Numeric so the UI thread never serialises a string. */
|
|
@@ -68,7 +68,7 @@ export type SheetSharedState = {
|
|
|
68
68
|
gestureSource: SharedValue<GestureSource>;
|
|
69
69
|
/** The scrollable's live offset, written by its scroll handler; negative while bounced past the top. */
|
|
70
70
|
scrollOffsetY: SharedValue<number>;
|
|
71
|
-
/** Where the scrollable is held while the sheet is below its highest
|
|
71
|
+
/** Where the scrollable is held while the sheet is below its highest snap point. */
|
|
72
72
|
scrollLockedAt: SharedValue<number>;
|
|
73
73
|
/** Which scrollable is the body, `NONE` for static content. Set by `createBottomSheetScrollable` on focus. */
|
|
74
74
|
scrollableType: SharedValue<ScrollableType>;
|
|
@@ -82,7 +82,7 @@ export type SheetSharedState = {
|
|
|
82
82
|
/** Whether the next open is the mount open — reported as `"mount"` and skipped by `animateOnMount: false`. */
|
|
83
83
|
mountPending: SharedValue<boolean>;
|
|
84
84
|
/** The `snapPoints` prop, string-keyed so identity churn is harmless. */
|
|
85
|
-
|
|
85
|
+
snapPointSpec: SharedValue<readonly SnapPointSpec[]>;
|
|
86
86
|
config: SharedValue<SheetWorkletConfig>;
|
|
87
87
|
};
|
|
88
88
|
|
|
@@ -93,14 +93,14 @@ export type SheetSharedState = {
|
|
|
93
93
|
export type SheetGeometry = {
|
|
94
94
|
restingBottom: SharedValue<number>;
|
|
95
95
|
closedHeight: SharedValue<number>;
|
|
96
|
-
/** The height the sheet may occupy; what a `%`
|
|
96
|
+
/** The height the sheet may occupy; what a `%` snap point resolves against. */
|
|
97
97
|
maxHeight: SharedValue<number>;
|
|
98
98
|
/** The safe-area band an attached sheet reserves. */
|
|
99
99
|
band: SharedValue<number>;
|
|
100
100
|
bandNow: SharedValue<number>;
|
|
101
|
-
/** Ascending, unique, the dynamic
|
|
102
|
-
|
|
103
|
-
/** The last
|
|
101
|
+
/** Ascending, unique, the dynamic snap point included when sizing to content. */
|
|
102
|
+
snapPoints: SharedValue<readonly number[]>;
|
|
103
|
+
/** The last snap point, or the closed height when there is none yet. */
|
|
104
104
|
highest: SharedValue<number>;
|
|
105
105
|
keyboardLift: SharedValue<number>;
|
|
106
106
|
/** `base + keyboardLift`, clamped. What the view shows. */
|
|
@@ -111,7 +111,7 @@ export type SheetGeometry = {
|
|
|
111
111
|
index: SharedValue<number>;
|
|
112
112
|
sheetState: SharedValue<SheetState>;
|
|
113
113
|
layoutReady: SharedValue<boolean>;
|
|
114
|
-
/** What the body may fill above the footer or the band, sized against its
|
|
114
|
+
/** What the body may fill above the footer or the band, sized against its snap point. */
|
|
115
115
|
contentArea: SharedValue<number>;
|
|
116
116
|
footerHeight: SharedValue<number>;
|
|
117
117
|
footerTop: SharedValue<number>;
|
|
@@ -121,12 +121,12 @@ export type SheetGeometry = {
|
|
|
121
121
|
* How tall the body's clipping box is: `contentArea + bodyInset` — the body
|
|
122
122
|
* is laid out to the sheet's bottom line — held under a sticky footer to
|
|
123
123
|
* the footer's live top edge, so nothing of the body shows below it while
|
|
124
|
-
* the sheet is lower than its
|
|
124
|
+
* the sheet is lower than its snap point.
|
|
125
125
|
*/
|
|
126
126
|
bodyClip: SharedValue<number>;
|
|
127
127
|
/**
|
|
128
|
-
* How tall a detached card's surface is: `height` between the
|
|
129
|
-
* first
|
|
128
|
+
* How tall a detached card's surface is: `height` between the snap points, the
|
|
129
|
+
* first snap point's height below them and the last's above, so a close, a
|
|
130
130
|
* rubber-band and an over-drag move the card as a rigid body. Equal to
|
|
131
131
|
* `height` when attached.
|
|
132
132
|
*/
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
surfaceHeight,
|
|
21
21
|
} from "../core";
|
|
22
22
|
import type { SheetGeometry, SheetSharedState } from "./state.types";
|
|
23
|
-
import {
|
|
23
|
+
import { useSnapPoints } from "./use-snap-points";
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Where an unmeasured panel goes: far enough down that no frame of it shows
|
|
@@ -46,9 +46,9 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
|
|
|
46
46
|
const maxHeight = useDerivedValue(() => availableHeight(state.containerHeight.value, resting.value));
|
|
47
47
|
const band = useDerivedValue(() => bottomBand(state.config.value.detached !== null, state.config.value.bottomInset));
|
|
48
48
|
const bandCurrent = useDerivedValue(() => bandNow(band.value, state.keyboardProgress.value));
|
|
49
|
-
const
|
|
49
|
+
const snapPoints = useSnapPoints(state, maxHeight, band);
|
|
50
50
|
const highest = useDerivedValue(() => {
|
|
51
|
-
const list =
|
|
51
|
+
const list = snapPoints.value;
|
|
52
52
|
return list.length > 0 ? (list[list.length - 1] as number) : closed.value;
|
|
53
53
|
});
|
|
54
54
|
// The keyboard the sheet answers to: owned, and under a behaviour that does
|
|
@@ -71,9 +71,9 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
|
|
|
71
71
|
if (!(container > 0)) return OFFSCREEN;
|
|
72
72
|
return positionFor(container, resting.value, height.value);
|
|
73
73
|
});
|
|
74
|
-
const index = useDerivedValue(() => indexForHeight(state.base.value,
|
|
74
|
+
const index = useDerivedValue(() => indexForHeight(state.base.value, snapPoints.value, closed.value));
|
|
75
75
|
const sheetStateValue = useDerivedValue(() =>
|
|
76
|
-
sheetState(state.base.value, height.value,
|
|
76
|
+
sheetState(state.base.value, height.value, snapPoints.value, closed.value, maxHeight.value)
|
|
77
77
|
);
|
|
78
78
|
const layoutReady = useDerivedValue(() =>
|
|
79
79
|
isLayoutReady({
|
|
@@ -88,9 +88,9 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
|
|
|
88
88
|
const footer = useDerivedValue(() =>
|
|
89
89
|
footerHeight(state.config.value.hasFooter, state.footerContentHeight.value, bandCurrent.value)
|
|
90
90
|
);
|
|
91
|
-
// The body is sized against the highest
|
|
92
|
-
// never reflows during a snap between
|
|
93
|
-
// takes `base` above every
|
|
91
|
+
// The body is sized against the highest snap point rather than `base`, so it
|
|
92
|
+
// never reflows during a snap between snap points — but a `fillParent` snap
|
|
93
|
+
// takes `base` above every snap point, and the body follows it up.
|
|
94
94
|
const area = useDerivedValue(() => {
|
|
95
95
|
const sheetHeight = Math.min(maxHeight.value, Math.max(highest.value, state.base.value) + lift.value);
|
|
96
96
|
return contentArea({
|
|
@@ -126,7 +126,7 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
|
|
|
126
126
|
|
|
127
127
|
const surface = useDerivedValue(() => {
|
|
128
128
|
if (state.config.value.detached === null) return height.value;
|
|
129
|
-
const list =
|
|
129
|
+
const list = snapPoints.value;
|
|
130
130
|
const lowest = list.length > 0 ? (list[0] as number) : 0;
|
|
131
131
|
return surfaceHeight(height.value, lowest, highest.value);
|
|
132
132
|
});
|
|
@@ -138,7 +138,7 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
|
|
|
138
138
|
maxHeight,
|
|
139
139
|
band,
|
|
140
140
|
bandNow: bandCurrent,
|
|
141
|
-
|
|
141
|
+
snapPoints,
|
|
142
142
|
highest,
|
|
143
143
|
keyboardLift: lift,
|
|
144
144
|
height,
|
|
@@ -159,7 +159,7 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
|
|
|
159
159
|
maxHeight,
|
|
160
160
|
band,
|
|
161
161
|
bandCurrent,
|
|
162
|
-
|
|
162
|
+
snapPoints,
|
|
163
163
|
highest,
|
|
164
164
|
lift,
|
|
165
165
|
height,
|