react-native-nitro-modal 0.1.5 → 0.3.0-rc
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 +65 -11
- package/android/src/main/java/com/margelo/nitro/nitromodal/BottomSheetPresenter.kt +335 -34
- package/android/src/main/java/com/margelo/nitro/nitromodal/HybridNitroModal.kt +63 -22
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalConfig.kt +31 -1
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalContentRoot.kt +48 -5
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalPresenter.kt +63 -4
- package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalHostView.kt +68 -8
- package/ios/HybridNitroModal.swift +111 -20
- package/ios/ModalConfig.swift +44 -2
- package/ios/NitroModalHostView.swift +73 -8
- package/ios/SheetView.swift +575 -0
- package/ios/SheetViewController.swift +19 -450
- package/lib/module/NitroModal.js +39 -7
- package/lib/module/NitroModal.js.map +1 -1
- package/lib/module/NitroModalView.js +4 -3
- package/lib/module/NitroModalView.js.map +1 -1
- package/lib/module/NitroModalView.native.js +3 -0
- package/lib/module/NitroModalView.native.js.map +1 -1
- package/lib/module/NitroModalView.web.js +159 -0
- package/lib/module/NitroModalView.web.js.map +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/web/AnimatedValue.js +126 -0
- package/lib/module/web/AnimatedValue.js.map +1 -0
- package/lib/module/web/ModalConfig.js +30 -0
- package/lib/module/web/ModalConfig.js.map +1 -0
- package/lib/module/web/ModalController.js +306 -0
- package/lib/module/web/ModalController.js.map +1 -0
- package/lib/module/web/ModalPresenter.js +280 -0
- package/lib/module/web/ModalPresenter.js.map +1 -0
- package/lib/module/web/PopupPresenter.js +117 -0
- package/lib/module/web/PopupPresenter.js.map +1 -0
- package/lib/module/web/SheetPresenter.js +494 -0
- package/lib/module/web/SheetPresenter.js.map +1 -0
- package/lib/module/web/dom.js +138 -0
- package/lib/module/web/dom.js.map +1 -0
- package/lib/typescript/src/NitroModal.d.ts +35 -4
- package/lib/typescript/src/NitroModal.d.ts.map +1 -1
- package/lib/typescript/src/NitroModal.nitro.d.ts +29 -2
- package/lib/typescript/src/NitroModal.nitro.d.ts.map +1 -1
- package/lib/typescript/src/NitroModalView.d.ts +2 -1
- package/lib/typescript/src/NitroModalView.d.ts.map +1 -1
- package/lib/typescript/src/NitroModalView.native.d.ts +1 -0
- package/lib/typescript/src/NitroModalView.native.d.ts.map +1 -1
- package/lib/typescript/src/NitroModalView.web.d.ts +6 -0
- package/lib/typescript/src/NitroModalView.web.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/web/AnimatedValue.d.ts +49 -0
- package/lib/typescript/src/web/AnimatedValue.d.ts.map +1 -0
- package/lib/typescript/src/web/ModalConfig.d.ts +30 -0
- package/lib/typescript/src/web/ModalConfig.d.ts.map +1 -0
- package/lib/typescript/src/web/ModalController.d.ts +70 -0
- package/lib/typescript/src/web/ModalController.d.ts.map +1 -0
- package/lib/typescript/src/web/ModalPresenter.d.ts +107 -0
- package/lib/typescript/src/web/ModalPresenter.d.ts.map +1 -0
- package/lib/typescript/src/web/PopupPresenter.d.ts +20 -0
- package/lib/typescript/src/web/PopupPresenter.d.ts.map +1 -0
- package/lib/typescript/src/web/SheetPresenter.d.ts +65 -0
- package/lib/typescript/src/web/SheetPresenter.d.ts.map +1 -0
- package/lib/typescript/src/web/dom.d.ts +42 -0
- package/lib/typescript/src/web/dom.d.ts.map +1 -0
- package/nitrogen/generated/android/c++/JHybridNitroModalSpec.cpp +60 -6
- package/nitrogen/generated/android/c++/JHybridNitroModalSpec.hpp +10 -2
- package/nitrogen/generated/android/c++/JNamedSheetDetent.hpp +64 -0
- package/nitrogen/generated/android/c++/JSheetDetent.cpp +26 -0
- package/nitrogen/generated/android/c++/JSheetDetent.hpp +42 -37
- package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.cpp +20 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/HybridNitroModalSpec.kt +40 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/NamedSheetDetent.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/SheetDetent.kt +47 -10
- package/nitrogen/generated/android/nitromodal+autolinking.cmake +1 -0
- package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.hpp +53 -8
- package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Umbrella.hpp +4 -3
- package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.hpp +32 -5
- package/nitrogen/generated/ios/c++/views/HybridNitroModalComponent.mm +24 -0
- package/nitrogen/generated/ios/swift/HybridNitroModalSpec.swift +4 -0
- package/nitrogen/generated/ios/swift/HybridNitroModalSpec_cxx.swift +110 -5
- package/nitrogen/generated/ios/swift/NamedSheetDetent.swift +48 -0
- package/nitrogen/generated/ios/swift/SheetDetent.swift +15 -33
- package/nitrogen/generated/shared/c++/HybridNitroModalSpec.cpp +8 -0
- package/nitrogen/generated/shared/c++/HybridNitroModalSpec.hpp +14 -5
- package/nitrogen/generated/shared/c++/{SheetDetent.hpp → NamedSheetDetent.hpp} +17 -17
- package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.cpp +9 -1
- package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.hpp +15 -2
- package/nitrogen/generated/shared/json/NitroModalConfig.json +4 -0
- package/package.json +3 -2
- package/src/NitroModal.nitro.ts +30 -2
- package/src/NitroModal.tsx +71 -8
- package/src/NitroModalView.native.tsx +4 -0
- package/src/NitroModalView.tsx +9 -4
- package/src/NitroModalView.web.tsx +180 -0
- package/src/index.tsx +2 -0
- package/src/web/AnimatedValue.ts +193 -0
- package/src/web/ModalConfig.ts +67 -0
- package/src/web/ModalController.ts +338 -0
- package/src/web/ModalPresenter.ts +333 -0
- package/src/web/PopupPresenter.ts +129 -0
- package/src/web/SheetPresenter.ts +647 -0
- package/src/web/dom.ts +171 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type CSSProperties,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { Modal, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
12
|
+
import type { ReactNativeView } from 'react-native-nitro-modules';
|
|
13
|
+
import type { NitroModalMethods, NitroModalProps } from './NitroModal.nitro';
|
|
14
|
+
import { clipOverflow } from './web/dom';
|
|
15
|
+
import { ModalController } from './web/ModalController';
|
|
16
|
+
|
|
17
|
+
interface WebNitroModalViewProps extends NitroModalProps {
|
|
18
|
+
hybridRef?: (instance: NitroModalMethods) => void;
|
|
19
|
+
/** Inline: the frame the sheet fills. */
|
|
20
|
+
style?: StyleProp<ViewStyle>;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The web view takes plain functions; only native host components need them wrapped. */
|
|
25
|
+
export function callback<T>(func: T): T {
|
|
26
|
+
return func;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Web implementation of the hybrid view. The modal renders through React
|
|
31
|
+
* Native Web's `Modal` (a portal with focus trapping, Escape handling and
|
|
32
|
+
* stacking with other modals) and is laid out and animated by the presenters
|
|
33
|
+
* in `./web`. In place, it renders nothing.
|
|
34
|
+
*
|
|
35
|
+
* An inline sheet renders in place instead: a view laid out with `style`,
|
|
36
|
+
* holding the sheet's elements.
|
|
37
|
+
*/
|
|
38
|
+
function WebNitroModalView(props: WebNitroModalViewProps) {
|
|
39
|
+
const { hybridRef, children } = props;
|
|
40
|
+
const isInline = props.isInline && props.mode === 'bottomSheet';
|
|
41
|
+
const [mounted, setMounted] = useState(false);
|
|
42
|
+
const [controller] = useState(() => new ModalController(setMounted));
|
|
43
|
+
const overlay = useRef<HTMLDivElement>(null);
|
|
44
|
+
const backdrop = useRef<HTMLDivElement>(null);
|
|
45
|
+
const surface = useRef<HTMLDivElement>(null);
|
|
46
|
+
const content = useRef<HTMLDivElement>(null);
|
|
47
|
+
const grabber = useRef<HTMLDivElement>(null);
|
|
48
|
+
|
|
49
|
+
useLayoutEffect(() => {
|
|
50
|
+
controller.attach();
|
|
51
|
+
return () => controller.detach();
|
|
52
|
+
}, [controller]);
|
|
53
|
+
|
|
54
|
+
useLayoutEffect(() => {
|
|
55
|
+
hybridRef?.(controller.methods as NitroModalMethods);
|
|
56
|
+
}, [controller, hybridRef]);
|
|
57
|
+
|
|
58
|
+
useLayoutEffect(() => {
|
|
59
|
+
controller.update(props);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
useLayoutEffect(() => {
|
|
63
|
+
if (!mounted) return undefined;
|
|
64
|
+
controller.elementsDidMount({
|
|
65
|
+
overlay: overlay.current!,
|
|
66
|
+
backdrop: backdrop.current!,
|
|
67
|
+
surface: surface.current!,
|
|
68
|
+
content: content.current!,
|
|
69
|
+
grabber: grabber.current!,
|
|
70
|
+
});
|
|
71
|
+
return () => controller.elementsWillUnmount();
|
|
72
|
+
}, [controller, mounted]);
|
|
73
|
+
|
|
74
|
+
useLayoutEffect(() => {
|
|
75
|
+
if (mounted) {
|
|
76
|
+
controller.contentDidRender();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const hostRef = useCallback(
|
|
81
|
+
(node: unknown) => {
|
|
82
|
+
controller.hostDidChange(node instanceof HTMLElement ? node : null);
|
|
83
|
+
},
|
|
84
|
+
[controller]
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Styles set here never change; the presenters write sizes, positions,
|
|
88
|
+
// colors and animated values straight to the elements.
|
|
89
|
+
const layer = mounted ? (
|
|
90
|
+
// Focusable so the modal's focus trap lands here first rather than on the first input.
|
|
91
|
+
<div
|
|
92
|
+
ref={overlay}
|
|
93
|
+
tabIndex={-1}
|
|
94
|
+
style={
|
|
95
|
+
isInline
|
|
96
|
+
? { ...styles.inlineOverlay, overflow: clipOverflow() }
|
|
97
|
+
: styles.overlay
|
|
98
|
+
}
|
|
99
|
+
>
|
|
100
|
+
<div ref={backdrop} aria-hidden style={styles.backdrop} />
|
|
101
|
+
<div ref={surface} style={styles.surface}>
|
|
102
|
+
<div ref={content} style={styles.content}>
|
|
103
|
+
{children}
|
|
104
|
+
</div>
|
|
105
|
+
<div ref={grabber} aria-hidden style={styles.grabber} />
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
) : null;
|
|
109
|
+
|
|
110
|
+
if (isInline) {
|
|
111
|
+
return (
|
|
112
|
+
<View ref={hostRef} style={props.style} pointerEvents="box-none">
|
|
113
|
+
{layer}
|
|
114
|
+
</View>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!layer) return null;
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<Modal
|
|
122
|
+
visible
|
|
123
|
+
transparent
|
|
124
|
+
animationType="none"
|
|
125
|
+
onRequestClose={controller.handleBackPress}
|
|
126
|
+
>
|
|
127
|
+
{layer}
|
|
128
|
+
</Modal>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const styles = {
|
|
133
|
+
overlay: {
|
|
134
|
+
position: 'fixed',
|
|
135
|
+
inset: 0,
|
|
136
|
+
outline: 'none',
|
|
137
|
+
},
|
|
138
|
+
// Fills the host view; the sheet hides below its bottom edge.
|
|
139
|
+
inlineOverlay: {
|
|
140
|
+
position: 'absolute',
|
|
141
|
+
inset: 0,
|
|
142
|
+
outline: 'none',
|
|
143
|
+
pointerEvents: 'none',
|
|
144
|
+
},
|
|
145
|
+
backdrop: {
|
|
146
|
+
position: 'absolute',
|
|
147
|
+
inset: 0,
|
|
148
|
+
touchAction: 'none',
|
|
149
|
+
},
|
|
150
|
+
surface: {
|
|
151
|
+
position: 'absolute',
|
|
152
|
+
left: 0,
|
|
153
|
+
top: 0,
|
|
154
|
+
boxSizing: 'border-box',
|
|
155
|
+
willChange: 'transform',
|
|
156
|
+
},
|
|
157
|
+
// Pinned at the origin like the native content view; the React content is
|
|
158
|
+
// positioned and sized inside it by `NitroModal`.
|
|
159
|
+
content: {
|
|
160
|
+
position: 'absolute',
|
|
161
|
+
left: 0,
|
|
162
|
+
top: 0,
|
|
163
|
+
},
|
|
164
|
+
grabber: {
|
|
165
|
+
position: 'absolute',
|
|
166
|
+
top: 5,
|
|
167
|
+
left: '50%',
|
|
168
|
+
width: 36,
|
|
169
|
+
height: 5,
|
|
170
|
+
marginLeft: -18,
|
|
171
|
+
borderRadius: 2.5,
|
|
172
|
+
backgroundColor: 'rgba(127, 127, 127, 0.4)',
|
|
173
|
+
pointerEvents: 'none',
|
|
174
|
+
},
|
|
175
|
+
} satisfies Record<string, CSSProperties>;
|
|
176
|
+
|
|
177
|
+
export const NitroModalView = WebNitroModalView as unknown as ReactNativeView<
|
|
178
|
+
NitroModalProps,
|
|
179
|
+
NitroModalMethods
|
|
180
|
+
>;
|
package/src/index.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export {
|
|
2
2
|
NitroModal,
|
|
3
|
+
type ModalPresentation,
|
|
3
4
|
type NitroModalProps,
|
|
4
5
|
type NitroModalRef,
|
|
5
6
|
} from './NitroModal';
|
|
@@ -7,6 +8,7 @@ export type {
|
|
|
7
8
|
DismissReason,
|
|
8
9
|
KeyboardBehavior,
|
|
9
10
|
ModalMode,
|
|
11
|
+
NamedSheetDetent,
|
|
10
12
|
PopupAnimation,
|
|
11
13
|
SheetDetent,
|
|
12
14
|
} from './NitroModal.nitro';
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
|
|
3
|
+
/** How an `AnimatedValue` moves to its target. */
|
|
4
|
+
export type Motion =
|
|
5
|
+
| {
|
|
6
|
+
type: 'spring';
|
|
7
|
+
/** Period of the undamped spring, in seconds. Lower is snappier. */
|
|
8
|
+
response: number;
|
|
9
|
+
/** 1 settles without overshoot; lower values bounce. */
|
|
10
|
+
dampingRatio: number;
|
|
11
|
+
}
|
|
12
|
+
| {
|
|
13
|
+
type: 'timing';
|
|
14
|
+
/** In milliseconds. */
|
|
15
|
+
duration: number;
|
|
16
|
+
easing: (progress: number) => number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Jumps to the target at once. The completion still arrives on the next frame. */
|
|
20
|
+
export const INSTANT: Motion = {
|
|
21
|
+
type: 'timing',
|
|
22
|
+
duration: 0,
|
|
23
|
+
easing: (t) => t,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface AnimateOptions {
|
|
27
|
+
/** Starting velocity in units per second. Defaults to the current velocity. */
|
|
28
|
+
velocity?: number;
|
|
29
|
+
/** Finish as soon as a spring reaches the target instead of when it settles. */
|
|
30
|
+
stopAtTarget?: boolean;
|
|
31
|
+
/** Runs once the target is reached, unless another animation or `set` takes over first. */
|
|
32
|
+
onEnd?: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Run {
|
|
36
|
+
target: number;
|
|
37
|
+
motion: Motion;
|
|
38
|
+
options: AnimateOptions;
|
|
39
|
+
from: number;
|
|
40
|
+
startTime: number;
|
|
41
|
+
/** Side of the target the value started on, for `stopAtTarget`. */
|
|
42
|
+
side: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Longest frame integrated at once, so a dropped frame can't throw the spring off. */
|
|
46
|
+
const MAX_FRAME_SECONDS = 0.064;
|
|
47
|
+
const SPRING_STEP_SECONDS = 0.004;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A number animated on `requestAnimationFrame` that reports every frame
|
|
51
|
+
* through `onChange`. A new animation starts from the current value and
|
|
52
|
+
* velocity, so transitions can be turned around mid-flight.
|
|
53
|
+
*/
|
|
54
|
+
export class AnimatedValue {
|
|
55
|
+
private current: number;
|
|
56
|
+
private speed = 0;
|
|
57
|
+
private run: Run | null = null;
|
|
58
|
+
private frame: number | null = null;
|
|
59
|
+
private lastTime = 0;
|
|
60
|
+
|
|
61
|
+
/** `precision`: distance from the target at which a spring counts as settled. */
|
|
62
|
+
constructor(
|
|
63
|
+
value: number,
|
|
64
|
+
private readonly precision: number,
|
|
65
|
+
private readonly onChange: (value: number) => void
|
|
66
|
+
) {
|
|
67
|
+
this.current = value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get value(): number {
|
|
71
|
+
return this.current;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get isAnimating(): boolean {
|
|
75
|
+
return this.run != null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Jumps to `value`, cancelling any animation. */
|
|
79
|
+
set(value: number) {
|
|
80
|
+
this.stop();
|
|
81
|
+
this.current = value;
|
|
82
|
+
this.speed = 0;
|
|
83
|
+
this.onChange(value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
stop() {
|
|
87
|
+
this.run = null;
|
|
88
|
+
if (this.frame != null) {
|
|
89
|
+
cancelAnimationFrame(this.frame);
|
|
90
|
+
this.frame = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
animateTo(target: number, motion: Motion, options: AnimateOptions = {}) {
|
|
95
|
+
if (options.velocity !== undefined) {
|
|
96
|
+
this.speed = options.velocity;
|
|
97
|
+
}
|
|
98
|
+
const now = performance.now();
|
|
99
|
+
this.run = {
|
|
100
|
+
target,
|
|
101
|
+
motion,
|
|
102
|
+
options,
|
|
103
|
+
from: this.current,
|
|
104
|
+
startTime: now,
|
|
105
|
+
side: Math.sign(this.current - target),
|
|
106
|
+
};
|
|
107
|
+
if (motion.type === 'timing' && motion.duration <= 0) {
|
|
108
|
+
this.current = target;
|
|
109
|
+
this.speed = 0;
|
|
110
|
+
this.onChange(target);
|
|
111
|
+
}
|
|
112
|
+
if (this.frame == null) {
|
|
113
|
+
this.lastTime = now;
|
|
114
|
+
this.frame = requestAnimationFrame(this.step);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private step = (time: number) => {
|
|
119
|
+
this.frame = null;
|
|
120
|
+
const run = this.run;
|
|
121
|
+
if (!run) return;
|
|
122
|
+
const dt = Math.min(
|
|
123
|
+
Math.max((time - this.lastTime) / 1000, 0),
|
|
124
|
+
MAX_FRAME_SECONDS
|
|
125
|
+
);
|
|
126
|
+
this.lastTime = time;
|
|
127
|
+
|
|
128
|
+
const done =
|
|
129
|
+
run.motion.type === 'timing'
|
|
130
|
+
? this.stepTiming(run, run.motion, time, dt)
|
|
131
|
+
: this.stepSpring(run, run.motion, dt);
|
|
132
|
+
if (done) {
|
|
133
|
+
this.current = run.target;
|
|
134
|
+
this.speed = 0;
|
|
135
|
+
this.run = null;
|
|
136
|
+
}
|
|
137
|
+
this.onChange(this.current);
|
|
138
|
+
if (done) {
|
|
139
|
+
run.options.onEnd?.();
|
|
140
|
+
} else {
|
|
141
|
+
this.frame = requestAnimationFrame(this.step);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
private stepTiming(
|
|
146
|
+
run: Run,
|
|
147
|
+
motion: Extract<Motion, { type: 'timing' }>,
|
|
148
|
+
time: number,
|
|
149
|
+
dt: number
|
|
150
|
+
): boolean {
|
|
151
|
+
const progress =
|
|
152
|
+
motion.duration > 0
|
|
153
|
+
? Math.min(Math.max((time - run.startTime) / motion.duration, 0), 1)
|
|
154
|
+
: 1;
|
|
155
|
+
const next = run.from + (run.target - run.from) * motion.easing(progress);
|
|
156
|
+
if (dt > 0) {
|
|
157
|
+
this.speed = (next - this.current) / dt;
|
|
158
|
+
}
|
|
159
|
+
this.current = next;
|
|
160
|
+
return progress >= 1;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private stepSpring(
|
|
164
|
+
run: Run,
|
|
165
|
+
motion: Extract<Motion, { type: 'spring' }>,
|
|
166
|
+
dt: number
|
|
167
|
+
): boolean {
|
|
168
|
+
const omega = (2 * Math.PI) / motion.response;
|
|
169
|
+
const stiffness = omega * omega;
|
|
170
|
+
const damping = 2 * motion.dampingRatio * omega;
|
|
171
|
+
let offset = this.current - run.target;
|
|
172
|
+
let velocity = this.speed;
|
|
173
|
+
const steps = Math.ceil(dt / SPRING_STEP_SECONDS);
|
|
174
|
+
for (let i = 0; i < steps; i++) {
|
|
175
|
+
const h = dt / steps;
|
|
176
|
+
velocity += (-stiffness * offset - damping * velocity) * h;
|
|
177
|
+
offset += velocity * h;
|
|
178
|
+
}
|
|
179
|
+
this.current = run.target + offset;
|
|
180
|
+
this.speed = velocity;
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
run.options.stopAtTarget &&
|
|
184
|
+
(run.side === 0 || Math.sign(offset) !== run.side)
|
|
185
|
+
) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
return (
|
|
189
|
+
Math.abs(offset) < this.precision &&
|
|
190
|
+
Math.abs(velocity) < this.precision * 20
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
KeyboardBehavior,
|
|
3
|
+
ModalMode,
|
|
4
|
+
NitroModalProps,
|
|
5
|
+
PopupAnimation,
|
|
6
|
+
SheetDetent,
|
|
7
|
+
} from '../NitroModal.nitro';
|
|
8
|
+
|
|
9
|
+
/** Snapshot of the props that shape a presentation. */
|
|
10
|
+
export interface ModalConfig {
|
|
11
|
+
mode: ModalMode;
|
|
12
|
+
/**
|
|
13
|
+
* A `bottomSheet` rendered in place, filling the host view: no backdrop,
|
|
14
|
+
* the page behind stays interactive, and the user can't dismiss it.
|
|
15
|
+
*/
|
|
16
|
+
isInline: boolean;
|
|
17
|
+
detents: SheetDetent[];
|
|
18
|
+
initialDetentIndex: number;
|
|
19
|
+
/** 0xAARRGGBB, from `processColor`. */
|
|
20
|
+
backdropColor: number;
|
|
21
|
+
backdropOpacity: number;
|
|
22
|
+
backdropBlurRadius: number;
|
|
23
|
+
dismissOnBackdropPress: boolean;
|
|
24
|
+
dismissOnSwipe: boolean;
|
|
25
|
+
dismissOnBackButton: boolean;
|
|
26
|
+
grabberVisible: boolean;
|
|
27
|
+
/** `null` uses the default. */
|
|
28
|
+
cornerRadius: number | null;
|
|
29
|
+
/** 0xAARRGGBB; `null` uses the system background. */
|
|
30
|
+
contentBackgroundColor: number | null;
|
|
31
|
+
keyboardBehavior: KeyboardBehavior;
|
|
32
|
+
popupAnimation: PopupAnimation;
|
|
33
|
+
pullToRefreshEnabled: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function makeConfig(props: NitroModalProps): ModalConfig {
|
|
37
|
+
// An inline sheet is part of the page; the user can't dismiss it.
|
|
38
|
+
const isInline = props.isInline && props.mode === 'bottomSheet';
|
|
39
|
+
return {
|
|
40
|
+
mode: props.mode,
|
|
41
|
+
isInline,
|
|
42
|
+
detents: props.detents,
|
|
43
|
+
initialDetentIndex: Number.isFinite(props.initialDetentIndex)
|
|
44
|
+
? Math.trunc(props.initialDetentIndex)
|
|
45
|
+
: 0,
|
|
46
|
+
backdropColor: props.backdropColor,
|
|
47
|
+
backdropOpacity: props.backdropOpacity,
|
|
48
|
+
backdropBlurRadius: props.backdropBlurRadius,
|
|
49
|
+
dismissOnBackdropPress: props.dismissOnBackdropPress && !isInline,
|
|
50
|
+
dismissOnSwipe: props.dismissOnSwipe && !isInline,
|
|
51
|
+
dismissOnBackButton: props.dismissOnBackButton && !isInline,
|
|
52
|
+
grabberVisible: props.grabberVisible,
|
|
53
|
+
cornerRadius: props.cornerRadius >= 0 ? props.cornerRadius : null,
|
|
54
|
+
contentBackgroundColor: props.contentBackgroundColor ?? null,
|
|
55
|
+
keyboardBehavior: props.keyboardBehavior,
|
|
56
|
+
popupAnimation: props.popupAnimation,
|
|
57
|
+
pullToRefreshEnabled: props.pullToRefreshEnabled,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isSameConfig(a: ModalConfig, b: ModalConfig): boolean {
|
|
62
|
+
return (Object.keys(a) as (keyof ModalConfig)[]).every((key) =>
|
|
63
|
+
key === 'detents'
|
|
64
|
+
? a.detents.join() === b.detents.join()
|
|
65
|
+
: a[key] === b[key]
|
|
66
|
+
);
|
|
67
|
+
}
|