@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.
Files changed (143) hide show
  1. package/README.md +132 -0
  2. package/package.json +57 -0
  3. package/src/animation/AGENTS.md +58 -0
  4. package/src/animation/animation.types.ts +35 -0
  5. package/src/animation/resolve-animation.ts +75 -0
  6. package/src/animation/use-animate-to.ts +107 -0
  7. package/src/animation/use-settle-callbacks.ts +60 -0
  8. package/src/components/AGENTS.md +80 -0
  9. package/src/components/bottom-sheet-background.tsx +34 -0
  10. package/src/components/bottom-sheet-close.tsx +45 -0
  11. package/src/components/bottom-sheet-container.tsx +79 -0
  12. package/src/components/bottom-sheet-content.tsx +102 -0
  13. package/src/components/bottom-sheet-description.tsx +33 -0
  14. package/src/components/bottom-sheet-footer.tsx +97 -0
  15. package/src/components/bottom-sheet-handle.tsx +72 -0
  16. package/src/components/bottom-sheet-overlay.tsx +91 -0
  17. package/src/components/bottom-sheet-portal.tsx +96 -0
  18. package/src/components/bottom-sheet-text-input.tsx +36 -0
  19. package/src/components/bottom-sheet-title.tsx +30 -0
  20. package/src/components/bottom-sheet-trigger.tsx +51 -0
  21. package/src/components/bottom-sheet.context.tsx +134 -0
  22. package/src/components/bottom-sheet.tsx +475 -0
  23. package/src/components/bottom-sheet.types.ts +235 -0
  24. package/src/core/AGENTS.md +160 -0
  25. package/src/core/animation/select-animation.test.ts +57 -0
  26. package/src/core/animation/select-animation.ts +104 -0
  27. package/src/core/backdrop/backdrop-opacity.test.ts +49 -0
  28. package/src/core/backdrop/backdrop-opacity.ts +21 -0
  29. package/src/core/detents/dynamic-detent.test.ts +40 -0
  30. package/src/core/detents/dynamic-detent.ts +41 -0
  31. package/src/core/detents/index-for-height.test.ts +65 -0
  32. package/src/core/detents/index-for-height.ts +44 -0
  33. package/src/core/detents/normalize-detents.test.ts +72 -0
  34. package/src/core/detents/normalize-detents.ts +63 -0
  35. package/src/core/detents/over-drag.test.ts +46 -0
  36. package/src/core/detents/over-drag.ts +23 -0
  37. package/src/core/detents/select-snap-height.test.ts +52 -0
  38. package/src/core/detents/select-snap-height.ts +44 -0
  39. package/src/core/detents/sheet-state.test.ts +42 -0
  40. package/src/core/detents/sheet-state.ts +32 -0
  41. package/src/core/footer/body-inset.test.ts +95 -0
  42. package/src/core/footer/body-inset.ts +65 -0
  43. package/src/core/footer/bottom-band.test.ts +46 -0
  44. package/src/core/footer/bottom-band.ts +30 -0
  45. package/src/core/footer/footer-top.test.ts +61 -0
  46. package/src/core/footer/footer-top.ts +27 -0
  47. package/src/core/footer/sheet-insets.test.ts +27 -0
  48. package/src/core/footer/sheet-insets.ts +30 -0
  49. package/src/core/geometry/clamp-height.test.ts +25 -0
  50. package/src/core/geometry/clamp-height.ts +12 -0
  51. package/src/core/geometry/closed-height.test.ts +37 -0
  52. package/src/core/geometry/closed-height.ts +29 -0
  53. package/src/core/geometry/detached-frame.test.ts +50 -0
  54. package/src/core/geometry/detached-frame.ts +49 -0
  55. package/src/core/geometry/position.test.ts +21 -0
  56. package/src/core/geometry/position.ts +13 -0
  57. package/src/core/geometry/surface-height.test.ts +30 -0
  58. package/src/core/geometry/surface-height.ts +22 -0
  59. package/src/core/haptic/crossed-detent.test.ts +43 -0
  60. package/src/core/haptic/crossed-detent.ts +33 -0
  61. package/src/core/index.ts +92 -0
  62. package/src/core/intent/layout-ready.test.ts +37 -0
  63. package/src/core/intent/layout-ready.ts +24 -0
  64. package/src/core/intent/resolve-intent.test.ts +163 -0
  65. package/src/core/intent/resolve-intent.ts +98 -0
  66. package/src/core/keyboard/container-layout-guard.test.ts +32 -0
  67. package/src/core/keyboard/container-layout-guard.ts +31 -0
  68. package/src/core/keyboard/content-area.test.ts +34 -0
  69. package/src/core/keyboard/content-area.ts +26 -0
  70. package/src/core/keyboard/keyboard-lift.test.ts +54 -0
  71. package/src/core/keyboard/keyboard-lift.ts +47 -0
  72. package/src/core/keyboard/keyboard-owner.test.ts +35 -0
  73. package/src/core/keyboard/keyboard-owner.ts +25 -0
  74. package/src/core/keyboard/keyboard-ownership.test.ts +69 -0
  75. package/src/core/keyboard/keyboard-ownership.ts +39 -0
  76. package/src/core/keyboard/keyboard-reset-guard.test.ts +33 -0
  77. package/src/core/keyboard/keyboard-reset-guard.ts +33 -0
  78. package/src/core/keyboard/keyboard-step.test.ts +66 -0
  79. package/src/core/keyboard/keyboard-step.ts +53 -0
  80. package/src/core/machine/define-sheet-machine.test.ts +187 -0
  81. package/src/core/machine/define-sheet-machine.ts +35 -0
  82. package/src/core/machine/index.ts +23 -0
  83. package/src/core/machine/machine.types.ts +80 -0
  84. package/src/core/machine/step-transition.test.ts +46 -0
  85. package/src/core/machine/step-transition.ts +59 -0
  86. package/src/core/machine/transition.test.ts +51 -0
  87. package/src/core/machine/transition.ts +58 -0
  88. package/src/core/result.test.ts +15 -0
  89. package/src/core/result.ts +20 -0
  90. package/src/core/scroll/scroll-lock.test.ts +40 -0
  91. package/src/core/scroll/scroll-lock.ts +35 -0
  92. package/src/core/scroll/scroll-pan.test.ts +99 -0
  93. package/src/core/scroll/scroll-pan.ts +96 -0
  94. package/src/core/sheet.types.ts +93 -0
  95. package/src/display-name.test.ts +181 -0
  96. package/src/docs.test.ts +45 -0
  97. package/src/flat-worklet.test.ts +130 -0
  98. package/src/gesture/AGENTS.md +71 -0
  99. package/src/gesture/gesture.types.ts +37 -0
  100. package/src/gesture/use-scroll-lock.ts +162 -0
  101. package/src/gesture/use-sheet-pan.ts +250 -0
  102. package/src/index.ts +127 -0
  103. package/src/keyboard/AGENTS.md +84 -0
  104. package/src/keyboard/use-bottom-sheet-text-input.ts +79 -0
  105. package/src/keyboard/use-keyboard-animation-guard.ts +59 -0
  106. package/src/keyboard/use-sheet-keyboard.ts +163 -0
  107. package/src/layout/AGENTS.md +52 -0
  108. package/src/layout/use-container-layout.ts +63 -0
  109. package/src/layout/use-measure-height.ts +40 -0
  110. package/src/lib/AGENTS.md +35 -0
  111. package/src/lib/compose-refs.ts +37 -0
  112. package/src/lib/merge-props.ts +42 -0
  113. package/src/lib/slot.tsx +44 -0
  114. package/src/lib/use-controllable-state.ts +45 -0
  115. package/src/no-classname.test.ts +27 -0
  116. package/src/portal/AGENTS.md +101 -0
  117. package/src/portal/bottom-sheet-host.tsx +67 -0
  118. package/src/portal/bottom-sheet-provider.tsx +33 -0
  119. package/src/portal/host.context.tsx +25 -0
  120. package/src/portal/sheet-registry.context.tsx +124 -0
  121. package/src/portal/sheet-registry.test.ts +87 -0
  122. package/src/portal/sheet-registry.ts +78 -0
  123. package/src/purity.test.ts +90 -0
  124. package/src/scrollable/AGENTS.md +123 -0
  125. package/src/scrollable/bottom-sheet-flat-list.tsx +15 -0
  126. package/src/scrollable/bottom-sheet-scroll-view.tsx +15 -0
  127. package/src/scrollable/bottom-sheet-section-list.tsx +22 -0
  128. package/src/scrollable/create-bottom-sheet-scrollable.tsx +248 -0
  129. package/src/scrollable/scrollable.types.ts +76 -0
  130. package/src/source-tree.test.ts +33 -0
  131. package/src/state/AGENTS.md +63 -0
  132. package/src/state/state.types.ts +134 -0
  133. package/src/state/use-detents.ts +43 -0
  134. package/src/state/use-sheet-geometry.ts +178 -0
  135. package/src/state/use-sheet-intents.ts +144 -0
  136. package/src/state/use-sheet-state.ts +157 -0
  137. package/src/steps/AGENTS.md +100 -0
  138. package/src/steps/bottom-sheet-step.tsx +61 -0
  139. package/src/steps/bottom-sheet-steps.tsx +215 -0
  140. package/src/steps/steps.context.tsx +73 -0
  141. package/src/steps/steps.types.ts +61 -0
  142. package/src/steps/use-sheet-machine.ts +70 -0
  143. package/src/steps/use-sheet-step.ts +29 -0
