@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
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# @delacour/react-native-bottom-sheet
|
|
2
|
+
|
|
3
|
+
A headless bottom sheet engine for React Native — Gesture Handler for the
|
|
4
|
+
pans, Reanimated for the motion, keyboard-controller for the keyboard,
|
|
5
|
+
teleport for the portal.
|
|
6
|
+
|
|
7
|
+
No tokens, no `className`, no theme. Every radius, colour and inset is a
|
|
8
|
+
`style` you pass in. If you want a sheet already wearing a design system, use
|
|
9
|
+
[`@delacour/react-native-ui/bottom-sheet`](https://ui.delacour.co.nz/docs/native/components/bottom-sheet),
|
|
10
|
+
which is this engine with the tokens attached.
|
|
11
|
+
|
|
12
|
+
Documentation: [ui.delacour.co.nz/docs/bottom-sheet](https://ui.delacour.co.nz/docs/bottom-sheet).
|
|
13
|
+
|
|
14
|
+
> **Alpha.** This package, `@delacour/react-native-ui` and
|
|
15
|
+
> `@delacour/react-native-charts` publish to npm's `alpha` tag, and every alpha
|
|
16
|
+
> is also `latest`.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun add @delacour/react-native-bottom-sheet@alpha
|
|
22
|
+
bunx expo install react-native-gesture-handler react-native-reanimated react-native-worklets react-native-keyboard-controller react-native-safe-area-context react-native-teleport
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The native peers go through `expo install`, so the SDK picks versions it can
|
|
26
|
+
build; `react` and `react-native` are already in any app.
|
|
27
|
+
|
|
28
|
+
**Fabric only, dev client only.** `react-native-teleport` moves native views
|
|
29
|
+
between hosts, which needs the new architecture — the default since React
|
|
30
|
+
Native 0.76 and the only architecture in Expo SDK 53 and later. It is a native
|
|
31
|
+
module, so it does not run in Expo Go: build a dev client
|
|
32
|
+
(`npx expo run:ios`, or an EAS development build) and rebuild it after
|
|
33
|
+
installing.
|
|
34
|
+
|
|
35
|
+
| Peer | Range | Why |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `react` | `>=19` | Ref-as-prop |
|
|
38
|
+
| `react-native` | `>=0.81` | Fabric `getBoundingClientRect` |
|
|
39
|
+
| `react-native-gesture-handler` | `>=2.28` | The handle and content pans |
|
|
40
|
+
| `react-native-reanimated` | `>=4` | Every animated value |
|
|
41
|
+
| `react-native-worklets` | `>=0.5` | `scheduleOnRN` from the UI thread |
|
|
42
|
+
| `react-native-keyboard-controller` | `>=1.18` | The only keyboard source — frame-accurate height and progress |
|
|
43
|
+
| `react-native-safe-area-context` | `>=5` | The bottom band under a sticky footer |
|
|
44
|
+
| `react-native-teleport` | `>=1.2` | The portal — the React tree stays put, so context reaches every part |
|
|
45
|
+
|
|
46
|
+
The package has no runtime dependencies of its own.
|
|
47
|
+
|
|
48
|
+
## Providers
|
|
49
|
+
|
|
50
|
+
Your app mounts four, in this order. The package renders only the last — a
|
|
51
|
+
nested gesture root is dead weight, and a nested `KeyboardProvider` is a
|
|
52
|
+
second keyboard that disagrees with the first.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { BottomSheetProvider } from "@delacour/react-native-bottom-sheet";
|
|
56
|
+
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
57
|
+
import { KeyboardProvider } from "react-native-keyboard-controller";
|
|
58
|
+
import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context";
|
|
59
|
+
|
|
60
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
61
|
+
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
|
62
|
+
<KeyboardProvider>
|
|
63
|
+
<BottomSheetProvider>{children}</BottomSheetProvider>
|
|
64
|
+
</KeyboardProvider>
|
|
65
|
+
</SafeAreaProvider>
|
|
66
|
+
</GestureHandlerRootView>;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`BottomSheetProvider` is the root host every sheet teleports to, so a sheet
|
|
70
|
+
draws over the navigator wherever it was written. Without it a `Portal`
|
|
71
|
+
renders where it is written, as an inline sheet.
|
|
72
|
+
|
|
73
|
+
## Anatomy
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { BottomSheet } from "@delacour/react-native-bottom-sheet";
|
|
77
|
+
|
|
78
|
+
<BottomSheet snapPoints={["40%", "90%"]}>
|
|
79
|
+
<BottomSheet.Trigger>
|
|
80
|
+
<Text>Open</Text>
|
|
81
|
+
</BottomSheet.Trigger>
|
|
82
|
+
<BottomSheet.Portal>
|
|
83
|
+
<BottomSheet.Overlay style={{ backgroundColor: "rgba(0,0,0,0.4)" }} />
|
|
84
|
+
<BottomSheet.Container>
|
|
85
|
+
<BottomSheet.Background style={{ backgroundColor: "#fff", borderTopLeftRadius: 16, borderTopRightRadius: 16 }} />
|
|
86
|
+
<BottomSheet.Handle style={{ alignItems: "center", paddingVertical: 12 }}>
|
|
87
|
+
<View style={{ width: 36, height: 5, borderRadius: 3, backgroundColor: "#ccc" }} />
|
|
88
|
+
</BottomSheet.Handle>
|
|
89
|
+
<BottomSheet.Content style={{ padding: 16 }}>
|
|
90
|
+
<BottomSheet.Title>Filters</BottomSheet.Title>
|
|
91
|
+
<BottomSheet.Description>Narrow the list down.</BottomSheet.Description>
|
|
92
|
+
<BottomSheet.TextInput placeholder="Search" />
|
|
93
|
+
<BottomSheet.Close>
|
|
94
|
+
<Text>Close</Text>
|
|
95
|
+
</BottomSheet.Close>
|
|
96
|
+
</BottomSheet.Content>
|
|
97
|
+
<BottomSheet.Footer padding={16}>
|
|
98
|
+
<Button title="Apply" onPress={apply} />
|
|
99
|
+
</BottomSheet.Footer>
|
|
100
|
+
</BottomSheet.Container>
|
|
101
|
+
</BottomSheet.Portal>
|
|
102
|
+
</BottomSheet>;
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`Container` is the panel that moves; `Content` is the static body inside it.
|
|
106
|
+
Swap the body for `BottomSheet.ScrollView`, `FlatList` or `SectionList` and
|
|
107
|
+
the list's content size becomes the sheet's own detent; swap it for
|
|
108
|
+
`BottomSheet.Steps` with a controller from `useSheetMachine` and the sheet's
|
|
109
|
+
height glides between the steps of a typed machine. `Footer` is sticky by
|
|
110
|
+
default, standing on the safe-area band and riding the keyboard's edge.
|
|
111
|
+
`<BottomSheet.Portal inline>` renders in place; `<BottomSheet.Host name>`
|
|
112
|
+
gives a native modal a target of its own; `detached` floats the sheet as a
|
|
113
|
+
card.
|
|
114
|
+
|
|
115
|
+
Sizing and behaviour — `snapPoints`, `dynamicSizing`, `maxDynamicContentSize`,
|
|
116
|
+
`keyboardBehavior`, `detached`, the haptic worklet props — are the root's.
|
|
117
|
+
Every part takes `style` and `ref`, and the parts that render text or a press
|
|
118
|
+
take `asChild` to hand their behaviour to an element of yours.
|
|
119
|
+
|
|
120
|
+
## The maths on its own
|
|
121
|
+
|
|
122
|
+
`@delacour/react-native-bottom-sheet/core` is every detent, keyboard, footer
|
|
123
|
+
and geometry calculation in the package, plus the step machine, importable
|
|
124
|
+
with no native module in the module graph:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { defineSheetMachine, positionFor, UNMEASURED } from "@delacour/react-native-bottom-sheet/core";
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Licence
|
|
131
|
+
|
|
132
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@delacour/react-native-bottom-sheet",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Headless bottom sheet engine for React Native — no tokens, no className",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/delacournz/delacour-ui.git",
|
|
10
|
+
"directory": "packages/react-native-bottom-sheet"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"react-native",
|
|
14
|
+
"bottom-sheet",
|
|
15
|
+
"reanimated",
|
|
16
|
+
"gesture-handler",
|
|
17
|
+
"keyboard-controller",
|
|
18
|
+
"expo"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/index.ts",
|
|
30
|
+
"./core": "./src/core/index.ts"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"fmt": "biome format --write ./src",
|
|
34
|
+
"lint": "biome lint --write ./src",
|
|
35
|
+
"check": "biome check ./src",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"test": "bun test"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": ">=19",
|
|
41
|
+
"react-native": ">=0.81",
|
|
42
|
+
"react-native-gesture-handler": ">=2.28",
|
|
43
|
+
"react-native-keyboard-controller": ">=1.18",
|
|
44
|
+
"react-native-reanimated": ">=4",
|
|
45
|
+
"react-native-safe-area-context": ">=5",
|
|
46
|
+
"react-native-teleport": ">=1.2",
|
|
47
|
+
"react-native-worklets": ">=0.5"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@delacour/biome-config": "workspace:*",
|
|
51
|
+
"@delacour/tsconfig": "workspace:*",
|
|
52
|
+
"@types/bun": "^1.3.8",
|
|
53
|
+
"@types/react": "catalog:",
|
|
54
|
+
"react-native-teleport": "^1.2.2",
|
|
55
|
+
"typescript": "^5.9.3"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# animation
|
|
2
|
+
|
|
3
|
+
How `base` moves, and how the JS thread hears about it.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| Path | What |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `animation.types.ts` | `SheetAnimation` (the prop), `AnimateTo` / `JumpTo` / `SettleAt` (the worklets), `SettleListener` / `AnimateListener` (what JS hears) |
|
|
10
|
+
| `resolve-animation.ts` | Easing and reduce-motion names to Reanimated's values; `toReanimated` turns the core's resolved config into a `withSpring` / `withTiming` config |
|
|
11
|
+
| `use-animate-to.ts` | `animateTo(target, source, velocity)`, `jumpTo(target, source)` and `settleAt(target, source)`, hook-scope worklets |
|
|
12
|
+
| `use-settle-callbacks.ts` | The two JS listeners, stable for the sheet's lifetime, reading every prop through a ref |
|
|
13
|
+
|
|
14
|
+
## The shape
|
|
15
|
+
|
|
16
|
+
`selectAnimation` in the core picks the config as plain data — the consumer's
|
|
17
|
+
over the platform default, iOS a spring and everything else a timing, easing by
|
|
18
|
+
name. `resolve-animation.ts` is the one place a name becomes a Reanimated
|
|
19
|
+
function, and it runs on the JS thread once per config change.
|
|
20
|
+
|
|
21
|
+
`animateTo` cancels whatever owns `base`, records the target and the source in
|
|
22
|
+
the shared state, schedules `onAnimate` on JS, and hands `base` to the spring
|
|
23
|
+
or the timing. The completion callback settles only on `finished === true`:
|
|
24
|
+
a cancelled animation is someone else's now, and settling it would report an
|
|
25
|
+
index the sheet never reached. Settling writes `currentIndex` and schedules
|
|
26
|
+
`onSettle`.
|
|
27
|
+
|
|
28
|
+
`jumpTo` is `animateTo` without the motion — `forceClose`, a container resize,
|
|
29
|
+
`animateOnMount: false`.
|
|
30
|
+
|
|
31
|
+
`settleAt` is the completion alone: no `onAnimate`, no target recorded, `base`
|
|
32
|
+
written to the detent it is already within a settle tolerance of, then the same
|
|
33
|
+
`currentIndex` write and `onSettle`. The content pan uses it for a release the
|
|
34
|
+
list owns — the finger carried the sheet to the top and kept scrolling, so
|
|
35
|
+
nothing animated and nothing else would report the index.
|
|
36
|
+
|
|
37
|
+
## Reanimated 4
|
|
38
|
+
|
|
39
|
+
`scheduleOnRN` comes from `react-native-worklets`, not from Reanimated;
|
|
40
|
+
`cancelAnimation`, `withSpring`, `withTiming`, `Easing` and `ReduceMotion` from
|
|
41
|
+
Reanimated. Reanimated 4 replaced the spring's `restDisplacementThreshold` and
|
|
42
|
+
`restSpeedThreshold` with one relative `energyThreshold`, so those two fields
|
|
43
|
+
of the core's `IOS_SPRING` are kept as data and not forwarded; with
|
|
44
|
+
`overshootClamping` and a stiff spring, the default threshold
|
|
45
|
+
settles within a frame or two of them.
|
|
46
|
+
|
|
47
|
+
## The listeners
|
|
48
|
+
|
|
49
|
+
`useSettleCallbacks` returns two functions that never change identity. Every
|
|
50
|
+
prop they read goes through a ref, because `scheduleOnRN` calls the function
|
|
51
|
+
the worklet captured when it was built — a listener that closed over
|
|
52
|
+
`onIndexChange` directly would call the callback from the render the gesture
|
|
53
|
+
began in.
|
|
54
|
+
|
|
55
|
+
A settle at `-1` is where a physical close becomes state: `onClose`, then
|
|
56
|
+
`setOpen(false)` — the root's `onOpenChange(false)` — then `setPresented(false)`
|
|
57
|
+
and the portal's children unmount unless `keepMounted`. Any other settle reports
|
|
58
|
+
through `onIndexChange` only when the index moved.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AnimationSource, SheetAnimationConfig } from "../core";
|
|
2
|
+
|
|
3
|
+
/** The `animation` prop — a spring or a timing, every field optional. */
|
|
4
|
+
export type SheetAnimation = SheetAnimationConfig;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Moves `base` to a height. A hook-scope worklet: callable from a gesture
|
|
8
|
+
* callback, a reaction or the intent resolver, all on the UI thread.
|
|
9
|
+
*
|
|
10
|
+
* `velocity` is in height space per second — upward positive — and only a
|
|
11
|
+
* spring reads it.
|
|
12
|
+
*/
|
|
13
|
+
export type AnimateTo = (target: number, source: AnimationSource, velocity: number) => void;
|
|
14
|
+
|
|
15
|
+
/** Puts `base` at a height with no animation and settles immediately. */
|
|
16
|
+
export type JumpTo = (target: number, source: AnimationSource) => void;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Settles at a height the sheet is already on — the bookkeeping of a finished
|
|
20
|
+
* animation with no animation: `currentIndex`, then `onSettle`. For a release
|
|
21
|
+
* that moved the sheet by hand and never animated.
|
|
22
|
+
*/
|
|
23
|
+
export type SettleAt = (target: number, source: AnimationSource) => void;
|
|
24
|
+
|
|
25
|
+
/** What the JS thread hears when an animation lands — `count` is how many detents there were, for the handle's accessibility value. */
|
|
26
|
+
export type SettleListener = (index: number, height: number, source: AnimationSource, count: number) => void;
|
|
27
|
+
|
|
28
|
+
/** What the JS thread hears when an animation starts. */
|
|
29
|
+
export type AnimateListener = (
|
|
30
|
+
fromIndex: number,
|
|
31
|
+
toIndex: number,
|
|
32
|
+
fromHeight: number,
|
|
33
|
+
toHeight: number,
|
|
34
|
+
source: AnimationSource
|
|
35
|
+
) => void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Easing,
|
|
3
|
+
type EasingFunction,
|
|
4
|
+
ReduceMotion,
|
|
5
|
+
type WithSpringConfig,
|
|
6
|
+
type WithTimingConfig,
|
|
7
|
+
} from "react-native-reanimated";
|
|
8
|
+
import type { EasingName, ReduceMotionMode, ResolvedAnimation } from "../core";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A core easing name to Reanimated's function. The core keeps its defaults as
|
|
12
|
+
* data so `bun test` can read them; this is the one place the name becomes a
|
|
13
|
+
* function, and it happens on the JS thread before any worklet runs.
|
|
14
|
+
*/
|
|
15
|
+
export function easingFor(name: EasingName): EasingFunction {
|
|
16
|
+
switch (name) {
|
|
17
|
+
case "linear":
|
|
18
|
+
return Easing.linear;
|
|
19
|
+
case "outCubic":
|
|
20
|
+
return Easing.out(Easing.cubic);
|
|
21
|
+
case "inOutCubic":
|
|
22
|
+
return Easing.inOut(Easing.cubic);
|
|
23
|
+
default:
|
|
24
|
+
return Easing.out(Easing.exp);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function reduceMotionFor(mode: ReduceMotionMode): ReduceMotion {
|
|
29
|
+
switch (mode) {
|
|
30
|
+
case "always":
|
|
31
|
+
return ReduceMotion.Always;
|
|
32
|
+
case "never":
|
|
33
|
+
return ReduceMotion.Never;
|
|
34
|
+
default:
|
|
35
|
+
return ReduceMotion.System;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A resolved animation as the Reanimated config `withSpring` / `withTiming` takes. */
|
|
40
|
+
export type ReanimatedAnimation =
|
|
41
|
+
| { type: "spring"; config: WithSpringConfig }
|
|
42
|
+
| { type: "timing"; config: WithTimingConfig };
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Turns the core's plain-data resolution into a Reanimated config, once, on
|
|
46
|
+
* the JS thread.
|
|
47
|
+
*
|
|
48
|
+
* Reanimated 4 replaced `restDisplacementThreshold` / `restSpeedThreshold`
|
|
49
|
+
* with a single relative `energyThreshold`, so the two rest thresholds the
|
|
50
|
+
* core carries — the previous engine's defaults, kept as data for a future runtime that
|
|
51
|
+
* reads them — are not forwarded. With `overshootClamping` and a stiff spring
|
|
52
|
+
* the default energy threshold settles within a frame or two of them anyway.
|
|
53
|
+
*/
|
|
54
|
+
export function toReanimated(resolved: ResolvedAnimation): ReanimatedAnimation {
|
|
55
|
+
if (resolved.type === "spring") {
|
|
56
|
+
return {
|
|
57
|
+
type: "spring",
|
|
58
|
+
config: {
|
|
59
|
+
damping: resolved.damping,
|
|
60
|
+
stiffness: resolved.stiffness,
|
|
61
|
+
mass: resolved.mass,
|
|
62
|
+
overshootClamping: resolved.overshootClamping,
|
|
63
|
+
reduceMotion: reduceMotionFor(resolved.reduceMotion),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
type: "timing",
|
|
69
|
+
config: {
|
|
70
|
+
duration: resolved.duration,
|
|
71
|
+
easing: easingFor(resolved.easing),
|
|
72
|
+
reduceMotion: reduceMotionFor(resolved.reduceMotion),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
import { cancelAnimation, withSpring, withTiming } from "react-native-reanimated";
|
|
4
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
5
|
+
import { type AnimationSource, indexForHeight, type ReduceMotionMode, selectAnimation } from "../core";
|
|
6
|
+
import { ANIM_STATUS, type SheetGeometry, type SheetSharedState } from "../state/state.types";
|
|
7
|
+
import type { AnimateListener, AnimateTo, JumpTo, SettleAt, SettleListener, SheetAnimation } from "./animation.types";
|
|
8
|
+
import { toReanimated } from "./resolve-animation";
|
|
9
|
+
|
|
10
|
+
export type UseAnimateToOptions = {
|
|
11
|
+
state: SheetSharedState;
|
|
12
|
+
geometry: SheetGeometry;
|
|
13
|
+
animation: SheetAnimation | undefined;
|
|
14
|
+
overrideReduceMotion: ReduceMotionMode | undefined;
|
|
15
|
+
/** Stable across renders — the root wraps its props in refs. */
|
|
16
|
+
onSettle: SettleListener;
|
|
17
|
+
onAnimate: AnimateListener;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The one way `base` moves.
|
|
22
|
+
*
|
|
23
|
+
* `animateTo` cancels whatever is in flight, records the target and source,
|
|
24
|
+
* tells the JS thread an animation started, and hands `base` to a spring or a
|
|
25
|
+
* timing whose completion writes `currentIndex` and reports the settle.
|
|
26
|
+
* `jumpTo` is the same without the motion — `forceClose`, a container resize,
|
|
27
|
+
* and `animateOnMount: false`. `settleAt` is the completion alone: no
|
|
28
|
+
* `onAnimate`, no target recorded, `base` written to the detent it is already
|
|
29
|
+
* within a settle tolerance of. It is for the content pan whose release the
|
|
30
|
+
* list owns — the finger carried the sheet to the top and kept scrolling, so
|
|
31
|
+
* nothing animated and nothing would otherwise write `currentIndex`.
|
|
32
|
+
*
|
|
33
|
+
* Both are hook-scope worklets: they close over `indexForHeight` from the core
|
|
34
|
+
* in the ordinary way, which is what the flat-worklet rule permits and what a
|
|
35
|
+
* module-scope worklet could not do.
|
|
36
|
+
*
|
|
37
|
+
* A cancelled animation does not settle. The gesture that cancelled it will,
|
|
38
|
+
* and an animation that interrupted it has its own completion; settling on
|
|
39
|
+
* `finished === false` would report an index the sheet never reached.
|
|
40
|
+
*/
|
|
41
|
+
export function useAnimateTo(options: UseAnimateToOptions): {
|
|
42
|
+
animateTo: AnimateTo;
|
|
43
|
+
jumpTo: JumpTo;
|
|
44
|
+
settleAt: SettleAt;
|
|
45
|
+
} {
|
|
46
|
+
const { state, geometry, animation, overrideReduceMotion, onSettle, onAnimate } = options;
|
|
47
|
+
const platform = Platform.OS;
|
|
48
|
+
const animationKey = JSON.stringify(animation ?? null);
|
|
49
|
+
|
|
50
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: `animation` is keyed by its serialisation
|
|
51
|
+
const resolved = useMemo(
|
|
52
|
+
() => toReanimated(selectAnimation(animation, platform, overrideReduceMotion)),
|
|
53
|
+
[animationKey, platform, overrideReduceMotion]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return useMemo(() => {
|
|
57
|
+
const settle = (target: number, source: AnimationSource): void => {
|
|
58
|
+
"worklet";
|
|
59
|
+
state.animStatus.value = ANIM_STATUS.IDLE;
|
|
60
|
+
const index = Math.round(indexForHeight(target, geometry.detents.value, geometry.closedHeight.value));
|
|
61
|
+
state.currentIndex.value = index;
|
|
62
|
+
scheduleOnRN(onSettle, index, target, source, geometry.detents.value.length);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const begin = (target: number, source: AnimationSource): void => {
|
|
66
|
+
"worklet";
|
|
67
|
+
cancelAnimation(state.base);
|
|
68
|
+
const fromHeight = state.base.value;
|
|
69
|
+
const fromIndex = geometry.index.value;
|
|
70
|
+
const toIndex = indexForHeight(target, geometry.detents.value, geometry.closedHeight.value);
|
|
71
|
+
state.animStatus.value = ANIM_STATUS.RUNNING;
|
|
72
|
+
state.animSource.value = source;
|
|
73
|
+
state.animTarget.value = target;
|
|
74
|
+
scheduleOnRN(onAnimate, fromIndex, toIndex, fromHeight, target, source);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const animateTo: AnimateTo = (target, source, velocity) => {
|
|
78
|
+
"worklet";
|
|
79
|
+
begin(target, source);
|
|
80
|
+
const complete = (finished?: boolean): void => {
|
|
81
|
+
"worklet";
|
|
82
|
+
if (finished === true) settle(target, source);
|
|
83
|
+
};
|
|
84
|
+
if (resolved.type === "spring") {
|
|
85
|
+
state.base.value = withSpring(target, { ...resolved.config, velocity }, complete);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
state.base.value = withTiming(target, resolved.config, complete);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const jumpTo: JumpTo = (target, source) => {
|
|
92
|
+
"worklet";
|
|
93
|
+
begin(target, source);
|
|
94
|
+
state.base.value = target;
|
|
95
|
+
settle(target, source);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const settleAt: SettleAt = (target, source) => {
|
|
99
|
+
"worklet";
|
|
100
|
+
cancelAnimation(state.base);
|
|
101
|
+
state.base.value = target;
|
|
102
|
+
settle(target, source);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return { animateTo, jumpTo, settleAt };
|
|
106
|
+
}, [state, geometry, resolved, onSettle, onAnimate]);
|
|
107
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
import { type AnimationSource, CLOSED_INDEX } from "../core";
|
|
3
|
+
import type { AnimateListener, SettleListener } from "./animation.types";
|
|
4
|
+
|
|
5
|
+
export type UseSettleCallbacksOptions = {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
setOpen: (open: boolean) => void;
|
|
8
|
+
/** Unmounts the portal's children once a close has landed, unless the sheet is kept mounted. */
|
|
9
|
+
setPresented: (presented: boolean) => void;
|
|
10
|
+
/** The settled index and the detent count, for the handle's accessibility value. */
|
|
11
|
+
setIndex: (index: number) => void;
|
|
12
|
+
setDetentCount: (count: number) => void;
|
|
13
|
+
onIndexChange: ((index: number, height: number, source: AnimationSource) => void) | undefined;
|
|
14
|
+
onClose: (() => void) | undefined;
|
|
15
|
+
onAnimate: AnimateListener | undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The JS-thread half of an animation: two listeners the UI thread schedules
|
|
20
|
+
* through `scheduleOnRN`, stable for the sheet's lifetime so the worklets that
|
|
21
|
+
* capture them never rebuild.
|
|
22
|
+
*
|
|
23
|
+
* Every prop they read goes through a ref. `scheduleOnRN` takes the function
|
|
24
|
+
* it was handed at the time the worklet was built, so a listener that closed
|
|
25
|
+
* over `onIndexChange` directly would call the callback from the render the
|
|
26
|
+
* gesture began in.
|
|
27
|
+
*
|
|
28
|
+
* A settle at `-1` is the one place a physical close becomes state: `onClose`,
|
|
29
|
+
* then `setOpen(false)` — which is `onOpenChange(false)` — then the portal
|
|
30
|
+
* unmounts. A settle anywhere else reports through `onIndexChange` only when
|
|
31
|
+
* the index actually moved.
|
|
32
|
+
*/
|
|
33
|
+
export function useSettleCallbacks(options: UseSettleCallbacksOptions): {
|
|
34
|
+
onSettle: SettleListener;
|
|
35
|
+
onAnimate: AnimateListener;
|
|
36
|
+
} {
|
|
37
|
+
const latest = useRef(options);
|
|
38
|
+
latest.current = options;
|
|
39
|
+
const lastReported = useRef(CLOSED_INDEX);
|
|
40
|
+
|
|
41
|
+
const onSettle = useCallback<SettleListener>((index, height, source, count) => {
|
|
42
|
+
const current = latest.current;
|
|
43
|
+
current.setIndex(index);
|
|
44
|
+
current.setDetentCount(count);
|
|
45
|
+
if (index !== lastReported.current) {
|
|
46
|
+
lastReported.current = index;
|
|
47
|
+
current.onIndexChange?.(index, height, source);
|
|
48
|
+
}
|
|
49
|
+
if (index !== CLOSED_INDEX) return;
|
|
50
|
+
current.onClose?.();
|
|
51
|
+
if (current.isOpen) current.setOpen(false);
|
|
52
|
+
current.setPresented(false);
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const onAnimate = useCallback<AnimateListener>((fromIndex, toIndex, fromHeight, toHeight, source) => {
|
|
56
|
+
latest.current.onAnimate?.(fromIndex, toIndex, fromHeight, toHeight, source);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
return { onSettle, onAnimate };
|
|
60
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# components
|
|
2
|
+
|
|
3
|
+
The compound `BottomSheet` and its parts. Every part accepts `style` and `ref`;
|
|
4
|
+
none takes a class; every `displayName` is `DelacourBottomSheet.BottomSheet.X`.
|
|
5
|
+
|
|
6
|
+
## Files
|
|
7
|
+
|
|
8
|
+
| Path | What |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| `bottom-sheet.tsx` | The root — state, geometry, animation, pans, intents, the imperative ref, and the `Object.assign` that names the parts |
|
|
11
|
+
| `bottom-sheet.types.ts` | Every prop type, `BottomSheetRef`, `BottomSheetContextValue`, `BottomSheetAnimatedValue` |
|
|
12
|
+
| `bottom-sheet.context.tsx` | Three contexts: `BottomSheet` (open state and ref methods), `BottomSheetAnimated` (the shared values), `BottomSheetInternal` (what the parts share) |
|
|
13
|
+
| `bottom-sheet-trigger.tsx` | Opens the sheet; `asChild` donates `onPress` |
|
|
14
|
+
| `bottom-sheet-portal.tsx` | The frame the sheet measures itself against — inside a teleport `Portal` to the nearest host under a provider, in place with `inline` or without one; registers the sheet as presented |
|
|
15
|
+
| `bottom-sheet-overlay.tsx` | The scrim: a React Native `Pressable`, written before the panel |
|
|
16
|
+
| `bottom-sheet-container.tsx` | The panel — the surface that moves; inset by `detachedFrame` and `box-none` when detached |
|
|
17
|
+
| `bottom-sheet-background.tsx` | The panel's absolute-fill surface, `pointerEvents: none`; exactly the sheet's `height` tall when detached |
|
|
18
|
+
| `bottom-sheet-handle.tsx` | The grabber's row: the handle pan and the adjustable accessibility element |
|
|
19
|
+
| `bottom-sheet-content.tsx` | The static body: the content pan, the `bodyClip` clip, the `contentArea + bodyInset` layout box, the measured inner box and the inset spacer |
|
|
20
|
+
| `bottom-sheet-footer.tsx` | The sticky footer: translated to `footerTop`, its styled box padded through the band and measured less the band into `footerContentHeight` |
|
|
21
|
+
| `bottom-sheet-close.tsx` | The dismiss control; `asChild` donates `onPress` |
|
|
22
|
+
| `bottom-sheet-title.tsx` | The heading; publishes the `nativeID` the panel is labelled by |
|
|
23
|
+
| `bottom-sheet-description.tsx` | Supporting copy; publishes a `nativeID` of its own |
|
|
24
|
+
|
|
25
|
+
`Host` and `Provider` live in `src/portal` and are named on the compound from
|
|
26
|
+
there. Parts that arrive later: `Footer` and `TextInput` (BSHEET-3), `ScrollView`,
|
|
27
|
+
`FlatList` and `SectionList` (BSHEET-4), `Steps` and `Step` (BSHEET-6b).
|
|
28
|
+
|
|
29
|
+
## The render tree
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
teleport Portal — only under a provider (absoluteFill of the host, zIndex from the registry)
|
|
33
|
+
Portal — the frame (absolute, top: topInset, box-none, overflow hidden, zIndex; measures the container)
|
|
34
|
+
Overlay (absoluteFill — margins and gap included, opacity from index, pointerEvents auto|none, RN Pressable)
|
|
35
|
+
Container — the panel (absoluteFill, translateY: position; detached: left/width from detachedFrame, overflow visible, box-none)
|
|
36
|
+
Background (absoluteFill, pointerEvents none; detached: height = sheet height)
|
|
37
|
+
Handle (pan detector; measures handleHeight)
|
|
38
|
+
Content (pan detector; clip: overflow hidden, animated maxHeight = bodyClip)
|
|
39
|
+
layout box (animated maxHeight = contentArea + bodyInset; height too under fillParent)
|
|
40
|
+
inner box (measures contentHeight) + trailing spacer (bodyInset, + footerGap above a footer)
|
|
41
|
+
Footer (absolute, translateY: footerTop; the styled box pads through the band, measures footerContentHeight = box − band)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The overlay is written **before** the panel on purpose. Native hit-testing
|
|
45
|
+
walks siblings back to front, so a touch on the panel never reaches the
|
|
46
|
+
overlay and a touch beside it does — with no Gesture Handler tap anywhere to
|
|
47
|
+
out-compete a `TextInput`. That was the first risk in the plan, and it holds
|
|
48
|
+
on the simulator: a field inside the sheet focuses on the first tap.
|
|
49
|
+
|
|
50
|
+
## State opens, intents move
|
|
51
|
+
|
|
52
|
+
`isOpen` — controlled through `isOpen`/`onOpenChange`, or held by the root —
|
|
53
|
+
is the one source of truth for whether the sheet is open. Flipping it true
|
|
54
|
+
sets `presented` (which mounts the portal's children) and dispatches an
|
|
55
|
+
`open` intent, which resolves once every measurement has landed. Every close
|
|
56
|
+
path — swipe, overlay, `Close`, the ref, a controlled `isOpen: false` —
|
|
57
|
+
animates to the closed height, and the settle at `-1` is what flips `isOpen`
|
|
58
|
+
back and unmounts the children. Closing a sheet that is not open is nothing.
|
|
59
|
+
|
|
60
|
+
The ref's `close()` checks the sheet's effective openness on the JS thread
|
|
61
|
+
before dispatching, so a `close()` while a queued open is still waiting on
|
|
62
|
+
layout clears the queue rather than racing it.
|
|
63
|
+
|
|
64
|
+
## Accessibility
|
|
65
|
+
|
|
66
|
+
No `accessible` container: one would collapse the whole sheet into a single
|
|
67
|
+
VoiceOver element. Instead the panel is `accessibilityViewIsModal` while open
|
|
68
|
+
under an overlay, its descendants are hidden while it is closed, and the title
|
|
69
|
+
labels it through `accessibilityLabelledBy`. The handle is the sheet's one
|
|
70
|
+
`adjustable` element: increment and decrement move a detent, escape closes,
|
|
71
|
+
and its value reads `Detent i of n`. The overlay is hidden from assistive
|
|
72
|
+
technology while it is transparent.
|
|
73
|
+
|
|
74
|
+
## Contexts
|
|
75
|
+
|
|
76
|
+
All three are provided once, by the root. A teleported portal keeps the React
|
|
77
|
+
tree in place, so they reach every part without being re-provided — and so
|
|
78
|
+
does every context the app provides around the trigger.
|
|
79
|
+
`useBottomSheetInternal` is exported for a part written outside the package —
|
|
80
|
+
a skin's `Footer`, a scrollable — and never for an app.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { StyleSheet } from "react-native";
|
|
3
|
+
import Animated, { useAnimatedStyle } from "react-native-reanimated";
|
|
4
|
+
import { useBottomSheetInternal } from "./bottom-sheet.context";
|
|
5
|
+
import type { BottomSheetBackgroundProps } from "./bottom-sheet.types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The panel's surface: an absolute fill behind the handle and the body that
|
|
9
|
+
* takes no touches. Colour, radius and shadow arrive as `style` — this is
|
|
10
|
+
* where a skin puts `rounded-t-2xl bg-popover`, and a detached sheet rounds
|
|
11
|
+
* every corner here.
|
|
12
|
+
*
|
|
13
|
+
* The panel is as tall as the frame, so an attached surface fills it and its
|
|
14
|
+
* bottom edge is always off-screen. A detached card has a visible bottom
|
|
15
|
+
* edge: the surface is then the geometry's `surfaceHeight` tall, on the UI
|
|
16
|
+
* thread — the sheet's height between its detents, and the nearest detent's
|
|
17
|
+
* height beyond them — so the corners the skin rounds are on screen, the gap
|
|
18
|
+
* under the card is the overlay's, and a close or an over-drag moves the
|
|
19
|
+
* whole card as one rigid body rather than shrinking it onto the resting
|
|
20
|
+
* line.
|
|
21
|
+
*/
|
|
22
|
+
export function BottomSheetBackground({ style, ref, ...props }: BottomSheetBackgroundProps): ReactElement {
|
|
23
|
+
const { geometry, detached } = useBottomSheetInternal();
|
|
24
|
+
const surface = geometry.surfaceHeight;
|
|
25
|
+
const isDetached = detached !== null;
|
|
26
|
+
|
|
27
|
+
const sized = useAnimatedStyle(() => {
|
|
28
|
+
if (!isDetached) return { bottom: 0, height: undefined };
|
|
29
|
+
return { bottom: undefined, height: surface.value };
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return <Animated.View pointerEvents="none" ref={ref} style={[StyleSheet.absoluteFill, style, sized]} {...props} />;
|
|
33
|
+
}
|
|
34
|
+
BottomSheetBackground.displayName = "DelacourBottomSheet.BottomSheet.Background";
|