@delacour/react-native-bottom-sheet 0.1.0-alpha.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 +132 -0
- package/package.json +57 -0
- package/src/animation/AGENTS.md +58 -0
- package/src/animation/animation.types.ts +35 -0
- package/src/animation/resolve-animation.ts +75 -0
- package/src/animation/use-animate-to.ts +107 -0
- package/src/animation/use-settle-callbacks.ts +60 -0
- package/src/components/AGENTS.md +80 -0
- package/src/components/bottom-sheet-background.tsx +34 -0
- package/src/components/bottom-sheet-close.tsx +45 -0
- package/src/components/bottom-sheet-container.tsx +79 -0
- package/src/components/bottom-sheet-content.tsx +102 -0
- package/src/components/bottom-sheet-description.tsx +33 -0
- package/src/components/bottom-sheet-footer.tsx +97 -0
- package/src/components/bottom-sheet-handle.tsx +72 -0
- package/src/components/bottom-sheet-overlay.tsx +91 -0
- package/src/components/bottom-sheet-portal.tsx +96 -0
- package/src/components/bottom-sheet-text-input.tsx +36 -0
- package/src/components/bottom-sheet-title.tsx +30 -0
- package/src/components/bottom-sheet-trigger.tsx +51 -0
- package/src/components/bottom-sheet.context.tsx +134 -0
- package/src/components/bottom-sheet.tsx +475 -0
- package/src/components/bottom-sheet.types.ts +235 -0
- package/src/core/AGENTS.md +160 -0
- package/src/core/animation/select-animation.test.ts +57 -0
- package/src/core/animation/select-animation.ts +104 -0
- package/src/core/backdrop/backdrop-opacity.test.ts +49 -0
- package/src/core/backdrop/backdrop-opacity.ts +21 -0
- package/src/core/detents/dynamic-detent.test.ts +40 -0
- package/src/core/detents/dynamic-detent.ts +41 -0
- package/src/core/detents/index-for-height.test.ts +65 -0
- package/src/core/detents/index-for-height.ts +44 -0
- package/src/core/detents/normalize-detents.test.ts +72 -0
- package/src/core/detents/normalize-detents.ts +63 -0
- package/src/core/detents/over-drag.test.ts +46 -0
- package/src/core/detents/over-drag.ts +23 -0
- package/src/core/detents/select-snap-height.test.ts +52 -0
- package/src/core/detents/select-snap-height.ts +44 -0
- package/src/core/detents/sheet-state.test.ts +42 -0
- package/src/core/detents/sheet-state.ts +32 -0
- package/src/core/footer/body-inset.test.ts +95 -0
- package/src/core/footer/body-inset.ts +65 -0
- package/src/core/footer/bottom-band.test.ts +46 -0
- package/src/core/footer/bottom-band.ts +30 -0
- package/src/core/footer/footer-top.test.ts +61 -0
- package/src/core/footer/footer-top.ts +27 -0
- package/src/core/footer/sheet-insets.test.ts +27 -0
- package/src/core/footer/sheet-insets.ts +30 -0
- package/src/core/geometry/clamp-height.test.ts +25 -0
- package/src/core/geometry/clamp-height.ts +12 -0
- package/src/core/geometry/closed-height.test.ts +37 -0
- package/src/core/geometry/closed-height.ts +29 -0
- package/src/core/geometry/detached-frame.test.ts +50 -0
- package/src/core/geometry/detached-frame.ts +49 -0
- package/src/core/geometry/position.test.ts +21 -0
- package/src/core/geometry/position.ts +13 -0
- package/src/core/geometry/surface-height.test.ts +30 -0
- package/src/core/geometry/surface-height.ts +22 -0
- package/src/core/haptic/crossed-detent.test.ts +43 -0
- package/src/core/haptic/crossed-detent.ts +33 -0
- package/src/core/index.ts +92 -0
- package/src/core/intent/layout-ready.test.ts +37 -0
- package/src/core/intent/layout-ready.ts +24 -0
- package/src/core/intent/resolve-intent.test.ts +163 -0
- package/src/core/intent/resolve-intent.ts +98 -0
- package/src/core/keyboard/container-layout-guard.test.ts +32 -0
- package/src/core/keyboard/container-layout-guard.ts +31 -0
- package/src/core/keyboard/content-area.test.ts +34 -0
- package/src/core/keyboard/content-area.ts +26 -0
- package/src/core/keyboard/keyboard-lift.test.ts +54 -0
- package/src/core/keyboard/keyboard-lift.ts +47 -0
- package/src/core/keyboard/keyboard-owner.test.ts +35 -0
- package/src/core/keyboard/keyboard-owner.ts +25 -0
- package/src/core/keyboard/keyboard-ownership.test.ts +69 -0
- package/src/core/keyboard/keyboard-ownership.ts +39 -0
- package/src/core/keyboard/keyboard-reset-guard.test.ts +33 -0
- package/src/core/keyboard/keyboard-reset-guard.ts +33 -0
- package/src/core/keyboard/keyboard-step.test.ts +66 -0
- package/src/core/keyboard/keyboard-step.ts +53 -0
- package/src/core/machine/define-sheet-machine.test.ts +187 -0
- package/src/core/machine/define-sheet-machine.ts +35 -0
- package/src/core/machine/index.ts +23 -0
- package/src/core/machine/machine.types.ts +80 -0
- package/src/core/machine/step-transition.test.ts +46 -0
- package/src/core/machine/step-transition.ts +59 -0
- package/src/core/machine/transition.test.ts +51 -0
- package/src/core/machine/transition.ts +58 -0
- package/src/core/result.test.ts +15 -0
- package/src/core/result.ts +20 -0
- package/src/core/scroll/scroll-lock.test.ts +40 -0
- package/src/core/scroll/scroll-lock.ts +35 -0
- package/src/core/scroll/scroll-pan.test.ts +99 -0
- package/src/core/scroll/scroll-pan.ts +96 -0
- package/src/core/sheet.types.ts +93 -0
- package/src/display-name.test.ts +181 -0
- package/src/docs.test.ts +45 -0
- package/src/flat-worklet.test.ts +130 -0
- package/src/gesture/AGENTS.md +71 -0
- package/src/gesture/gesture.types.ts +37 -0
- package/src/gesture/use-scroll-lock.ts +162 -0
- package/src/gesture/use-sheet-pan.ts +250 -0
- package/src/index.ts +127 -0
- package/src/keyboard/AGENTS.md +84 -0
- package/src/keyboard/use-bottom-sheet-text-input.ts +79 -0
- package/src/keyboard/use-keyboard-animation-guard.ts +59 -0
- package/src/keyboard/use-sheet-keyboard.ts +163 -0
- package/src/layout/AGENTS.md +52 -0
- package/src/layout/use-container-layout.ts +63 -0
- package/src/layout/use-measure-height.ts +40 -0
- package/src/lib/AGENTS.md +35 -0
- package/src/lib/compose-refs.ts +37 -0
- package/src/lib/merge-props.ts +42 -0
- package/src/lib/slot.tsx +44 -0
- package/src/lib/use-controllable-state.ts +45 -0
- package/src/no-classname.test.ts +27 -0
- package/src/portal/AGENTS.md +101 -0
- package/src/portal/bottom-sheet-host.tsx +67 -0
- package/src/portal/bottom-sheet-provider.tsx +33 -0
- package/src/portal/host.context.tsx +25 -0
- package/src/portal/sheet-registry.context.tsx +124 -0
- package/src/portal/sheet-registry.test.ts +87 -0
- package/src/portal/sheet-registry.ts +78 -0
- package/src/purity.test.ts +90 -0
- package/src/scrollable/AGENTS.md +123 -0
- package/src/scrollable/bottom-sheet-flat-list.tsx +15 -0
- package/src/scrollable/bottom-sheet-scroll-view.tsx +15 -0
- package/src/scrollable/bottom-sheet-section-list.tsx +22 -0
- package/src/scrollable/create-bottom-sheet-scrollable.tsx +248 -0
- package/src/scrollable/scrollable.types.ts +76 -0
- package/src/source-tree.test.ts +33 -0
- package/src/state/AGENTS.md +63 -0
- package/src/state/state.types.ts +134 -0
- package/src/state/use-detents.ts +43 -0
- package/src/state/use-sheet-geometry.ts +178 -0
- package/src/state/use-sheet-intents.ts +144 -0
- package/src/state/use-sheet-state.ts +157 -0
- package/src/steps/AGENTS.md +100 -0
- package/src/steps/bottom-sheet-step.tsx +61 -0
- package/src/steps/bottom-sheet-steps.tsx +215 -0
- package/src/steps/steps.context.tsx +73 -0
- package/src/steps/steps.types.ts +61 -0
- package/src/steps/use-sheet-machine.ts +70 -0
- package/src/steps/use-sheet-step.ts +29 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { Insets, NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps } from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
type AnimatedRef,
|
|
5
|
+
type SharedValue,
|
|
6
|
+
scrollTo,
|
|
7
|
+
useAnimatedProps,
|
|
8
|
+
useAnimatedReaction,
|
|
9
|
+
useAnimatedScrollHandler,
|
|
10
|
+
useDerivedValue,
|
|
11
|
+
} from "react-native-reanimated";
|
|
12
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
13
|
+
import { GESTURE_SOURCE, scrollLockTarget, shouldLockScroll } from "../core";
|
|
14
|
+
import type { ScrollableHandle } from "../scrollable/scrollable.types";
|
|
15
|
+
import type { SheetGeometry, SheetSharedState } from "../state/state.types";
|
|
16
|
+
|
|
17
|
+
export type ScrollLockOptions = {
|
|
18
|
+
/** The root's `enableContentPanningGesture`. Off, the list is never locked — nothing else could take the drag. */
|
|
19
|
+
enableContentPan: boolean;
|
|
20
|
+
/** The caller's `showsVerticalScrollIndicator`; shown only while unlocked. @default true */
|
|
21
|
+
showsVerticalScrollIndicator?: boolean;
|
|
22
|
+
/** The caller's `scrollIndicatorInsets`; `indicatorInset` is added to its `bottom`. */
|
|
23
|
+
scrollIndicatorInsets?: Insets;
|
|
24
|
+
/** What trails the body — the geometry's `bodyInset` — so the indicator ends above the footer or the band. */
|
|
25
|
+
indicatorInset?: SharedValue<number>;
|
|
26
|
+
/** The caller's `bounces`; on only while unlocked. @default true */
|
|
27
|
+
bounces?: boolean;
|
|
28
|
+
/** The caller's `decelerationRate`; `0` while locked. @default "normal" */
|
|
29
|
+
decelerationRate?: ScrollViewProps["decelerationRate"];
|
|
30
|
+
/** The caller's own scroll callbacks, called on the JS thread with the event the worklet saw. */
|
|
31
|
+
listeners?: ScrollListeners;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type ScrollListener = (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
35
|
+
|
|
36
|
+
export type ScrollListeners = Pick<
|
|
37
|
+
{ [K in keyof ScrollViewProps]?: ScrollListener },
|
|
38
|
+
"onScroll" | "onScrollBeginDrag" | "onScrollEndDrag" | "onMomentumScrollEnd"
|
|
39
|
+
>;
|
|
40
|
+
|
|
41
|
+
type ScrollLockProps = Pick<
|
|
42
|
+
ScrollViewProps,
|
|
43
|
+
"decelerationRate" | "showsVerticalScrollIndicator" | "bounces" | "scrollIndicatorInsets"
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
export type ScrollLock = {
|
|
47
|
+
/** `true` while the sheet is below its highest detent: the list is held at `scrollLockedAt`. */
|
|
48
|
+
locked: SharedValue<boolean>;
|
|
49
|
+
/** For the list's `onScroll`. Tracks the offset into `scrollOffsetY` and enforces the lock. */
|
|
50
|
+
scrollHandler: ReturnType<typeof useAnimatedScrollHandler>;
|
|
51
|
+
/** For the list's `animatedProps`: deceleration, indicator and bounce, switched by the lock, and the indicator's inset. */
|
|
52
|
+
animatedProps: Partial<ScrollLockProps>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The scroll lock: the UI-thread half of a scrollable body.
|
|
57
|
+
*
|
|
58
|
+
* Below the highest detent a drag on the list must move the sheet, not the
|
|
59
|
+
* rows. The content pan already does the moving — the native scroll and the
|
|
60
|
+
* pan are simultaneous — so this hook's job is to make sure the list does not
|
|
61
|
+
* *also* move: every scroll event while locked is answered with a `scrollTo`
|
|
62
|
+
* back to where the lock engaged, and the offset the pan reads is pinned
|
|
63
|
+
* there too, so the pan sees the list consume nothing.
|
|
64
|
+
*
|
|
65
|
+
* The lock engages when the sheet leaves its highest detent by any path — a
|
|
66
|
+
* drag, a `snapToIndex`, a keyboard — and takes the offset of that moment as
|
|
67
|
+
* its target. A list scrolled a hundred pixels, then collapsed by the handle,
|
|
68
|
+
* holds those hundred pixels rather than jumping to the top; when the sheet
|
|
69
|
+
* comes back up it continues from there.
|
|
70
|
+
*
|
|
71
|
+
* The animated props do the rest: `decelerationRate: 0` while locked so a
|
|
72
|
+
* fling has no momentum for the lock to fight, the indicator hidden so a
|
|
73
|
+
* pinned list does not flash a bar, and `bounces` off so a pull-down at the
|
|
74
|
+
* top rubber-bands the sheet and not the rows. The indicator's bottom inset
|
|
75
|
+
* follows `indicatorInset` — what the body reserves for the footer or the
|
|
76
|
+
* band — so the bar ends where the rows stop being visible.
|
|
77
|
+
*/
|
|
78
|
+
export function useScrollLock(
|
|
79
|
+
state: SheetSharedState,
|
|
80
|
+
geometry: SheetGeometry,
|
|
81
|
+
ref: AnimatedRef<ScrollableHandle>,
|
|
82
|
+
options: ScrollLockOptions
|
|
83
|
+
): ScrollLock {
|
|
84
|
+
const {
|
|
85
|
+
enableContentPan,
|
|
86
|
+
showsVerticalScrollIndicator = true,
|
|
87
|
+
scrollIndicatorInsets,
|
|
88
|
+
indicatorInset,
|
|
89
|
+
bounces = true,
|
|
90
|
+
decelerationRate = "normal",
|
|
91
|
+
listeners,
|
|
92
|
+
} = options;
|
|
93
|
+
const onScroll = listeners?.onScroll;
|
|
94
|
+
const onScrollBeginDrag = listeners?.onScrollBeginDrag;
|
|
95
|
+
const onScrollEndDrag = listeners?.onScrollEndDrag;
|
|
96
|
+
const onMomentumScrollEnd = listeners?.onMomentumScrollEnd;
|
|
97
|
+
const { scrollOffsetY, scrollLockedAt } = state;
|
|
98
|
+
const sheetState = geometry.sheetState;
|
|
99
|
+
|
|
100
|
+
const locked = useDerivedValue(() => shouldLockScroll(sheetState.value, enableContentPan));
|
|
101
|
+
|
|
102
|
+
useAnimatedReaction(
|
|
103
|
+
() => locked.value,
|
|
104
|
+
(now, before) => {
|
|
105
|
+
if (now && before !== true) {
|
|
106
|
+
scrollLockedAt.value = scrollLockTarget(
|
|
107
|
+
scrollOffsetY.value,
|
|
108
|
+
state.gestureSource.value === GESTURE_SOURCE.CONTENT
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const scrollHandler = useAnimatedScrollHandler(
|
|
115
|
+
useMemo(() => {
|
|
116
|
+
const track = (event: NativeScrollEvent, listener: ScrollListener | undefined): void => {
|
|
117
|
+
"worklet";
|
|
118
|
+
if (listener) scheduleOnRN(listener, { nativeEvent: event } as NativeSyntheticEvent<NativeScrollEvent>);
|
|
119
|
+
if (locked.value) {
|
|
120
|
+
const target = scrollLockedAt.value;
|
|
121
|
+
scrollTo(ref, 0, target, false);
|
|
122
|
+
scrollOffsetY.value = target;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
scrollOffsetY.value = event.contentOffset.y;
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
onScroll: (event) => {
|
|
129
|
+
"worklet";
|
|
130
|
+
track(event, onScroll);
|
|
131
|
+
},
|
|
132
|
+
onBeginDrag: (event) => {
|
|
133
|
+
"worklet";
|
|
134
|
+
track(event, onScrollBeginDrag);
|
|
135
|
+
},
|
|
136
|
+
onEndDrag: (event) => {
|
|
137
|
+
"worklet";
|
|
138
|
+
track(event, onScrollEndDrag);
|
|
139
|
+
},
|
|
140
|
+
onMomentumEnd: (event) => {
|
|
141
|
+
"worklet";
|
|
142
|
+
track(event, onMomentumScrollEnd);
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}, [locked, scrollLockedAt, scrollOffsetY, ref, onScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollEnd])
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const indicatorBottom = scrollIndicatorInsets?.bottom ?? 0;
|
|
149
|
+
const animatedProps = useAnimatedProps<ScrollLockProps>(() => ({
|
|
150
|
+
decelerationRate: locked.value ? 0 : decelerationRate,
|
|
151
|
+
showsVerticalScrollIndicator: showsVerticalScrollIndicator && !locked.value,
|
|
152
|
+
bounces: bounces && !locked.value,
|
|
153
|
+
scrollIndicatorInsets: {
|
|
154
|
+
top: scrollIndicatorInsets?.top,
|
|
155
|
+
left: scrollIndicatorInsets?.left,
|
|
156
|
+
right: scrollIndicatorInsets?.right,
|
|
157
|
+
bottom: indicatorBottom + (indicatorInset ? indicatorInset.value : 0),
|
|
158
|
+
},
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
return useMemo(() => ({ locked, scrollHandler, animatedProps }), [locked, scrollHandler, animatedProps]);
|
|
162
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Gesture, type GestureType, type PanGesture } from "react-native-gesture-handler";
|
|
3
|
+
import { KeyboardController } from "react-native-keyboard-controller";
|
|
4
|
+
import { cancelAnimation, useSharedValue } from "react-native-reanimated";
|
|
5
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
6
|
+
import type { AnimateTo, SettleAt } from "../animation/animation.types";
|
|
7
|
+
import {
|
|
8
|
+
crossedDetent,
|
|
9
|
+
detentUnder,
|
|
10
|
+
GESTURE_SOURCE,
|
|
11
|
+
type GestureSource,
|
|
12
|
+
listDragHeight,
|
|
13
|
+
listOwnsRelease,
|
|
14
|
+
resistOverDrag,
|
|
15
|
+
restingDetent,
|
|
16
|
+
SCROLLABLE_TYPE,
|
|
17
|
+
selectSnapHeight,
|
|
18
|
+
} from "../core";
|
|
19
|
+
import { ANIM_STATUS, type SheetGeometry, type SheetSharedState } from "../state/state.types";
|
|
20
|
+
import {
|
|
21
|
+
CONTENT_ACTIVE_OFFSET_Y,
|
|
22
|
+
CONTENT_FAIL_OFFSET_X,
|
|
23
|
+
type SheetPanOptions,
|
|
24
|
+
type SheetPans,
|
|
25
|
+
SNAP_PROJECTION,
|
|
26
|
+
} from "./gesture.types";
|
|
27
|
+
|
|
28
|
+
type PanEvent = { translationY: number; velocityY: number };
|
|
29
|
+
|
|
30
|
+
/** Settle tolerance: a sheet within this of a detent is on it. */
|
|
31
|
+
const AT_DETENT = 0.5;
|
|
32
|
+
|
|
33
|
+
/** JS-thread, so `scheduleOnRN` has a plain function to call; `dismiss` returns a promise nobody awaits. */
|
|
34
|
+
function dismissKeyboard(): void {
|
|
35
|
+
void KeyboardController.dismiss();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The two pans — handle and content — sharing one set of handlers.
|
|
40
|
+
*
|
|
41
|
+
* Everything runs on the UI thread. `onStart` cancels whatever animation owns
|
|
42
|
+
* `base` and remembers where the drag began; `onChange` turns the finger's
|
|
43
|
+
* travel into a height, rubber-banded past the detents by `resistOverDrag`
|
|
44
|
+
* and never above the container; `onFinalize` picks the detent the finger was
|
|
45
|
+
* heading for with `selectSnapHeight` and hands it to `animateTo` with the
|
|
46
|
+
* release velocity. The close is a candidate only when `enablePanDownToClose`.
|
|
47
|
+
*
|
|
48
|
+
* A content pan the list owns at release — the sheet at its top, the list
|
|
49
|
+
* scrolled — does not snap, but it still settles: the finger moved `base` by
|
|
50
|
+
* hand, so no animation ran and nothing else writes `currentIndex`. `settleAt`
|
|
51
|
+
* on the detent under `base` is the bookkeeping of a finished animation
|
|
52
|
+
* without the motion; between detents (which the list ownership test should
|
|
53
|
+
* rule out) the sheet snaps to the nearest one with no velocity instead, since
|
|
54
|
+
* the release velocity is the list's momentum.
|
|
55
|
+
*
|
|
56
|
+
* The content pan waits six points of vertical travel before it claims a
|
|
57
|
+
* touch and gives up after twelve horizontal, so a tap lands on the button or
|
|
58
|
+
* the field under it and a horizontal pager inside the sheet keeps its swipe.
|
|
59
|
+
* The handle pan claims immediately — nothing under a grabber wants the touch.
|
|
60
|
+
*
|
|
61
|
+
* The handlers are built inside this `useMemo`, so they close over the core's
|
|
62
|
+
* worklets in the ordinary way. See the package `AGENTS.md`: that is legal
|
|
63
|
+
* precisely because these are not module-scope worklets.
|
|
64
|
+
*
|
|
65
|
+
* Per-gesture memory — where the drag began, the detent last under it,
|
|
66
|
+
* whether it is over-dragging — lives in shared values rather than closure
|
|
67
|
+
* variables. Each worklet gets its own copy of a captured `let`, so a write in
|
|
68
|
+
* `onStart` would never be seen by `onChange`.
|
|
69
|
+
*/
|
|
70
|
+
export function useSheetPan(
|
|
71
|
+
state: SheetSharedState,
|
|
72
|
+
geometry: SheetGeometry,
|
|
73
|
+
animateTo: AnimateTo,
|
|
74
|
+
settleAt: SettleAt,
|
|
75
|
+
options: SheetPanOptions
|
|
76
|
+
): SheetPans {
|
|
77
|
+
const startBase = useSharedValue(0);
|
|
78
|
+
const lastDetent = useSharedValue(-1);
|
|
79
|
+
const overDragging = useSharedValue(false);
|
|
80
|
+
const startScroll = useSharedValue(0);
|
|
81
|
+
const listHeld = useSharedValue(false);
|
|
82
|
+
const { enableHandlePanningGesture, enableContentPanningGesture, onDetentHaptic, onCloseHaptic, onOverDragHaptic } =
|
|
83
|
+
options;
|
|
84
|
+
|
|
85
|
+
return useMemo(() => {
|
|
86
|
+
const begin = (source: GestureSource): void => {
|
|
87
|
+
"worklet";
|
|
88
|
+
cancelAnimation(state.base);
|
|
89
|
+
state.animStatus.value = ANIM_STATUS.IDLE;
|
|
90
|
+
state.gestureSource.value = source;
|
|
91
|
+
startBase.value = state.base.value;
|
|
92
|
+
startScroll.value = state.scrollOffsetY.value;
|
|
93
|
+
// 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 detent.
|
|
95
|
+
listHeld.value = state.scrollOffsetY.value > 0 && state.base.value < geometry.highest.value - AT_DETENT;
|
|
96
|
+
lastDetent.value = detentUnder(state.base.value, geometry.detents.value);
|
|
97
|
+
overDragging.value = false;
|
|
98
|
+
// A finger on the sheet is a reason to put the keyboard away; the lift
|
|
99
|
+
// comes off as it goes and the drag continues from wherever `base` is.
|
|
100
|
+
if (state.config.value.enableBlurKeyboardOnGesture && state.keyboardProgress.value > 0) {
|
|
101
|
+
scheduleOnRN(dismissKeyboard);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const haptics = (raw: number, next: number, lowest: number, highest: number, detents: readonly number[]): void => {
|
|
106
|
+
"worklet";
|
|
107
|
+
const over = raw > highest || raw < lowest;
|
|
108
|
+
if (over && !overDragging.value && onOverDragHaptic) onOverDragHaptic();
|
|
109
|
+
overDragging.value = over;
|
|
110
|
+
|
|
111
|
+
if (crossedDetent(lastDetent.value, next, detents)) {
|
|
112
|
+
lastDetent.value = detentUnder(next, detents);
|
|
113
|
+
if (onDetentHaptic) onDetentHaptic();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Whether this pan is a content pan over a registered scrollable.
|
|
118
|
+
const overList = (source: GestureSource): boolean => {
|
|
119
|
+
"worklet";
|
|
120
|
+
return source === GESTURE_SOURCE.CONTENT && state.scrollableType.value !== SCROLLABLE_TYPE.NONE;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// The finger's travel as a height. Over a list the offset the list began
|
|
124
|
+
// with is a budget spent before the sheet moves — see `listDragHeight`.
|
|
125
|
+
const dragged = (translationY: number, highest: number): number => {
|
|
126
|
+
"worklet";
|
|
127
|
+
if (!overList(state.gestureSource.value)) return startBase.value - translationY;
|
|
128
|
+
return listDragHeight({
|
|
129
|
+
startBase: startBase.value,
|
|
130
|
+
translationY,
|
|
131
|
+
startOffset: startScroll.value,
|
|
132
|
+
held: listHeld.value,
|
|
133
|
+
highest,
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const move = (event: PanEvent): void => {
|
|
138
|
+
"worklet";
|
|
139
|
+
if (state.gestureSource.value === GESTURE_SOURCE.NONE) return;
|
|
140
|
+
const config = state.config.value;
|
|
141
|
+
const detents = geometry.detents.value;
|
|
142
|
+
const closed = geometry.closedHeight.value;
|
|
143
|
+
const highest = geometry.highest.value;
|
|
144
|
+
const raw = dragged(event.translationY, highest);
|
|
145
|
+
|
|
146
|
+
const lowest = config.enablePanDownToClose || detents.length === 0 ? closed : (detents[0] as number);
|
|
147
|
+
const factor = config.enableOverDrag ? config.overDragResistanceFactor : 0;
|
|
148
|
+
const next = Math.min(resistOverDrag(raw, lowest, highest, factor), geometry.maxHeight.value);
|
|
149
|
+
|
|
150
|
+
haptics(raw, next, lowest, highest, detents);
|
|
151
|
+
state.base.value = next;
|
|
152
|
+
if (listHeld.value && next >= highest - AT_DETENT) listHeld.value = false;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// The haptic for a close, the keyboard for a downward release — a release
|
|
156
|
+
// heading down under an open keyboard puts the keyboard away whatever
|
|
157
|
+
// `enableBlurKeyboardOnGesture` says — then the animation.
|
|
158
|
+
const settle = (target: number, closed: number, velocity: number): void => {
|
|
159
|
+
"worklet";
|
|
160
|
+
if (target <= closed && onCloseHaptic) onCloseHaptic();
|
|
161
|
+
if (target < startBase.value && state.keyboardProgress.value > 0) scheduleOnRN(dismissKeyboard);
|
|
162
|
+
animateTo(target, "gesture", velocity / 2);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// A release the list owns settles the sheet on the detent under `base`
|
|
166
|
+
// with no motion; `false` when it sits between detents and has to snap.
|
|
167
|
+
const settleResting = (): boolean => {
|
|
168
|
+
"worklet";
|
|
169
|
+
const resting = restingDetent(state.base.value, geometry.detents.value);
|
|
170
|
+
if (resting === null) return false;
|
|
171
|
+
settleAt(resting, "gesture");
|
|
172
|
+
return true;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const release = (event: PanEvent): void => {
|
|
176
|
+
"worklet";
|
|
177
|
+
if (state.gestureSource.value === GESTURE_SOURCE.NONE) return;
|
|
178
|
+
const source = state.gestureSource.value;
|
|
179
|
+
state.gestureSource.value = GESTURE_SOURCE.NONE;
|
|
180
|
+
const config = state.config.value;
|
|
181
|
+
const closed = geometry.closedHeight.value;
|
|
182
|
+
|
|
183
|
+
// The finger was scrolling rows at the top detent: the release is the
|
|
184
|
+
// list's momentum, not a snap — but the sheet still settles where it is.
|
|
185
|
+
const listOwns = listOwnsRelease({
|
|
186
|
+
scrollable: overList(source),
|
|
187
|
+
offset: state.scrollOffsetY.value,
|
|
188
|
+
base: state.base.value,
|
|
189
|
+
highest: geometry.highest.value,
|
|
190
|
+
});
|
|
191
|
+
if (listOwns && settleResting()) return;
|
|
192
|
+
|
|
193
|
+
// A list-owned release between detents (which `listOwnsRelease` should
|
|
194
|
+
// rule out) snaps to the nearest with no velocity and never closes.
|
|
195
|
+
const velocity = listOwns ? 0 : -event.velocityY;
|
|
196
|
+
const target = selectSnapHeight({
|
|
197
|
+
height: state.base.value,
|
|
198
|
+
velocity,
|
|
199
|
+
detents: geometry.detents.value,
|
|
200
|
+
closedHeight: config.enablePanDownToClose && !listOwns ? closed : null,
|
|
201
|
+
projection: SNAP_PROJECTION,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
settle(target, closed, velocity);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const attach = (pan: PanGesture, source: GestureSource, enabled: boolean): GestureType =>
|
|
208
|
+
pan
|
|
209
|
+
.enabled(enabled)
|
|
210
|
+
.shouldCancelWhenOutside(false)
|
|
211
|
+
.onStart(() => {
|
|
212
|
+
"worklet";
|
|
213
|
+
begin(source);
|
|
214
|
+
})
|
|
215
|
+
.onChange((event) => {
|
|
216
|
+
"worklet";
|
|
217
|
+
move(event);
|
|
218
|
+
})
|
|
219
|
+
.onFinalize((event) => {
|
|
220
|
+
"worklet";
|
|
221
|
+
release(event);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const handle = attach(Gesture.Pan(), GESTURE_SOURCE.HANDLE, enableHandlePanningGesture);
|
|
225
|
+
const content = attach(
|
|
226
|
+
Gesture.Pan()
|
|
227
|
+
.activeOffsetY([-CONTENT_ACTIVE_OFFSET_Y, CONTENT_ACTIVE_OFFSET_Y])
|
|
228
|
+
.failOffsetX([-CONTENT_FAIL_OFFSET_X, CONTENT_FAIL_OFFSET_X]),
|
|
229
|
+
GESTURE_SOURCE.CONTENT,
|
|
230
|
+
enableContentPanningGesture
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return { handle, content };
|
|
234
|
+
}, [
|
|
235
|
+
state,
|
|
236
|
+
geometry,
|
|
237
|
+
animateTo,
|
|
238
|
+
settleAt,
|
|
239
|
+
startBase,
|
|
240
|
+
startScroll,
|
|
241
|
+
listHeld,
|
|
242
|
+
lastDetent,
|
|
243
|
+
overDragging,
|
|
244
|
+
enableHandlePanningGesture,
|
|
245
|
+
enableContentPanningGesture,
|
|
246
|
+
onDetentHaptic,
|
|
247
|
+
onCloseHaptic,
|
|
248
|
+
onOverDragHaptic,
|
|
249
|
+
]);
|
|
250
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export type { AnimateListener, AnimateTo, JumpTo, SettleListener, SheetAnimation } from "./animation/animation.types";
|
|
2
|
+
export { easingFor, reduceMotionFor, toReanimated } from "./animation/resolve-animation";
|
|
3
|
+
export { useAnimateTo } from "./animation/use-animate-to";
|
|
4
|
+
export { BottomSheet } from "./components/bottom-sheet";
|
|
5
|
+
export {
|
|
6
|
+
BottomSheetAnimatedContext,
|
|
7
|
+
BottomSheetContext,
|
|
8
|
+
BottomSheetInternalContext,
|
|
9
|
+
type BottomSheetInternalValue,
|
|
10
|
+
type ContentHeightSource,
|
|
11
|
+
useBottomSheet,
|
|
12
|
+
useBottomSheetAnimated,
|
|
13
|
+
useBottomSheetInternal,
|
|
14
|
+
useOptionalBottomSheet,
|
|
15
|
+
useOptionalBottomSheetAnimated,
|
|
16
|
+
useOptionalBottomSheetInternal,
|
|
17
|
+
} from "./components/bottom-sheet.context";
|
|
18
|
+
export type {
|
|
19
|
+
BottomSheetAnimatedValue,
|
|
20
|
+
BottomSheetBackgroundProps,
|
|
21
|
+
BottomSheetCloseProps,
|
|
22
|
+
BottomSheetContainerProps,
|
|
23
|
+
BottomSheetContentProps,
|
|
24
|
+
BottomSheetContextValue,
|
|
25
|
+
BottomSheetDescriptionProps,
|
|
26
|
+
BottomSheetFooterProps,
|
|
27
|
+
BottomSheetHandleProps,
|
|
28
|
+
BottomSheetOverlayProps,
|
|
29
|
+
BottomSheetPortalProps,
|
|
30
|
+
BottomSheetProps,
|
|
31
|
+
BottomSheetRef,
|
|
32
|
+
BottomSheetTextInputProps,
|
|
33
|
+
BottomSheetTitleProps,
|
|
34
|
+
BottomSheetTriggerProps,
|
|
35
|
+
} from "./components/bottom-sheet.types";
|
|
36
|
+
export * from "./core";
|
|
37
|
+
export type { HapticWorklet, SheetHaptics, SheetPanOptions, SheetPans } from "./gesture/gesture.types";
|
|
38
|
+
export {
|
|
39
|
+
type ScrollListeners,
|
|
40
|
+
type ScrollLock,
|
|
41
|
+
type ScrollLockOptions,
|
|
42
|
+
useScrollLock,
|
|
43
|
+
} from "./gesture/use-scroll-lock";
|
|
44
|
+
export { useSheetPan } from "./gesture/use-sheet-pan";
|
|
45
|
+
export { type BottomSheetTextInputHandlers, useBottomSheetTextInput } from "./keyboard/use-bottom-sheet-text-input";
|
|
46
|
+
export {
|
|
47
|
+
type KeyboardAnimationValues,
|
|
48
|
+
reconcileKeyboardAnimation,
|
|
49
|
+
useKeyboardAnimationGuard,
|
|
50
|
+
} from "./keyboard/use-keyboard-animation-guard";
|
|
51
|
+
export {
|
|
52
|
+
type SheetKeyboardRegistry,
|
|
53
|
+
type UseSheetKeyboardOptions,
|
|
54
|
+
useSheetKeyboard,
|
|
55
|
+
} from "./keyboard/use-sheet-keyboard";
|
|
56
|
+
export { type ContainerLayout, useContainerLayout } from "./layout/use-container-layout";
|
|
57
|
+
export { useMeasureHeight } from "./layout/use-measure-height";
|
|
58
|
+
export { Slot } from "./lib/slot";
|
|
59
|
+
export { useControllableState } from "./lib/use-controllable-state";
|
|
60
|
+
export { BottomSheetHost, type BottomSheetHostProps } from "./portal/bottom-sheet-host";
|
|
61
|
+
export { BottomSheetProvider, type BottomSheetProviderProps } from "./portal/bottom-sheet-provider";
|
|
62
|
+
export { useBottomSheetHostName, useOptionalBottomSheetHostName } from "./portal/host.context";
|
|
63
|
+
export {
|
|
64
|
+
INITIAL_REGISTRY,
|
|
65
|
+
isTop,
|
|
66
|
+
reduceRegistry,
|
|
67
|
+
type SheetRegistryAction,
|
|
68
|
+
type SheetRegistryEntry,
|
|
69
|
+
type SheetRegistryResult,
|
|
70
|
+
type SheetRegistryState,
|
|
71
|
+
type StackBehavior,
|
|
72
|
+
topOf,
|
|
73
|
+
zIndexOf,
|
|
74
|
+
} from "./portal/sheet-registry";
|
|
75
|
+
export {
|
|
76
|
+
type BottomSheetRegistryValue,
|
|
77
|
+
useBottomSheetRegistry,
|
|
78
|
+
useOptionalBottomSheetRegistry,
|
|
79
|
+
} from "./portal/sheet-registry.context";
|
|
80
|
+
export { BottomSheetFlatList } from "./scrollable/bottom-sheet-flat-list";
|
|
81
|
+
export { BottomSheetScrollView } from "./scrollable/bottom-sheet-scroll-view";
|
|
82
|
+
export { BottomSheetSectionList } from "./scrollable/bottom-sheet-section-list";
|
|
83
|
+
export { createBottomSheetScrollable } from "./scrollable/create-bottom-sheet-scrollable";
|
|
84
|
+
export type {
|
|
85
|
+
BottomSheetFlatListComponent,
|
|
86
|
+
BottomSheetFlatListProps,
|
|
87
|
+
BottomSheetScrollableProps,
|
|
88
|
+
BottomSheetScrollViewComponent,
|
|
89
|
+
BottomSheetScrollViewProps,
|
|
90
|
+
BottomSheetSectionListComponent,
|
|
91
|
+
BottomSheetSectionListProps,
|
|
92
|
+
FocusHook,
|
|
93
|
+
ScrollableHandle,
|
|
94
|
+
ScrollableInnerComponent,
|
|
95
|
+
ScrollableInnerProps,
|
|
96
|
+
} from "./scrollable/scrollable.types";
|
|
97
|
+
export {
|
|
98
|
+
ANIM_STATUS,
|
|
99
|
+
type AnimStatus,
|
|
100
|
+
type SheetGeometry,
|
|
101
|
+
type SheetSharedState,
|
|
102
|
+
type SheetWorkletConfig,
|
|
103
|
+
} from "./state/state.types";
|
|
104
|
+
export { useSheetGeometry } from "./state/use-sheet-geometry";
|
|
105
|
+
export {
|
|
106
|
+
type IntentRequest,
|
|
107
|
+
type SheetIntentDispatch,
|
|
108
|
+
type SheetIntents,
|
|
109
|
+
useSheetIntents,
|
|
110
|
+
} from "./state/use-sheet-intents";
|
|
111
|
+
export { useSheetState } from "./state/use-sheet-state";
|
|
112
|
+
export { BottomSheetStep } from "./steps/bottom-sheet-step";
|
|
113
|
+
export { BottomSheetSteps } from "./steps/bottom-sheet-steps";
|
|
114
|
+
export {
|
|
115
|
+
SheetStepContext,
|
|
116
|
+
type StepsLayoutValue,
|
|
117
|
+
useOptionalStepsLayout,
|
|
118
|
+
useStepsLayout,
|
|
119
|
+
} from "./steps/steps.context";
|
|
120
|
+
export type {
|
|
121
|
+
BottomSheetStepProps,
|
|
122
|
+
BottomSheetStepsProps,
|
|
123
|
+
SheetStepController,
|
|
124
|
+
UseSheetMachineOptions,
|
|
125
|
+
} from "./steps/steps.types";
|
|
126
|
+
export { useSheetMachine } from "./steps/use-sheet-machine";
|
|
127
|
+
export { useOptionalSheetStep, useSheetStep } from "./steps/use-sheet-step";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# keyboard
|
|
2
|
+
|
|
3
|
+
keyboard-controller's shared values, turned into the sheet's; which keyboard
|
|
4
|
+
is the sheet's to answer; and what each `keyboardBehavior` does about it.
|
|
5
|
+
|
|
6
|
+
## Files
|
|
7
|
+
|
|
8
|
+
| Path | What |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| `use-sheet-keyboard.ts` | The writer reaction (`keyboardHeight`, `keyboardProgress`, `keyboardOwned`) and the behaviour reaction (`extend` / `fillParent` snaps, `restore`); returns the registry `useBottomSheetTextInput` writes into |
|
|
11
|
+
| `use-bottom-sheet-text-input.ts` | `useBottomSheetTextInput()` → `{ ref, onFocus, onBlur }`, the explicit registration; inert outside a sheet |
|
|
12
|
+
| `use-keyboard-animation-guard.ts` | `useKeyboardAnimationGuard` and `reconcileKeyboardAnimation` — the stale-keyboard repair over `core/keyboard/keyboard-reset-guard` |
|
|
13
|
+
|
|
14
|
+
The decisions are in `core/keyboard/`: `resolveKeyboardOwner` (who owns it),
|
|
15
|
+
`keyboardStep` (what a behaviour does this frame), `keyboardInContainer`,
|
|
16
|
+
`keyboardLift`, `contentArea`, `isInputInsideSheet`. Nothing here contains a
|
|
17
|
+
rule; it samples shared values and carries out what the core returns.
|
|
18
|
+
|
|
19
|
+
## Source
|
|
20
|
+
|
|
21
|
+
`useKeyboardContext().reanimated` — `height` is **negative**, pixels from the
|
|
22
|
+
window's bottom edge; `progress` runs 0 to 1 and tracks an interactive
|
|
23
|
+
dismiss frame by frame. `useReanimatedFocusedInput().input` is the focused
|
|
24
|
+
field's frame, `null` between a blur and the next focus. All three are
|
|
25
|
+
UI-thread shared values, which is why every reaction here is a
|
|
26
|
+
`useAnimatedReaction` and nothing crosses to JS.
|
|
27
|
+
|
|
28
|
+
`keyboardInContainer` subtracts `containerBottomOffset` — a frame that ends
|
|
29
|
+
above the window's bottom is overlapped by that much less — and the result
|
|
30
|
+
is the positive `keyboardHeight` the geometry reads.
|
|
31
|
+
|
|
32
|
+
## Ownership
|
|
33
|
+
|
|
34
|
+
`keyboardScope` picks the rule. `registered`: only a field that went through
|
|
35
|
+
`useBottomSheetTextInput` or `BottomSheet.TextInput`. `inside` (the default):
|
|
36
|
+
those, plus any focused input whose frame overlaps the sheet — the frame
|
|
37
|
+
comes from keyboard-controller, the sheet's top from `position`, and
|
|
38
|
+
`isInputInsideSheet` compares them in window space. `always`: every keyboard.
|
|
39
|
+
|
|
40
|
+
Ownership is **sticky through a transition**. The focused-input frame can
|
|
41
|
+
land a frame after `progress` starts, and it goes null the moment a field
|
|
42
|
+
blurs while the keyboard takes another quarter second to leave; without
|
|
43
|
+
stickiness the sheet would drop by the lift and chase the keyboard down. A
|
|
44
|
+
different focused input releases at once — that keyboard is someone else's
|
|
45
|
+
— and so does `progress` reaching zero. `resolveKeyboardOwner` is the rule,
|
|
46
|
+
with its tests.
|
|
47
|
+
|
|
48
|
+
The registration path is the reliable one. `ref` records the field's native
|
|
49
|
+
node, `onFocus` claims, and `onBlur` releases unless React Native's own focus
|
|
50
|
+
registry says another registered field already has focus — the check that
|
|
51
|
+
stops a tap from one field to the next reading as a close and a reopen.
|
|
52
|
+
|
|
53
|
+
## Behaviours
|
|
54
|
+
|
|
55
|
+
| `keyboardBehavior` | On show | `keyboardBlurBehavior: "restore"` on hide |
|
|
56
|
+
| --- | --- | --- |
|
|
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 detent held before |
|
|
59
|
+
| `fillParent` | `animateTo(maxHeight, "keyboard")`; no lift; the body shrinks by the keyboard | Back to the detent held before |
|
|
60
|
+
| `none` | Nothing; the keyboard is not the sheet's problem | Nothing |
|
|
61
|
+
|
|
62
|
+
The behaviour reaction samples `progress` and `keyboardOwned` and hands two
|
|
63
|
+
consecutive samples to `keyboardStep`, which answers `apply`, `restore`,
|
|
64
|
+
`release` or nothing. A rise applies once; a fall restores once; an
|
|
65
|
+
interactive dismiss that turns back mid-way applies again. A finger on the
|
|
66
|
+
sheet owns `base`, so neither runs while a gesture is active — the release
|
|
67
|
+
settles wherever the finger left it.
|
|
68
|
+
|
|
69
|
+
`enableBlurKeyboardOnGesture` (default on) dismisses the keyboard the moment
|
|
70
|
+
a pan starts, and a pan that ends heading down dismisses it regardless; both
|
|
71
|
+
live in `gesture/use-sheet-pan.ts`, through `KeyboardController.dismiss` on
|
|
72
|
+
the JS thread.
|
|
73
|
+
|
|
74
|
+
## The stale-keyboard guard
|
|
75
|
+
|
|
76
|
+
On iOS `KeyboardProvider` writes its shared values on `keyboardWillShow` /
|
|
77
|
+
`keyboardWillHide` and nothing else, so a teardown that produces no
|
|
78
|
+
`willHide` — a native-stack pop over a focused field, an app suspend —
|
|
79
|
+
leaves them pinned open app-wide, and a sheet mounted into that would lift
|
|
80
|
+
for a keyboard that is not there. `reconcileKeyboardAnimation` snaps them to
|
|
81
|
+
closed when React Native's focus registry says nothing is focused; the sheet
|
|
82
|
+
runs it on every presentation, and `useKeyboardAnimationGuard` is the same
|
|
83
|
+
thing for a screen of your own. Same decision as `@delacour/react-native-ui`'s
|
|
84
|
+
guard, over the same core function, so the two never disagree.
|