@@ -0,0 +1,30 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text } from "react-native";
3
+ import { Slot } from "../lib/slot";
4
+ import { useBottomSheetInternal } from "./bottom-sheet.context";
5
+ import type { BottomSheetTitleProps } from "./bottom-sheet.types";
6
+
7
+ /**
8
+ * The sheet's heading, and what labels the panel for a screen reader: it
9
+ * publishes a `nativeID` the container names in `accessibilityLabelledBy`, so
10
+ * focus entering the sheet announces its purpose first. A `Text` on its own;
11
+ * with `asChild`, the id and the heading role land on the child instead.
12
+ */
13
+ export function BottomSheetTitle({ asChild = false, children, ...props }: BottomSheetTitleProps): ReactElement {
14
+ const { titleId } = useBottomSheetInternal();
15
+
16
+ if (asChild) {
17
+ return (
18
+ <Slot accessibilityRole="header" nativeID={titleId} {...props}>
19
+ {children}
20
+ </Slot>
21
+ );
22
+ }
23
+
24
+ return (
25
+ <Text accessibilityRole="header" nativeID={titleId} {...props}>
26
+ {children}
27
+ </Text>
28
+ );
29
+ }
30
+ BottomSheetTitle.displayName = "DelacourBottomSheet.BottomSheet.Title";
@@ -0,0 +1,51 @@
1
+ import { type ReactElement, useCallback } from "react";
2
+ import { type GestureResponderEvent, Pressable } from "react-native";
3
+ import { Slot } from "../lib/slot";
4
+ import { useBottomSheet } from "./bottom-sheet.context";
5
+ import type { BottomSheetTriggerProps } from "./bottom-sheet.types";
6
+
7
+ /**
8
+ * The control that opens the sheet.
9
+ *
10
+ * On its own it is a React Native `Pressable`. With `asChild` it renders
11
+ * nothing and hands its `onPress` to the child instead, chained ahead of any
12
+ * the child already had — so `<BottomSheet.Trigger asChild><Button /></…>` is
13
+ * the button, with the button's own feedback and haptic, and no extra view.
14
+ *
15
+ * Donating the press rather than wrapping the child is deliberate. Two tap
16
+ * gestures in an ancestor/descendant pair are not simultaneous: Gesture
17
+ * Handler gives the press to the descendant, the button's own detector wins,
18
+ * and a wrapping trigger never fires. The corollary is that the child has to
19
+ * be something that handles `onPress`.
20
+ */
21
+ export function BottomSheetTrigger({
22
+ asChild = false,
23
+ children,
24
+ onPress,
25
+ ...props
26
+ }: BottomSheetTriggerProps): ReactElement {
27
+ const { open } = useBottomSheet();
28
+
29
+ const handlePress = useCallback(
30
+ (event: GestureResponderEvent) => {
31
+ open();
32
+ onPress?.(event);
33
+ },
34
+ [open, onPress]
35
+ );
36
+
37
+ if (asChild) {
38
+ return (
39
+ <Slot onPress={handlePress} {...props}>
40
+ {children}
41
+ </Slot>
42
+ );
43
+ }
44
+
45
+ return (
46
+ <Pressable accessibilityRole="button" onPress={handlePress} {...props}>
47
+ {children}
48
+ </Pressable>
49
+ );
50
+ }
51
+ BottomSheetTrigger.displayName = "DelacourBottomSheet.BottomSheet.Trigger";
@@ -0,0 +1,134 @@
1
+ import { createContext, type RefObject, useContext } from "react";
2
+ import type { AnimateTo, SheetAnimation } from "../animation/animation.types";
3
+ import type { DetachedOptions, ReduceMotionMode, ScrollableType, SheetEvent, SheetStepOverride } from "../core";
4
+ import type { SheetPans } from "../gesture/gesture.types";
5
+ import type { SheetKeyboardRegistry } from "../keyboard/use-sheet-keyboard";
6
+ import type { ContainerLayout } from "../layout/use-container-layout";
7
+ import type { ScrollableHandle } from "../scrollable/scrollable.types";
8
+ import type { SheetGeometry, SheetSharedState } from "../state/state.types";
9
+ import type { SheetIntentDispatch } from "../state/use-sheet-intents";
10
+ import type { SheetStepController } from "../steps/steps.types";
11
+ import type { BottomSheetAnimatedValue, BottomSheetContextValue } from "./bottom-sheet.types";
12
+
13
+ /** Who writes `contentHeight`: `Content`'s own layout, or a `Steps` body animating it. */
14
+ export type ContentHeightSource = "content" | "steps";
15
+
16
+ const OUTSIDE = (hook: string): Error =>
17
+ new Error(
18
+ `[@delacour/react-native-bottom-sheet] ${hook} outside <BottomSheet>. Every part reads its sheet from context.`
19
+ );
20
+
21
+ /** The open state and the imperative methods, for triggers, close buttons and consumers. */
22
+ export const BottomSheetContext = createContext<BottomSheetContextValue | null>(null);
23
+ BottomSheetContext.displayName = "DelacourBottomSheet.Context";
24
+
25
+ export function useBottomSheet(): BottomSheetContextValue {
26
+ const value = useContext(BottomSheetContext);
27
+ if (value === null) throw OUTSIDE("useBottomSheet");
28
+ return value;
29
+ }
30
+
31
+ export function useOptionalBottomSheet(): BottomSheetContextValue | null {
32
+ return useContext(BottomSheetContext);
33
+ }
34
+
35
+ /** The shared values a skin animates against — the position, the index, every measured height. */
36
+ export const BottomSheetAnimatedContext = createContext<BottomSheetAnimatedValue | null>(null);
37
+ BottomSheetAnimatedContext.displayName = "DelacourBottomSheet.AnimatedContext";
38
+
39
+ export function useBottomSheetAnimated(): BottomSheetAnimatedValue {
40
+ const value = useContext(BottomSheetAnimatedContext);
41
+ if (value === null) throw OUTSIDE("useBottomSheetAnimated");
42
+ return value;
43
+ }
44
+
45
+ export function useOptionalBottomSheetAnimated(): BottomSheetAnimatedValue | null {
46
+ return useContext(BottomSheetAnimatedContext);
47
+ }
48
+
49
+ /**
50
+ * Everything the parts share and a consumer should not need: the raw state,
51
+ * the geometry, the pans, the portal's mount flag and the accessibility ids.
52
+ * Exported for a part written outside this package — a skin's `Footer`, a
53
+ * scrollable built with `createBottomSheetScrollable` — never for an app.
54
+ */
55
+ export type BottomSheetInternalValue = {
56
+ state: SheetSharedState;
57
+ geometry: SheetGeometry;
58
+ pans: SheetPans;
59
+ animateTo: AnimateTo;
60
+ dispatch: SheetIntentDispatch;
61
+ containerLayout: ContainerLayout;
62
+ /** Whether the portal's children are mounted: open, closing, or kept. */
63
+ presented: boolean;
64
+ keepMounted: boolean;
65
+ topInset: number;
66
+ /** The root's `bottomInset`, for a part that reserves the resting safe-area band on the JS side. */
67
+ bottomInset: number;
68
+ /** The overlay tells the panel it exists, for `accessibilityViewIsModal`. */
69
+ hasOverlay: boolean;
70
+ setHasOverlay: (has: boolean) => void;
71
+ /** A handle sets this on mount; a container with none reports a zero handle height. */
72
+ handleMounted: RefObject<boolean>;
73
+ /** A sticky footer sets this on mount; the dynamic detent counts it and the body reserves space for it. */
74
+ hasFooter: boolean;
75
+ setHasFooter: (has: boolean) => void;
76
+ /** Where `useBottomSheetTextInput` registers its field. */
77
+ keyboard: SheetKeyboardRegistry;
78
+ enableHandlePanningGesture: boolean;
79
+ /** The root's prop, for the scroll lock: with no content pan there is nothing for a lock to hand a drag to. */
80
+ enableContentPanningGesture: boolean;
81
+ titleId: string;
82
+ descriptionId: string;
83
+ /**
84
+ * A scrollable built with `createBottomSheetScrollable` registers itself on
85
+ * focus and withdraws on blur; the sheet's content pan reads the type it
86
+ * wrote. One scrollable at a time — the last to focus wins.
87
+ */
88
+ setScrollableRef: (ref: RefObject<ScrollableHandle | null>, type: ScrollableType) => void;
89
+ removeScrollableRef: (ref: RefObject<ScrollableHandle | null>) => void;
90
+ /** The id the registry knows this sheet by, from `useId`. */
91
+ sheetId: string;
92
+ stackBehavior: "push" | "replace";
93
+ /** The resolved `detached` prop: `null` when attached. The panel and the body read it on the JS side. */
94
+ detached: DetachedOptions | null;
95
+ /** The root's `animation` and `overrideReduceMotion`, for a part that animates in step with the sheet. */
96
+ animation: SheetAnimation | undefined;
97
+ overrideReduceMotion: ReduceMotionMode | undefined;
98
+ /**
99
+ * `Content` measures itself into `contentHeight` only while this reads
100
+ * `content`; a `Steps` body sets `steps` and drives the value itself.
101
+ */
102
+ contentHeightSource: RefObject<ContentHeightSource>;
103
+ setContentHeightSource: (source: ContentHeightSource) => void;
104
+ /**
105
+ * What the current step asks of the root: its `snapPoints` in place of the
106
+ * prop, and `dismissible: false` to hold off pan-down-to-close and the
107
+ * overlay's press. `null` with no `Steps` body.
108
+ */
109
+ stepOverride: SheetStepOverride | null;
110
+ setStepOverride: (override: SheetStepOverride | null) => void;
111
+ /**
112
+ * The `Steps` body's controller, so `useSheetStep` reaches it from a
113
+ * `Footer` written beside the body. The ref is written during `Steps`'s
114
+ * render, so a footer rendered after it in the same pass already sees it;
115
+ * the state is set from a layout effect and is what re-renders a consumer
116
+ * that did not.
117
+ */
118
+ stepControllerRef: RefObject<SheetStepController<string, unknown, SheetEvent> | null>;
119
+ stepController: SheetStepController<string, unknown, SheetEvent> | null;
120
+ setStepController: (controller: SheetStepController<string, unknown, SheetEvent> | null) => void;
121
+ };
122
+
123
+ export const BottomSheetInternalContext = createContext<BottomSheetInternalValue | null>(null);
124
+ BottomSheetInternalContext.displayName = "DelacourBottomSheet.InternalContext";
125
+
126
+ export function useBottomSheetInternal(): BottomSheetInternalValue {
127
+ const value = useContext(BottomSheetInternalContext);
128
+ if (value === null) throw OUTSIDE("useBottomSheetInternal");
129
+ return value;
130
+ }
131
+
132
+ export function useOptionalBottomSheetInternal(): BottomSheetInternalValue | null {
133
+ return useContext(BottomSheetInternalContext);
134
+ }
@@ -0,0 +1,475 @@
1
+ import {
2
+ type ReactElement,
3
+ type RefObject,
4
+ useCallback,
5
+ useEffect,
6
+ useId,
7
+ useImperativeHandle,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from "react";
12
+ import { BackHandler } from "react-native";
13
+ import { useAnimateTo } from "../animation/use-animate-to";
14
+ import { useSettleCallbacks } from "../animation/use-settle-callbacks";
15
+ import {
16
+ CLOSED_INDEX,
17
+ indexForHeight,
18
+ resolveDetached,
19
+ SCROLLABLE_TYPE,
20
+ type ScrollableType,
21
+ type SheetEvent,
22
+ type SheetStepOverride,
23
+ } from "../core";
24
+ import { useSheetPan } from "../gesture/use-sheet-pan";
25
+ import { useSheetKeyboard } from "../keyboard/use-sheet-keyboard";
26
+ import { useContainerLayout } from "../layout/use-container-layout";
27
+ import { useControllableState } from "../lib/use-controllable-state";
28
+ import { BottomSheetHost } from "../portal/bottom-sheet-host";
29
+ import { BottomSheetProvider } from "../portal/bottom-sheet-provider";
30
+ import { isTop } from "../portal/sheet-registry";
31
+ import { useOptionalSheetRegistry } from "../portal/sheet-registry.context";
32
+ import { BottomSheetFlatList } from "../scrollable/bottom-sheet-flat-list";
33
+ import { BottomSheetScrollView } from "../scrollable/bottom-sheet-scroll-view";
34
+ import { BottomSheetSectionList } from "../scrollable/bottom-sheet-section-list";
35
+ import type { ScrollableHandle } from "../scrollable/scrollable.types";
36
+ import { ANIM_STATUS, type SheetWorkletConfig } from "../state/state.types";
37
+ import { useSheetGeometry } from "../state/use-sheet-geometry";
38
+ import { type IntentRequest, useSheetIntents } from "../state/use-sheet-intents";
39
+ import { useSheetState } from "../state/use-sheet-state";
40
+ import { BottomSheetStep } from "../steps/bottom-sheet-step";
41
+ import { BottomSheetSteps } from "../steps/bottom-sheet-steps";
42
+ import type { SheetStepController } from "../steps/steps.types";
43
+ import {
44
+ BottomSheetAnimatedContext,
45
+ BottomSheetContext,
46
+ BottomSheetInternalContext,
47
+ type BottomSheetInternalValue,
48
+ type ContentHeightSource,
49
+ } from "./bottom-sheet.context";
50
+ import type {
51
+ BottomSheetAnimatedValue,
52
+ BottomSheetContextValue,
53
+ BottomSheetProps,
54
+ BottomSheetRef,
55
+ } from "./bottom-sheet.types";
56
+ import { BottomSheetBackground } from "./bottom-sheet-background";
57
+ import { BottomSheetClose } from "./bottom-sheet-close";
58
+ import { BottomSheetContainer } from "./bottom-sheet-container";
59
+ import { BottomSheetContent } from "./bottom-sheet-content";
60
+ import { BottomSheetDescription } from "./bottom-sheet-description";
61
+ import { BottomSheetFooter } from "./bottom-sheet-footer";
62
+ import { BottomSheetHandle } from "./bottom-sheet-handle";
63
+ import { BottomSheetOverlay } from "./bottom-sheet-overlay";
64
+ import { BottomSheetPortal } from "./bottom-sheet-portal";
65
+ import { BottomSheetTextInput } from "./bottom-sheet-text-input";
66
+ import { BottomSheetTitle } from "./bottom-sheet-title";
67
+ import { BottomSheetTrigger } from "./bottom-sheet-trigger";
68
+
69
+ const EMPTY_SNAP_POINTS: readonly never[] = [];
70
+
71
+ /**
72
+ * The root: owns the open state, the shared values, the geometry, the
73
+ * animation and the pans, and hands all of it to the parts through three
74
+ * contexts. It renders no view of its own.
75
+ *
76
+ * **State opens, intents move.** `isOpen` — controlled or not — is the one
77
+ * source of truth for whether the sheet is open. Flipping it true mounts the
78
+ * portal's children and queues an `open` intent, which the UI thread resolves
79
+ * once every measurement has landed; a swipe down, an overlay press, a
80
+ * `Close`, or the ref's `close()` animate to the closed height, and the settle
81
+ * at `-1` is what flips `isOpen` back. Closing a sheet that is not open is
82
+ * nothing at all — there is no present/dismiss pair to deadlock.
83
+ *
84
+ * A sheet mid-open counts as open for a `close()`: the animation's target,
85
+ * not the last settled index, is what the resolver is told. Without that a
86
+ * close during the open animation would resolve to nothing and the sheet would
87
+ * finish opening.
88
+ */
89
+ function BottomSheetRoot({
90
+ children,
91
+ ref,
92
+ isOpen: isOpenProp,
93
+ defaultOpen = false,
94
+ onOpenChange,
95
+ index: indexProp,
96
+ initialIndex = 0,
97
+ onIndexChange,
98
+ onAnimate,
99
+ onClose,
100
+ snapPoints = EMPTY_SNAP_POINTS,
101
+ dynamicSizing = true,
102
+ maxDynamicContentSize,
103
+ topInset = 0,
104
+ bottomInset = 0,
105
+ detached,
106
+ enablePanDownToClose = true,
107
+ enableHandlePanningGesture = true,
108
+ enableContentPanningGesture = true,
109
+ enableOverDrag = true,
110
+ overDragResistanceFactor = 2.5,
111
+ keyboardBehavior = "interactive",
112
+ keyboardBlurBehavior = "restore",
113
+ keyboardScope = "inside",
114
+ enableBlurKeyboardOnGesture = true,
115
+ animation,
116
+ overrideReduceMotion,
117
+ animateOnMount = true,
118
+ keepMounted = false,
119
+ stackBehavior = "push",
120
+ closeOnBack = true,
121
+ onDetentHaptic,
122
+ onCloseHaptic,
123
+ onOverDragHaptic,
124
+ }: BottomSheetProps): ReactElement {
125
+ const [isOpen, setOpen] = useControllableState<boolean>({
126
+ value: isOpenProp,
127
+ defaultValue: defaultOpen,
128
+ onChange: onOpenChange,
129
+ });
130
+ const [presented, setPresented] = useState(isOpen);
131
+ const [index, setIndex] = useState(CLOSED_INDEX);
132
+ const [detentCount, setDetentCount] = useState(0);
133
+ const [hasOverlay, setHasOverlay] = useState(false);
134
+ const [hasFooter, setHasFooter] = useState(false);
135
+ const handleMounted = useRef(false);
136
+ const titleId = useId();
137
+ const descriptionId = useId();
138
+ const sheetId = useId();
139
+ const registry = useOptionalSheetRegistry();
140
+
141
+ const detachedOptions = useMemo(() => resolveDetached(detached), [detached]);
142
+
143
+ // A `Steps` body may override the detents and the dismissibility for the
144
+ // step it is on. A step that names its `snapPoints` is sized by them, so
145
+ // dynamic sizing is off while it is current.
146
+ const [stepOverride, setStepOverride] = useState<SheetStepOverride | null>(null);
147
+ const [stepController, setStepController] = useState<SheetStepController<string, unknown, SheetEvent> | null>(null);
148
+ const stepControllerRef = useRef<SheetStepController<string, unknown, SheetEvent> | null>(null);
149
+ const contentHeightSource = useRef<ContentHeightSource>("content");
150
+ const setContentHeightSource = useCallback((source: ContentHeightSource) => {
151
+ contentHeightSource.current = source;
152
+ }, []);
153
+ const effectiveSnapPoints = stepOverride?.snapPoints ?? snapPoints;
154
+ const effectiveDynamicSizing = stepOverride?.snapPoints === undefined ? dynamicSizing : false;
155
+ const effectivePanDownToClose = stepOverride?.dismissible === false ? false : enablePanDownToClose;
156
+
157
+ const config = useMemo<SheetWorkletConfig>(
158
+ () => ({
159
+ dynamicSizing: effectiveDynamicSizing,
160
+ maxDynamicContentSize,
161
+ hasFooter,
162
+ bottomInset,
163
+ detached: detachedOptions,
164
+ enablePanDownToClose: effectivePanDownToClose,
165
+ enableOverDrag,
166
+ overDragResistanceFactor,
167
+ initialIndex,
168
+ animateOnMount,
169
+ keyboardBehavior,
170
+ keyboardBlurBehavior,
171
+ keyboardScope,
172
+ enableBlurKeyboardOnGesture,
173
+ }),
174
+ [
175
+ effectiveDynamicSizing,
176
+ maxDynamicContentSize,
177
+ hasFooter,
178
+ bottomInset,
179
+ detachedOptions,
180
+ effectivePanDownToClose,
181
+ enableOverDrag,
182
+ overDragResistanceFactor,
183
+ initialIndex,
184
+ animateOnMount,
185
+ keyboardBehavior,
186
+ keyboardBlurBehavior,
187
+ keyboardScope,
188
+ enableBlurKeyboardOnGesture,
189
+ ]
190
+ );
191
+
192
+ const state = useSheetState(effectiveSnapPoints, config);
193
+ const geometry = useSheetGeometry(state);
194
+ const listeners = useSettleCallbacks({
195
+ isOpen,
196
+ setOpen,
197
+ setPresented,
198
+ setIndex,
199
+ setDetentCount,
200
+ onIndexChange,
201
+ onClose,
202
+ onAnimate,
203
+ });
204
+ const { animateTo, jumpTo, settleAt } = useAnimateTo({
205
+ state,
206
+ geometry,
207
+ animation,
208
+ overrideReduceMotion,
209
+ onSettle: listeners.onSettle,
210
+ onAnimate: listeners.onAnimate,
211
+ });
212
+ const intents = useSheetIntents(state, geometry, animateTo, jumpTo);
213
+ const { dispatch } = intents;
214
+ const pans = useSheetPan(state, geometry, animateTo, settleAt, {
215
+ enableHandlePanningGesture,
216
+ enableContentPanningGesture,
217
+ onDetentHaptic,
218
+ onCloseHaptic,
219
+ onOverDragHaptic,
220
+ });
221
+ const containerLayout = useContainerLayout(state);
222
+ const keyboard = useSheetKeyboard({ state, geometry, animateTo, presented });
223
+
224
+ // One scrollable at a time: the last to focus is the body the content pan
225
+ // reasons about, and its withdrawal puts the offset back to the top.
226
+ const scrollableRef = useRef<RefObject<ScrollableHandle | null> | null>(null);
227
+ const setScrollableRef = useCallback(
228
+ (ref: RefObject<ScrollableHandle | null>, type: ScrollableType) => {
229
+ scrollableRef.current = ref;
230
+ state.scrollableType.value = type;
231
+ },
232
+ [state]
233
+ );
234
+ const removeScrollableRef = useCallback(
235
+ (ref: RefObject<ScrollableHandle | null>) => {
236
+ if (scrollableRef.current !== ref) return;
237
+ scrollableRef.current = null;
238
+ state.scrollableType.value = SCROLLABLE_TYPE.NONE;
239
+ state.scrollOffsetY.value = 0;
240
+ state.scrollLockedAt.value = 0;
241
+ },
242
+ [state]
243
+ );
244
+
245
+ // Open for the purpose of a close: settled above the closed height, or on
246
+ // the way there.
247
+ const isEffectivelyOpen = useCallback((): boolean => {
248
+ if (state.animStatus.value === ANIM_STATUS.RUNNING) {
249
+ return indexForHeight(state.animTarget.value, geometry.detents.value, geometry.closedHeight.value) > CLOSED_INDEX;
250
+ }
251
+ return state.currentIndex.value > CLOSED_INDEX;
252
+ }, [state, geometry]);
253
+
254
+ const latestOpen = useRef(isOpen);
255
+ latestOpen.current = isOpen;
256
+ const moveQueued = useRef(false);
257
+
258
+ // The queue is judged by the JS-side record of the last request, never by
259
+ // reading the shared value back: a JS write is queued to the UI thread, so
260
+ // the read would still show the request before it. See `useSheetIntents`.
261
+ useEffect(() => {
262
+ if (isOpen) {
263
+ setPresented(true);
264
+ // A move queued by the ref on a closed sheet is a more specific open;
265
+ // it waits on the same layout and lands on its own detent.
266
+ if (!moveQueued.current) dispatch({ kind: "open" });
267
+ moveQueued.current = false;
268
+ return;
269
+ }
270
+ if (isEffectivelyOpen()) {
271
+ dispatch({ kind: "close" });
272
+ return;
273
+ }
274
+ // A queued open the state changed its mind about, before layout landed.
275
+ if (intents.lastKind() === "open") intents.clear();
276
+ }, [isOpen, dispatch, intents, isEffectivelyOpen]);
277
+
278
+ useEffect(() => {
279
+ if (indexProp !== undefined && isOpen) dispatch({ kind: "snapToIndex", index: indexProp });
280
+ }, [indexProp, isOpen, dispatch]);
281
+
282
+ const methods = useMemo<BottomSheetRef>(() => {
283
+ const close = (): void => {
284
+ if (isEffectivelyOpen()) {
285
+ dispatch({ kind: "close" });
286
+ return;
287
+ }
288
+ if (latestOpen.current) setOpen(false);
289
+ };
290
+ // A move on a closed sheet is an open: the state flips first so the
291
+ // portal mounts, and the queued intent lands on the requested detent.
292
+ const move = (request: IntentRequest): void => {
293
+ if (!isEffectivelyOpen() && !latestOpen.current) {
294
+ moveQueued.current = true;
295
+ setOpen(true);
296
+ }
297
+ dispatch(request);
298
+ };
299
+ return {
300
+ open: () => setOpen(true),
301
+ close,
302
+ dismiss: close,
303
+ forceClose: () => {
304
+ if (isEffectivelyOpen()) dispatch({ kind: "forceClose" });
305
+ else if (latestOpen.current) setOpen(false);
306
+ },
307
+ snapToIndex: (target) => (target < 0 ? close() : move({ kind: "snapToIndex", index: target })),
308
+ snapToPosition: (position) => move({ kind: "snapToPosition", position }),
309
+ expand: () => move({ kind: "expand" }),
310
+ collapse: () => move({ kind: "collapse" }),
311
+ };
312
+ }, [dispatch, setOpen, isEffectivelyOpen]);
313
+
314
+ useImperativeHandle(ref, () => methods, [methods]);
315
+
316
+ // The registry closes sheets it holds no ref to — `dismissAll`, a `replace`
317
+ // open in the same host — through the same methods the ref exposes.
318
+ const register = registry?.register;
319
+ useEffect(() => {
320
+ if (register === undefined) return;
321
+ return register(sheetId, { close: methods.close, forceClose: methods.forceClose });
322
+ }, [register, sheetId, methods]);
323
+
324
+ // Android's back button closes the top sheet of its host and nothing else:
325
+ // without a registry every sheet is its own top. Subscribed only while
326
+ // presented, so a closed sheet never swallows a press.
327
+ const registryTop = registry === null ? true : isTop(registry.state, sheetId);
328
+ useEffect(() => {
329
+ if (!presented || !closeOnBack || !registryTop) return;
330
+ const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
331
+ methods.close();
332
+ return true;
333
+ });
334
+ return () => subscription.remove();
335
+ }, [presented, closeOnBack, registryTop, methods]);
336
+
337
+ const contextValue = useMemo<BottomSheetContextValue>(
338
+ () => ({ ...methods, isOpen, setOpen, index, detentCount }),
339
+ [methods, isOpen, setOpen, index, detentCount]
340
+ );
341
+
342
+ const animatedValue = useMemo<BottomSheetAnimatedValue>(
343
+ () => ({
344
+ animatedIndex: geometry.index,
345
+ animatedPosition: geometry.position,
346
+ animatedHeight: geometry.height,
347
+ containerHeight: state.containerHeight,
348
+ handleHeight: state.handleHeight,
349
+ contentHeight: state.contentHeight,
350
+ footerHeight: geometry.footerHeight,
351
+ keyboardLift: geometry.keyboardLift,
352
+ detents: geometry.detents,
353
+ closedHeight: geometry.closedHeight,
354
+ }),
355
+ [geometry, state]
356
+ );
357
+
358
+ const internalValue = useMemo<BottomSheetInternalValue>(
359
+ () => ({
360
+ state,
361
+ geometry,
362
+ pans,
363
+ animateTo,
364
+ dispatch,
365
+ containerLayout,
366
+ presented,
367
+ keepMounted,
368
+ topInset,
369
+ bottomInset,
370
+ hasOverlay,
371
+ setHasOverlay,
372
+ hasFooter,
373
+ setHasFooter,
374
+ keyboard,
375
+ handleMounted,
376
+ enableHandlePanningGesture,
377
+ enableContentPanningGesture,
378
+ setScrollableRef,
379
+ removeScrollableRef,
380
+ titleId,
381
+ descriptionId,
382
+ sheetId,
383
+ stackBehavior,
384
+ detached: detachedOptions,
385
+ animation,
386
+ overrideReduceMotion,
387
+ contentHeightSource,
388
+ setContentHeightSource,
389
+ stepOverride,
390
+ setStepOverride,
391
+ stepControllerRef,
392
+ stepController,
393
+ setStepController,
394
+ }),
395
+ [
396
+ state,
397
+ geometry,
398
+ pans,
399
+ animateTo,
400
+ dispatch,
401
+ containerLayout,
402
+ presented,
403
+ keepMounted,
404
+ topInset,
405
+ bottomInset,
406
+ hasOverlay,
407
+ hasFooter,
408
+ keyboard,
409
+ enableHandlePanningGesture,
410
+ enableContentPanningGesture,
411
+ setScrollableRef,
412
+ removeScrollableRef,
413
+ titleId,
414
+ descriptionId,
415
+ sheetId,
416
+ stackBehavior,
417
+ detachedOptions,
418
+ animation,
419
+ overrideReduceMotion,
420
+ setContentHeightSource,
421
+ stepOverride,
422
+ stepController,
423
+ ]
424
+ );
425
+
426
+ return (
427
+ <BottomSheetContext.Provider value={contextValue}>
428
+ <BottomSheetAnimatedContext.Provider value={animatedValue}>
429
+ <BottomSheetInternalContext.Provider value={internalValue}>{children}</BottomSheetInternalContext.Provider>
430
+ </BottomSheetAnimatedContext.Provider>
431
+ </BottomSheetContext.Provider>
432
+ );
433
+ }
434
+
435
+ /**
436
+ * A headless bottom sheet.
437
+ *
438
+ * @example
439
+ * <BottomSheet snapPoints={["40%", "85%"]} dynamicSizing={false}>
440
+ * <BottomSheet.Trigger asChild><Button /></BottomSheet.Trigger>
441
+ * <BottomSheet.Portal inline>
442
+ * <BottomSheet.Overlay style={{ backgroundColor: "#0008" }} />
443
+ * <BottomSheet.Container>
444
+ * <BottomSheet.Background style={{ backgroundColor: "white", borderTopLeftRadius: 16, borderTopRightRadius: 16 }} />
445
+ * <BottomSheet.Handle style={{ padding: 12 }}><View style={pill} /></BottomSheet.Handle>
446
+ * <BottomSheet.Content style={{ padding: 16 }}>
447
+ * <BottomSheet.Title>Title</BottomSheet.Title>
448
+ * <BottomSheet.Close><Text>Close</Text></BottomSheet.Close>
449
+ * </BottomSheet.Content>
450
+ * </BottomSheet.Container>
451
+ * </BottomSheet.Portal>
452
+ * </BottomSheet>
453
+ */
454
+ export const BottomSheet = Object.assign(BottomSheetRoot, {
455
+ Trigger: BottomSheetTrigger,
456
+ Portal: BottomSheetPortal,
457
+ Overlay: BottomSheetOverlay,
458
+ Container: BottomSheetContainer,
459
+ Background: BottomSheetBackground,
460
+ Handle: BottomSheetHandle,
461
+ Content: BottomSheetContent,
462
+ Close: BottomSheetClose,
463
+ Title: BottomSheetTitle,
464
+ Description: BottomSheetDescription,
465
+ Footer: BottomSheetFooter,
466
+ TextInput: BottomSheetTextInput,
467
+ ScrollView: BottomSheetScrollView,
468
+ FlatList: BottomSheetFlatList,
469
+ SectionList: BottomSheetSectionList,
470
+ Host: BottomSheetHost,
471
+ Provider: BottomSheetProvider,
472
+ Steps: BottomSheetSteps,
473
+ Step: BottomSheetStep,
474
+ displayName: "DelacourBottomSheet.BottomSheet",
475
+ });