@delacour/react-native-bottom-sheet 0.2.0 → 0.3.0-alpha.20261023075309
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/package.json +1 -1
- package/src/components/bottom-sheet.tsx +3 -0
- package/src/components/bottom-sheet.types.ts +4 -1
- package/src/core/AGENTS.md +4 -3
- package/src/core/index.ts +8 -1
- package/src/core/keyboard/keyboard-lift.test.ts +46 -1
- package/src/core/keyboard/keyboard-lift.ts +36 -2
- package/src/core/sheet.types.ts +9 -0
- package/src/state/state.types.ts +2 -0
- package/src/state/use-sheet-geometry.ts +25 -1
- package/src/state/use-sheet-state.ts +3 -0
package/package.json
CHANGED
|
@@ -110,6 +110,7 @@ function BottomSheetRoot({
|
|
|
110
110
|
overDragResistanceFactor = 2.5,
|
|
111
111
|
keyboardBehavior = "interactive",
|
|
112
112
|
keyboardBlurBehavior = "restore",
|
|
113
|
+
keyboardLiftLimit = "container",
|
|
113
114
|
keyboardScope = "inside",
|
|
114
115
|
enableBlurKeyboardOnGesture = true,
|
|
115
116
|
animation,
|
|
@@ -168,6 +169,7 @@ function BottomSheetRoot({
|
|
|
168
169
|
animateOnMount,
|
|
169
170
|
keyboardBehavior,
|
|
170
171
|
keyboardBlurBehavior,
|
|
172
|
+
keyboardLiftLimit,
|
|
171
173
|
keyboardScope,
|
|
172
174
|
enableBlurKeyboardOnGesture,
|
|
173
175
|
}),
|
|
@@ -184,6 +186,7 @@ function BottomSheetRoot({
|
|
|
184
186
|
animateOnMount,
|
|
185
187
|
keyboardBehavior,
|
|
186
188
|
keyboardBlurBehavior,
|
|
189
|
+
keyboardLiftLimit,
|
|
187
190
|
keyboardScope,
|
|
188
191
|
enableBlurKeyboardOnGesture,
|
|
189
192
|
]
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
DetachedProp,
|
|
15
15
|
KeyboardBehavior,
|
|
16
16
|
KeyboardBlurBehavior,
|
|
17
|
+
KeyboardLiftLimit,
|
|
17
18
|
KeyboardScope,
|
|
18
19
|
ReduceMotionMode,
|
|
19
20
|
SnapPointSpec,
|
|
@@ -75,7 +76,7 @@ export type BottomSheetProps = SheetHaptics & {
|
|
|
75
76
|
snapPoints?: readonly SnapPointSpec[];
|
|
76
77
|
/** Adds the content's own height as a snap point. @default true */
|
|
77
78
|
dynamicSizing?: boolean;
|
|
78
|
-
/** Caps the dynamic snap point. */
|
|
79
|
+
/** Caps the dynamic snap point, and how high a keyboard may lift the sheet. */
|
|
79
80
|
maxDynamicContentSize?: number;
|
|
80
81
|
/** Pixels the frame leaves clear at the top. @default 0 */
|
|
81
82
|
topInset?: number;
|
|
@@ -103,6 +104,8 @@ export type BottomSheetProps = SheetHaptics & {
|
|
|
103
104
|
keyboardBehavior?: KeyboardBehavior;
|
|
104
105
|
/** @default "restore" */
|
|
105
106
|
keyboardBlurBehavior?: KeyboardBlurBehavior;
|
|
107
|
+
/** Whether `maxDynamicContentSize` also stops the keyboard's lift. @default "container" */
|
|
108
|
+
keyboardLiftLimit?: KeyboardLiftLimit;
|
|
106
109
|
/** @default "inside" */
|
|
107
110
|
keyboardScope?: KeyboardScope;
|
|
108
111
|
/** @default true */
|
package/src/core/AGENTS.md
CHANGED
|
@@ -33,7 +33,7 @@ at the moment a view needs it, by `geometry/position.ts`, and nowhere else.
|
|
|
33
33
|
| `snap-points/select-snap-height.ts` | W `selectSnapHeight` — nearest candidate to `height + projection · velocity`; closes only when given a closed height |
|
|
34
34
|
| `snap-points/over-drag.ts` | W `resistOverDrag` — slope 1 at the edge, square-root beyond; factor 0 clamps |
|
|
35
35
|
| `snap-points/sheet-state.ts` | W `sheetState` — closed / opened / extended / overExtended / fill |
|
|
36
|
-
| `keyboard/keyboard-lift.ts` | W `keyboardInContainer`, W `keyboardLift` — the lift, band and gap subtracted in step with progress |
|
|
36
|
+
| `keyboard/keyboard-lift.ts` | W `keyboardInContainer`, W `keyboardLift` — the lift, band and gap subtracted in step with progress, stopped at the ceiling; W `keyboardCeiling` — a capped dynamic sheet is lifted no higher than its cap |
|
|
37
37
|
| `keyboard/keyboard-owner.ts` | W `isInputInsideSheet` — the `keyboardScope: "inside"` geometry test |
|
|
38
38
|
| `keyboard/content-area.ts` | W `contentArea` — what the body may fill, never negative |
|
|
39
39
|
| `keyboard/container-layout-guard.ts` | `acceptContainerLayout` — refuses Android's adjustResize double-count |
|
|
@@ -71,11 +71,12 @@ band = detached ? 0 : inset bandNow = band · (
|
|
|
71
71
|
footerHeight = hasFooter ? footerContent + bandNow : 0
|
|
72
72
|
dynamicSnapPoint = min(handle + content + (hasFooter ? footerContent : 0) + band, maxDynamicContentSize, available)
|
|
73
73
|
snapPoints = sortAsc(unique(normalize(spec, available) ∪ {dynamicSnapPoint if dynamicSizing}))
|
|
74
|
-
|
|
74
|
+
ceiling = keyboardLiftLimit = maxDynamicContentSize && dynamicSizing && maxDynamicContentSize ? min(maxDynamicContentSize, maxHeight) : maxHeight
|
|
75
|
+
keyboardLift = behavior ∈ {interactive, extend} ? clamp(kb − band · p − (detached ? restingBottom : 0), 0, ceiling − base) : 0
|
|
75
76
|
height = clamp(base + keyboardLift, closedHeight, maxHeight)
|
|
76
77
|
translateY = C − restingBottom − height
|
|
77
78
|
index = piecewise(base, [closedHeight, …snapPoints] → [−1, 0, 1, …]) // base, never height
|
|
78
|
-
contentArea = max(0, min(maxHeight, highest + keyboardLift) − handle − footerHeight − kb − (hasFooter ? 0 : bandNow))
|
|
79
|
+
contentArea = max(0, min(maxHeight, highest + keyboardLift(base = highest)) − handle − footerHeight − kb − (hasFooter ? 0 : bandNow))
|
|
79
80
|
footerTop = max(0, height − kb − footerContent − bandNow)
|
|
80
81
|
bodyInset = hasFooter ? footerHeight : bandNow
|
|
81
82
|
bodyClip = hasFooter ? max(0, min(contentArea + bodyInset, height − kb − handle − bodyInset)) : contentArea + bodyInset
|
package/src/core/index.ts
CHANGED
|
@@ -31,7 +31,13 @@ export { isLayoutReady, type LayoutReadyInput } from "./intent/layout-ready";
|
|
|
31
31
|
export { type IntentResolution, type IntentState, resolveIntent } from "./intent/resolve-intent";
|
|
32
32
|
export { acceptContainerLayout, type ContainerLayoutInput } from "./keyboard/container-layout-guard";
|
|
33
33
|
export { type ContentAreaInput, contentArea } from "./keyboard/content-area";
|
|
34
|
-
export {
|
|
34
|
+
export {
|
|
35
|
+
type KeyboardCeilingInput,
|
|
36
|
+
type KeyboardLiftInput,
|
|
37
|
+
keyboardCeiling,
|
|
38
|
+
keyboardInContainer,
|
|
39
|
+
keyboardLift,
|
|
40
|
+
} from "./keyboard/keyboard-lift";
|
|
35
41
|
export { type InputInsideSheetInput, isInputInsideSheet } from "./keyboard/keyboard-owner";
|
|
36
42
|
export { type KeyboardOwnerInput, resolveKeyboardOwner } from "./keyboard/keyboard-ownership";
|
|
37
43
|
export { type KeyboardAnimationState, shouldResetKeyboardAnimation } from "./keyboard/keyboard-reset-guard";
|
|
@@ -75,6 +81,7 @@ export {
|
|
|
75
81
|
type GestureSource,
|
|
76
82
|
type KeyboardBehavior,
|
|
77
83
|
type KeyboardBlurBehavior,
|
|
84
|
+
type KeyboardLiftLimit,
|
|
78
85
|
type KeyboardScope,
|
|
79
86
|
SCROLLABLE_TYPE,
|
|
80
87
|
type ScrollableType,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type { KeyboardBehavior } from "../sheet.types";
|
|
3
|
-
import { keyboardInContainer, keyboardLift } from "./keyboard-lift";
|
|
3
|
+
import { keyboardCeiling, keyboardInContainer, keyboardLift } from "./keyboard-lift";
|
|
4
4
|
|
|
5
5
|
describe("keyboardInContainer", () => {
|
|
6
6
|
test("keyboard-controller's height is negative; the container's share is positive", () => {
|
|
@@ -52,3 +52,48 @@ describe("keyboardLift", () => {
|
|
|
52
52
|
expect(keyboardLift({ ...input, keyboardHeight: 0, progress: 0, behavior: "interactive" })).toBe(0);
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
describe("keyboardLift under a ceiling", () => {
|
|
57
|
+
const input = { keyboardHeight: 336, progress: 1, band: 0, gapBelow: 0, behavior: "interactive" as const };
|
|
58
|
+
|
|
59
|
+
test("a short sheet lifts until its top reaches the ceiling, and no further", () => {
|
|
60
|
+
expect(keyboardLift({ ...input, base: 600, ceiling: 724 })).toBe(124);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("a sheet already at the ceiling stays where it is; the keyboard covers its bottom", () => {
|
|
64
|
+
expect(keyboardLift({ ...input, base: 724, ceiling: 724 })).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a snap point above the ceiling is never pulled down — the lift is zero, not negative", () => {
|
|
68
|
+
expect(keyboardLift({ ...input, base: 800, ceiling: 724 })).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("a ceiling the lift never reaches changes nothing", () => {
|
|
72
|
+
expect(keyboardLift({ ...input, base: 200, ceiling: 724 })).toBe(336);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("without a ceiling the lift is the keyboard's", () => {
|
|
76
|
+
expect(keyboardLift({ ...input, base: 724 })).toBe(336);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("keyboardCeiling", () => {
|
|
81
|
+
const capped = { limit: "maxDynamicContentSize", dynamicSizing: true, maxHeight: 852 } as const;
|
|
82
|
+
|
|
83
|
+
test("a dynamic sheet with a cap is lifted no higher than the cap", () => {
|
|
84
|
+
expect(keyboardCeiling({ ...capped, maxDynamicContentSize: 724 })).toBe(724);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a cap above the container changes nothing", () => {
|
|
88
|
+
expect(keyboardCeiling({ ...capped, maxDynamicContentSize: 900 })).toBe(852);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("no cap, or explicit snap points, leave the container as the ceiling", () => {
|
|
92
|
+
expect(keyboardCeiling({ ...capped, maxDynamicContentSize: undefined })).toBe(852);
|
|
93
|
+
expect(keyboardCeiling({ ...capped, dynamicSizing: false, maxDynamicContentSize: 724 })).toBe(852);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("the default limit is the container, cap or no cap", () => {
|
|
97
|
+
expect(keyboardCeiling({ ...capped, limit: "container", maxDynamicContentSize: 724 })).toBe(852);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { KeyboardBehavior } from "../sheet.types";
|
|
1
|
+
import type { KeyboardBehavior, KeyboardLiftLimit } from "../sheet.types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* How much of the keyboard overlaps the sheet's container.
|
|
@@ -24,6 +24,10 @@ export type KeyboardLiftInput = {
|
|
|
24
24
|
behavior: KeyboardBehavior;
|
|
25
25
|
/** A detached sheet's resting bottom — pixels it already floats above the edge. */
|
|
26
26
|
gapBelow: number;
|
|
27
|
+
/** The keyboard-free height the lift sits on. Read only with `ceiling`. */
|
|
28
|
+
base?: number;
|
|
29
|
+
/** The height the lift may not carry `base` past — see `keyboardCeiling`. */
|
|
30
|
+
ceiling?: number;
|
|
27
31
|
};
|
|
28
32
|
|
|
29
33
|
/**
|
|
@@ -43,5 +47,35 @@ export function keyboardLift(input: KeyboardLiftInput): number {
|
|
|
43
47
|
const band = input.band > 0 ? input.band : 0;
|
|
44
48
|
const gap = input.gapBelow > 0 ? input.gapBelow : 0;
|
|
45
49
|
const lift = input.keyboardHeight - band * progress - gap;
|
|
46
|
-
|
|
50
|
+
if (!(lift > 0)) return 0;
|
|
51
|
+
if (input.ceiling === undefined) return lift;
|
|
52
|
+
const room = input.ceiling - (input.base ?? 0);
|
|
53
|
+
if (!(room > 0)) return 0;
|
|
54
|
+
return lift < room ? lift : room;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type KeyboardCeilingInput = {
|
|
58
|
+
limit: KeyboardLiftLimit;
|
|
59
|
+
dynamicSizing: boolean;
|
|
60
|
+
maxDynamicContentSize: number | undefined;
|
|
61
|
+
/** The container's available height. */
|
|
62
|
+
maxHeight: number;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The height a keyboard may lift the sheet to.
|
|
67
|
+
*
|
|
68
|
+
* The container's top, unless `limit` is `maxDynamicContentSize` and the sheet
|
|
69
|
+
* sizes to its content under that cap: then the lift stops at the cap and the
|
|
70
|
+
* keyboard covers the sheet's bottom instead, the body shrinking by it as
|
|
71
|
+
* always. Decided on the UI thread, in the frame the keyboard moves — a caller
|
|
72
|
+
* shrinking the cap from a JS keyboard event lands a few frames late, and the
|
|
73
|
+
* sheet overshoots to the container's top before settling.
|
|
74
|
+
*/
|
|
75
|
+
export function keyboardCeiling(input: KeyboardCeilingInput): number {
|
|
76
|
+
"worklet";
|
|
77
|
+
const cap = input.maxDynamicContentSize;
|
|
78
|
+
if (input.limit !== "maxDynamicContentSize" || !input.dynamicSizing || cap === undefined) return input.maxHeight;
|
|
79
|
+
const bounded = cap > 0 ? cap : 0;
|
|
80
|
+
return bounded < input.maxHeight ? bounded : input.maxHeight;
|
|
47
81
|
}
|
package/src/core/sheet.types.ts
CHANGED
|
@@ -22,6 +22,15 @@ export type KeyboardBehavior = "interactive" | "extend" | "fillParent" | "none";
|
|
|
22
22
|
/** What the sheet does when the keyboard it lifted for goes away. */
|
|
23
23
|
export type KeyboardBlurBehavior = "none" | "restore";
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* How high a keyboard may lift the sheet.
|
|
27
|
+
*
|
|
28
|
+
* `container` — to the container's top. `maxDynamicContentSize` — a sheet
|
|
29
|
+
* sized to its content stops at that cap, and the keyboard covers its bottom
|
|
30
|
+
* instead; for a caller whose cap already keeps the page in view.
|
|
31
|
+
*/
|
|
32
|
+
export type KeyboardLiftLimit = "container" | "maxDynamicContentSize";
|
|
33
|
+
|
|
25
34
|
/**
|
|
26
35
|
* Which focused inputs count as the sheet's own.
|
|
27
36
|
*
|
package/src/state/state.types.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
GestureSource,
|
|
6
6
|
KeyboardBehavior,
|
|
7
7
|
KeyboardBlurBehavior,
|
|
8
|
+
KeyboardLiftLimit,
|
|
8
9
|
KeyboardScope,
|
|
9
10
|
ScrollableType,
|
|
10
11
|
SheetIntent,
|
|
@@ -38,6 +39,7 @@ export type SheetWorkletConfig = {
|
|
|
38
39
|
animateOnMount: boolean;
|
|
39
40
|
keyboardBehavior: KeyboardBehavior;
|
|
40
41
|
keyboardBlurBehavior: KeyboardBlurBehavior;
|
|
42
|
+
keyboardLiftLimit: KeyboardLiftLimit;
|
|
41
43
|
keyboardScope: KeyboardScope;
|
|
42
44
|
enableBlurKeyboardOnGesture: boolean;
|
|
43
45
|
};
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
footerTop,
|
|
14
14
|
indexForHeight,
|
|
15
15
|
isLayoutReady,
|
|
16
|
+
keyboardCeiling,
|
|
16
17
|
keyboardLift,
|
|
17
18
|
positionFor,
|
|
18
19
|
restingBottom,
|
|
@@ -56,6 +57,17 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
|
|
|
56
57
|
const keyboardEffective = useDerivedValue(() =>
|
|
57
58
|
state.keyboardOwned.value && state.config.value.keyboardBehavior !== "none" ? state.keyboardHeight.value : 0
|
|
58
59
|
);
|
|
60
|
+
// Under `keyboardLiftLimit="maxDynamicContentSize"` a capped dynamic sheet is
|
|
61
|
+
// lifted no higher than its cap — decided here, on the UI thread, so it holds
|
|
62
|
+
// from the keyboard's first frame.
|
|
63
|
+
const ceiling = useDerivedValue(() =>
|
|
64
|
+
keyboardCeiling({
|
|
65
|
+
limit: state.config.value.keyboardLiftLimit,
|
|
66
|
+
dynamicSizing: state.config.value.dynamicSizing,
|
|
67
|
+
maxDynamicContentSize: state.config.value.maxDynamicContentSize,
|
|
68
|
+
maxHeight: maxHeight.value,
|
|
69
|
+
})
|
|
70
|
+
);
|
|
59
71
|
const lift = useDerivedValue(() =>
|
|
60
72
|
keyboardLift({
|
|
61
73
|
keyboardHeight: keyboardEffective.value,
|
|
@@ -63,6 +75,8 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
|
|
|
63
75
|
band: band.value,
|
|
64
76
|
behavior: state.config.value.keyboardBehavior,
|
|
65
77
|
gapBelow: resting.value,
|
|
78
|
+
base: state.base.value,
|
|
79
|
+
ceiling: ceiling.value,
|
|
66
80
|
})
|
|
67
81
|
);
|
|
68
82
|
const height = useDerivedValue(() => clampHeight(state.base.value + lift.value, closed.value, maxHeight.value));
|
|
@@ -92,7 +106,17 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
|
|
|
92
106
|
// never reflows during a snap between snap points — but a `fillParent` snap
|
|
93
107
|
// takes `base` above every snap point, and the body follows it up.
|
|
94
108
|
const area = useDerivedValue(() => {
|
|
95
|
-
const
|
|
109
|
+
const from = Math.max(highest.value, state.base.value);
|
|
110
|
+
const fromLift = keyboardLift({
|
|
111
|
+
keyboardHeight: keyboardEffective.value,
|
|
112
|
+
progress: state.keyboardProgress.value,
|
|
113
|
+
band: band.value,
|
|
114
|
+
behavior: state.config.value.keyboardBehavior,
|
|
115
|
+
gapBelow: resting.value,
|
|
116
|
+
base: from,
|
|
117
|
+
ceiling: ceiling.value,
|
|
118
|
+
});
|
|
119
|
+
const sheetHeight = Math.min(maxHeight.value, from + fromLift);
|
|
96
120
|
return contentArea({
|
|
97
121
|
sheetHeight,
|
|
98
122
|
handleHeight: state.handleHeight.value,
|
|
@@ -64,6 +64,7 @@ export function useSheetState(snapPoints: readonly SnapPointSpec[], config: Shee
|
|
|
64
64
|
animateOnMount,
|
|
65
65
|
keyboardBehavior,
|
|
66
66
|
keyboardBlurBehavior,
|
|
67
|
+
keyboardLiftLimit,
|
|
67
68
|
keyboardScope,
|
|
68
69
|
enableBlurKeyboardOnGesture,
|
|
69
70
|
} = config;
|
|
@@ -83,6 +84,7 @@ export function useSheetState(snapPoints: readonly SnapPointSpec[], config: Shee
|
|
|
83
84
|
animateOnMount,
|
|
84
85
|
keyboardBehavior,
|
|
85
86
|
keyboardBlurBehavior,
|
|
87
|
+
keyboardLiftLimit,
|
|
86
88
|
keyboardScope,
|
|
87
89
|
enableBlurKeyboardOnGesture,
|
|
88
90
|
};
|
|
@@ -100,6 +102,7 @@ export function useSheetState(snapPoints: readonly SnapPointSpec[], config: Shee
|
|
|
100
102
|
animateOnMount,
|
|
101
103
|
keyboardBehavior,
|
|
102
104
|
keyboardBlurBehavior,
|
|
105
|
+
keyboardLiftLimit,
|
|
103
106
|
keyboardScope,
|
|
104
107
|
enableBlurKeyboardOnGesture,
|
|
105
108
|
]);
|