react-native-nitro-modal 0.2.0 → 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 +48 -4
- 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 +38 -5
- package/lib/module/NitroModal.js.map +1 -1
- package/lib/module/NitroModalView.web.js +55 -25
- package/lib/module/NitroModalView.web.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/web/AnimatedValue.js +3 -0
- package/lib/module/web/AnimatedValue.js.map +1 -1
- package/lib/module/web/ModalConfig.js +8 -4
- package/lib/module/web/ModalConfig.js.map +1 -1
- package/lib/module/web/ModalController.js +29 -2
- package/lib/module/web/ModalController.js.map +1 -1
- package/lib/module/web/ModalPresenter.js +47 -12
- package/lib/module/web/ModalPresenter.js.map +1 -1
- package/lib/module/web/SheetPresenter.js +54 -29
- package/lib/module/web/SheetPresenter.js.map +1 -1
- package/lib/module/web/dom.js +13 -0
- package/lib/module/web/dom.js.map +1 -1
- package/lib/typescript/src/NitroModal.d.ts +33 -2
- 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.web.d.ts.map +1 -1
- 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 +1 -0
- package/lib/typescript/src/web/AnimatedValue.d.ts.map +1 -1
- package/lib/typescript/src/web/ModalConfig.d.ts +6 -0
- package/lib/typescript/src/web/ModalConfig.d.ts.map +1 -1
- package/lib/typescript/src/web/ModalController.d.ts +5 -0
- package/lib/typescript/src/web/ModalController.d.ts.map +1 -1
- package/lib/typescript/src/web/ModalPresenter.d.ts +9 -0
- package/lib/typescript/src/web/ModalPresenter.d.ts.map +1 -1
- package/lib/typescript/src/web/SheetPresenter.d.ts +1 -1
- package/lib/typescript/src/web/SheetPresenter.d.ts.map +1 -1
- package/lib/typescript/src/web/dom.d.ts +2 -0
- package/lib/typescript/src/web/dom.d.ts.map +1 -1
- 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 +1 -1
- package/src/NitroModal.nitro.ts +30 -2
- package/src/NitroModal.tsx +68 -4
- package/src/NitroModalView.web.tsx +54 -12
- package/src/index.tsx +2 -0
- package/src/web/AnimatedValue.ts +4 -0
- package/src/web/ModalConfig.ts +13 -3
- package/src/web/ModalController.ts +35 -2
- package/src/web/ModalPresenter.ts +46 -11
- package/src/web/SheetPresenter.ts +98 -33
- package/src/web/dom.ts +17 -0
|
@@ -6,7 +6,12 @@ import type {
|
|
|
6
6
|
NitroModalMethods,
|
|
7
7
|
NitroModalProps,
|
|
8
8
|
} from '../NitroModal.nitro';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
canUseDOM,
|
|
11
|
+
estimateViewport,
|
|
12
|
+
inlineInsets,
|
|
13
|
+
readSafeAreaInsets,
|
|
14
|
+
} from './dom';
|
|
10
15
|
import { isSameConfig, makeConfig, type ModalConfig } from './ModalConfig';
|
|
11
16
|
import type {
|
|
12
17
|
ModalElements,
|
|
@@ -58,6 +63,8 @@ export class ModalController implements PresenterListener {
|
|
|
58
63
|
private isAttached = false;
|
|
59
64
|
private presenter: ModalPresenter | null = null;
|
|
60
65
|
private elements: ModalElements | null = null;
|
|
66
|
+
/** Inline: the element the sheet fills. */
|
|
67
|
+
private host: HTMLElement | null = null;
|
|
61
68
|
private lastContentArea: ModalContentArea | null = null;
|
|
62
69
|
|
|
63
70
|
/** `setMounted` renders (or removes) the modal's elements. */
|
|
@@ -110,6 +117,15 @@ export class ModalController implements PresenterListener {
|
|
|
110
117
|
}
|
|
111
118
|
}
|
|
112
119
|
|
|
120
|
+
/** Inline: the element the sheet fills, rendered in place. */
|
|
121
|
+
hostDidChange(host: HTMLElement | null) {
|
|
122
|
+
if (host === this.host) return;
|
|
123
|
+
this.host = host;
|
|
124
|
+
if (!this.presenter) {
|
|
125
|
+
this.reportEstimatedContentArea();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
elementsDidMount(elements: ModalElements) {
|
|
114
130
|
this.elements = elements;
|
|
115
131
|
if (this.presenter && !this.presenter.isShown) {
|
|
@@ -273,6 +289,10 @@ export class ModalController implements PresenterListener {
|
|
|
273
289
|
this.props?.onDetentChange?.(index);
|
|
274
290
|
}
|
|
275
291
|
|
|
292
|
+
onPullToRefresh() {
|
|
293
|
+
this.props?.onPullToRefresh?.();
|
|
294
|
+
}
|
|
295
|
+
|
|
276
296
|
onContentAreaChange(width: number, height: number) {
|
|
277
297
|
const area = { width: Math.floor(width), height: Math.floor(height) };
|
|
278
298
|
const last = this.lastContentArea;
|
|
@@ -288,12 +308,25 @@ export class ModalController implements PresenterListener {
|
|
|
288
308
|
private reportEstimatedContentArea() {
|
|
289
309
|
const { config } = this;
|
|
290
310
|
if (!canUseDOM || !this.isAttached || !config) return;
|
|
311
|
+
if (config.isInline) {
|
|
312
|
+
const { host } = this;
|
|
313
|
+
if (!host || host.clientWidth === 0 || host.clientHeight === 0) return;
|
|
314
|
+
const viewport = { width: host.clientWidth, height: host.clientHeight };
|
|
315
|
+
const area = sheetContentArea(
|
|
316
|
+
config.detents,
|
|
317
|
+
viewport,
|
|
318
|
+
inlineInsets(host),
|
|
319
|
+
true
|
|
320
|
+
);
|
|
321
|
+
this.onContentAreaChange(area.width, area.height);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
291
324
|
const viewport = estimateViewport();
|
|
292
325
|
const insets = readSafeAreaInsets();
|
|
293
326
|
const area =
|
|
294
327
|
config.mode === 'popup'
|
|
295
328
|
? popupContentArea(viewport, insets)
|
|
296
|
-
: sheetContentArea(config.detents, viewport, insets);
|
|
329
|
+
: sheetContentArea(config.detents, viewport, insets, false);
|
|
297
330
|
this.onContentAreaChange(area.width, area.height);
|
|
298
331
|
}
|
|
299
332
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
canScrollWithin,
|
|
5
5
|
clipOverflow,
|
|
6
|
+
inlineInsets,
|
|
6
7
|
readKeyboardHeight,
|
|
7
8
|
readSafeAreaInsets,
|
|
8
9
|
toCSSColor,
|
|
@@ -34,6 +35,8 @@ export interface PresenterListener {
|
|
|
34
35
|
onSwipeDismiss(): void;
|
|
35
36
|
onDetentChange(index: number): void;
|
|
36
37
|
onContentAreaChange(width: number, height: number): void;
|
|
38
|
+
/** The user pulled the sheet past its lowest detent and let go. */
|
|
39
|
+
onPullToRefresh(): void;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
/**
|
|
@@ -45,6 +48,9 @@ export interface PresenterListener {
|
|
|
45
48
|
* Transitions are interruptible: dismissing mid-enter turns the enter around,
|
|
46
49
|
* and `cancelDismiss` turns an exit back around. Once an exit starts the modal
|
|
47
50
|
* stops taking input.
|
|
51
|
+
*
|
|
52
|
+
* Inline, the elements fill the host view in place of a full-window layer:
|
|
53
|
+
* only the surface takes input, and the keyboard is left to the page.
|
|
48
54
|
*/
|
|
49
55
|
export abstract class ModalPresenter {
|
|
50
56
|
protected elements: ModalElements | null = null;
|
|
@@ -59,6 +65,8 @@ export abstract class ModalPresenter {
|
|
|
59
65
|
private finished = false;
|
|
60
66
|
private observedContent: HTMLElement | null = null;
|
|
61
67
|
private resizeObserver: ResizeObserver | null = null;
|
|
68
|
+
/** Inline: follows the host's size, which can change without a window resize. */
|
|
69
|
+
private hostObserver: ResizeObserver | null = null;
|
|
62
70
|
|
|
63
71
|
constructor(
|
|
64
72
|
protected config: ModalConfig,
|
|
@@ -74,18 +82,26 @@ export abstract class ModalPresenter {
|
|
|
74
82
|
if (this.elements || this.finished) return;
|
|
75
83
|
this.elements = elements;
|
|
76
84
|
// The elements can outlive a previous presentation (an immediate re-present).
|
|
77
|
-
|
|
85
|
+
this.setInteractive(true);
|
|
78
86
|
elements.surface.style.overflow = clipOverflow();
|
|
79
87
|
elements.surface.style.opacity = '';
|
|
80
88
|
elements.surface.style.transform = '';
|
|
89
|
+
elements.backdrop.style.display = this.config.isInline ? 'none' : '';
|
|
81
90
|
|
|
82
91
|
window.addEventListener('resize', this.handleResize);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
92
|
+
if (this.config.isInline) {
|
|
93
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
94
|
+
this.hostObserver = new ResizeObserver(this.handleResize);
|
|
95
|
+
this.hostObserver.observe(elements.overlay);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
window.visualViewport?.addEventListener('resize', this.handleKeyboard);
|
|
99
|
+
window.visualViewport?.addEventListener('scroll', this.handleKeyboard);
|
|
100
|
+
elements.backdrop.addEventListener('click', this.handleBackdropClick);
|
|
101
|
+
elements.overlay.addEventListener('wheel', this.handleWheel, {
|
|
102
|
+
passive: false,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
89
105
|
if (typeof ResizeObserver !== 'undefined') {
|
|
90
106
|
this.resizeObserver = new ResizeObserver(this.handleContentResize);
|
|
91
107
|
}
|
|
@@ -122,7 +138,7 @@ export abstract class ModalPresenter {
|
|
|
122
138
|
if (!elements || this.isDismissing || this.finished) return;
|
|
123
139
|
this.isDismissing = true;
|
|
124
140
|
this.entering = false;
|
|
125
|
-
|
|
141
|
+
this.setInteractive(false);
|
|
126
142
|
// Also hides the on-screen keyboard.
|
|
127
143
|
const focused = document.activeElement;
|
|
128
144
|
if (focused instanceof HTMLElement && elements.overlay.contains(focused)) {
|
|
@@ -138,7 +154,7 @@ export abstract class ModalPresenter {
|
|
|
138
154
|
cancelDismiss(): boolean {
|
|
139
155
|
if (!this.elements || !this.isDismissing || this.finished) return false;
|
|
140
156
|
this.isDismissing = false;
|
|
141
|
-
this.
|
|
157
|
+
this.setInteractive(true);
|
|
142
158
|
this.entering = true;
|
|
143
159
|
this.animateIn();
|
|
144
160
|
return true;
|
|
@@ -196,6 +212,8 @@ export abstract class ModalPresenter {
|
|
|
196
212
|
this.elements?.overlay.removeEventListener('wheel', this.handleWheel);
|
|
197
213
|
this.resizeObserver?.disconnect();
|
|
198
214
|
this.resizeObserver = null;
|
|
215
|
+
this.hostObserver?.disconnect();
|
|
216
|
+
this.hostObserver = null;
|
|
199
217
|
this.observedContent = null;
|
|
200
218
|
}
|
|
201
219
|
|
|
@@ -226,14 +244,30 @@ export abstract class ModalPresenter {
|
|
|
226
244
|
: 'Canvas';
|
|
227
245
|
}
|
|
228
246
|
|
|
247
|
+
/** Inline, only the surface takes input; the rest reaches the page. */
|
|
248
|
+
private setInteractive(interactive: boolean) {
|
|
249
|
+
const { overlay, surface } = this.elements!;
|
|
250
|
+
if (this.config.isInline) {
|
|
251
|
+
overlay.style.pointerEvents = 'none';
|
|
252
|
+
surface.style.pointerEvents = interactive ? 'auto' : 'none';
|
|
253
|
+
} else {
|
|
254
|
+
overlay.style.pointerEvents = interactive ? '' : 'none';
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
229
258
|
private measureViewport() {
|
|
230
259
|
const { overlay } = this.elements!;
|
|
231
260
|
this.viewport = {
|
|
232
261
|
width: overlay.clientWidth,
|
|
233
262
|
height: overlay.clientHeight,
|
|
234
263
|
};
|
|
235
|
-
this.
|
|
236
|
-
|
|
264
|
+
if (this.config.isInline) {
|
|
265
|
+
this.insets = inlineInsets(overlay);
|
|
266
|
+
this.keyboardHeight = 0;
|
|
267
|
+
} else {
|
|
268
|
+
this.insets = readSafeAreaInsets();
|
|
269
|
+
this.keyboardHeight = readKeyboardHeight(this.viewport.height);
|
|
270
|
+
}
|
|
237
271
|
}
|
|
238
272
|
|
|
239
273
|
/** Re-targets the observer at the content's root element. Returns whether its size changed. */
|
|
@@ -275,6 +309,7 @@ export abstract class ModalPresenter {
|
|
|
275
309
|
};
|
|
276
310
|
|
|
277
311
|
private handleKeyboard = () => {
|
|
312
|
+
if (this.config.isInline) return;
|
|
278
313
|
const height = readKeyboardHeight(this.viewport.height);
|
|
279
314
|
if (height === this.keyboardHeight) return;
|
|
280
315
|
this.keyboardHeight = height;
|
|
@@ -13,8 +13,10 @@ import {
|
|
|
13
13
|
import type { ModalConfig } from './ModalConfig';
|
|
14
14
|
import { ModalPresenter, type PresenterListener } from './ModalPresenter';
|
|
15
15
|
|
|
16
|
-
/** Gap kept above
|
|
16
|
+
/** Gap kept above a modal sheet at its tallest. */
|
|
17
17
|
const TOP_GAP = 10;
|
|
18
|
+
/** How far below its lowest detent the sheet must be pulled to refresh. */
|
|
19
|
+
const REFRESH_THRESHOLD = 56;
|
|
18
20
|
/**
|
|
19
21
|
* Extra sheet height below the viewport so it stays attached to the bottom
|
|
20
22
|
* edge while rubber-banding upward.
|
|
@@ -47,39 +49,77 @@ interface Drag {
|
|
|
47
49
|
rawTop: number;
|
|
48
50
|
/** Whether the last movement moved the sheet rather than the scroller. */
|
|
49
51
|
sheetDrove: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The scroller moved during this gesture. It then keeps the downward part
|
|
54
|
+
* too: scrolled content goes back to its top, and only a new gesture moves
|
|
55
|
+
* the sheet.
|
|
56
|
+
*/
|
|
57
|
+
contentScrolled: boolean;
|
|
58
|
+
/** Started with the sheet resting on its lowest detent, so pulling it further down refreshes. */
|
|
59
|
+
canRefresh: boolean;
|
|
60
|
+
/** Pulled far enough that letting go refreshes. */
|
|
61
|
+
refreshArmed: boolean;
|
|
50
62
|
samples: { time: number; y: number }[];
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
function sheetWidth(available: number): number {
|
|
54
|
-
return Math.min(available, MAX_WIDTH);
|
|
65
|
+
function sheetWidth(available: number, isInline: boolean): number {
|
|
66
|
+
return isInline ? available : Math.min(available, MAX_WIDTH);
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
/** Tallest height the content may occupy (excludes the bottom safe area). */
|
|
58
|
-
function maximumDetentValue(
|
|
59
|
-
|
|
70
|
+
function maximumDetentValue(
|
|
71
|
+
viewportHeight: number,
|
|
72
|
+
insets: Insets,
|
|
73
|
+
isInline: boolean
|
|
74
|
+
): number {
|
|
75
|
+
const gap = isInline ? 0 : TOP_GAP;
|
|
76
|
+
return Math.max(0, viewportHeight - insets.top - insets.bottom - gap);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Content height at `detent`; `fitContent` uses the measured `fitting` height. */
|
|
80
|
+
function detentHeight(
|
|
81
|
+
detent: SheetDetent,
|
|
82
|
+
maximum: number,
|
|
83
|
+
fitting: number
|
|
84
|
+
): number {
|
|
85
|
+
switch (detent) {
|
|
86
|
+
case 'small':
|
|
87
|
+
return maximum * 0.25;
|
|
88
|
+
case 'medium':
|
|
89
|
+
return maximum * 0.5;
|
|
90
|
+
case 'large':
|
|
91
|
+
return maximum;
|
|
92
|
+
case 'fitContent':
|
|
93
|
+
return Math.min(Math.max(fitting, 1), maximum);
|
|
94
|
+
default:
|
|
95
|
+
return Number.isFinite(detent)
|
|
96
|
+
? Math.min(Math.max(detent, 1), maximum)
|
|
97
|
+
: maximum;
|
|
98
|
+
}
|
|
60
99
|
}
|
|
61
100
|
|
|
62
101
|
/** Height the content may occupy at the largest of `detents`. */
|
|
63
102
|
function contentHeight(detents: SheetDetent[], maximum: number): number {
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
detents.
|
|
68
|
-
)
|
|
69
|
-
return maximum;
|
|
70
|
-
}
|
|
71
|
-
return detents.includes('medium') ? maximum * 0.5 : maximum * 0.25;
|
|
103
|
+
if (detents.length === 0) return maximum;
|
|
104
|
+
// `fitContent` content can grow up to the maximum.
|
|
105
|
+
return Math.max(
|
|
106
|
+
...detents.map((detent) => detentHeight(detent, maximum, maximum))
|
|
107
|
+
);
|
|
72
108
|
}
|
|
73
109
|
|
|
74
110
|
/** Best guess of the content area before the sheet is on screen. */
|
|
75
111
|
export function sheetContentArea(
|
|
76
112
|
detents: SheetDetent[],
|
|
77
113
|
viewport: Size,
|
|
78
|
-
insets: Insets
|
|
114
|
+
insets: Insets,
|
|
115
|
+
isInline: boolean
|
|
79
116
|
): Size {
|
|
80
117
|
return {
|
|
81
|
-
width: sheetWidth(viewport.width),
|
|
82
|
-
height: contentHeight(
|
|
118
|
+
width: sheetWidth(viewport.width, isInline),
|
|
119
|
+
height: contentHeight(
|
|
120
|
+
detents,
|
|
121
|
+
maximumDetentValue(viewport.height, insets, isInline)
|
|
122
|
+
),
|
|
83
123
|
};
|
|
84
124
|
}
|
|
85
125
|
|
|
@@ -200,7 +240,11 @@ export class SheetPresenter extends ModalPresenter {
|
|
|
200
240
|
}
|
|
201
241
|
|
|
202
242
|
private get maximumDetentValue(): number {
|
|
203
|
-
return maximumDetentValue(
|
|
243
|
+
return maximumDetentValue(
|
|
244
|
+
this.viewport.height,
|
|
245
|
+
this.insets,
|
|
246
|
+
this.config.isInline
|
|
247
|
+
);
|
|
204
248
|
}
|
|
205
249
|
|
|
206
250
|
private get keyboardLift(): number {
|
|
@@ -210,17 +254,11 @@ export class SheetPresenter extends ModalPresenter {
|
|
|
210
254
|
}
|
|
211
255
|
|
|
212
256
|
private height(detent: SheetDetent): number {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return maximum * 0.5;
|
|
219
|
-
case 'large':
|
|
220
|
-
return maximum;
|
|
221
|
-
case 'fitContent':
|
|
222
|
-
return Math.min(Math.max(this.contentSize.height, 1), maximum);
|
|
223
|
-
}
|
|
257
|
+
return detentHeight(
|
|
258
|
+
detent,
|
|
259
|
+
this.maximumDetentValue,
|
|
260
|
+
this.contentSize.height
|
|
261
|
+
);
|
|
224
262
|
}
|
|
225
263
|
|
|
226
264
|
/** Sheet top when resting on `detents[index]`. The bottom safe area is added below the content. */
|
|
@@ -233,7 +271,10 @@ export class SheetPresenter extends ModalPresenter {
|
|
|
233
271
|
this.insets.bottom -
|
|
234
272
|
this.keyboardLift -
|
|
235
273
|
this.height(detent);
|
|
236
|
-
return Math.max(
|
|
274
|
+
return Math.max(
|
|
275
|
+
top,
|
|
276
|
+
this.insets.top + (this.config.isInline ? 0 : TOP_GAP)
|
|
277
|
+
);
|
|
237
278
|
}
|
|
238
279
|
|
|
239
280
|
private get detentTops(): number[] {
|
|
@@ -242,7 +283,7 @@ export class SheetPresenter extends ModalPresenter {
|
|
|
242
283
|
|
|
243
284
|
private layout() {
|
|
244
285
|
const { surface, content, grabber } = this.elements!;
|
|
245
|
-
const width = sheetWidth(this.viewport.width);
|
|
286
|
+
const width = sheetWidth(this.viewport.width, this.config.isInline);
|
|
246
287
|
const radius = `${this.config.cornerRadius ?? DEFAULT_CORNER_RADIUS}px`;
|
|
247
288
|
Object.assign(surface.style, {
|
|
248
289
|
left: `${(this.viewport.width - width) / 2}px`,
|
|
@@ -324,6 +365,9 @@ export class SheetPresenter extends ModalPresenter {
|
|
|
324
365
|
active: false,
|
|
325
366
|
rawTop: 0,
|
|
326
367
|
sheetDrove: false,
|
|
368
|
+
contentScrolled: false,
|
|
369
|
+
canRefresh: false,
|
|
370
|
+
refreshArmed: false,
|
|
327
371
|
samples: [],
|
|
328
372
|
};
|
|
329
373
|
}
|
|
@@ -351,8 +395,14 @@ export class SheetPresenter extends ModalPresenter {
|
|
|
351
395
|
|
|
352
396
|
private activate(drag: Drag) {
|
|
353
397
|
drag.active = true;
|
|
398
|
+
const isSettled = !this.top.isAnimating;
|
|
354
399
|
this.top.stop();
|
|
355
400
|
drag.rawTop = this.top.value;
|
|
401
|
+
drag.canRefresh =
|
|
402
|
+
this.config.pullToRefreshEnabled &&
|
|
403
|
+
!this.config.dismissOnSwipe &&
|
|
404
|
+
isSettled &&
|
|
405
|
+
Math.abs(drag.rawTop - Math.max(...this.detentTops)) < 1;
|
|
356
406
|
const { surface } = this.elements!;
|
|
357
407
|
// A native scroll on an ancestor makes React Native Web cancel the press
|
|
358
408
|
// under the pointer, so dragging from a button doesn't leave it pressed.
|
|
@@ -379,21 +429,33 @@ export class SheetPresenter extends ModalPresenter {
|
|
|
379
429
|
const { scroller } = drag;
|
|
380
430
|
if (scroller) {
|
|
381
431
|
// The browser's own scrolling is prevented for this gesture, so the
|
|
382
|
-
// content is scrolled here whenever the sheet can't move.
|
|
432
|
+
// content is scrolled here whenever the sheet can't move. Up: expand
|
|
433
|
+
// the sheet before scrolling. Down: scroll back to the top before
|
|
434
|
+
// collapsing the sheet, in a separate gesture.
|
|
383
435
|
const minTop = Math.min(...this.detentTops);
|
|
384
436
|
const drive =
|
|
385
|
-
delta < 0
|
|
437
|
+
delta < 0
|
|
438
|
+
? drag.rawTop > minTop + 0.5
|
|
439
|
+
: !drag.contentScrolled && scroller.scrollTop <= 0;
|
|
386
440
|
if (drive) {
|
|
387
441
|
drag.rawTop = Math.max(drag.rawTop + delta, minTop);
|
|
388
442
|
} else {
|
|
389
443
|
scroller.scrollTop -= delta;
|
|
444
|
+
if (delta !== 0) {
|
|
445
|
+
drag.contentScrolled = true;
|
|
446
|
+
}
|
|
390
447
|
}
|
|
391
448
|
drag.sheetDrove = drive;
|
|
392
449
|
} else {
|
|
393
450
|
drag.rawTop += delta;
|
|
394
451
|
drag.sheetDrove = true;
|
|
395
452
|
}
|
|
396
|
-
|
|
453
|
+
const top = this.constrained(drag.rawTop);
|
|
454
|
+
if (drag.canRefresh) {
|
|
455
|
+
drag.refreshArmed =
|
|
456
|
+
top - Math.max(...this.detentTops) >= REFRESH_THRESHOLD;
|
|
457
|
+
}
|
|
458
|
+
this.top.set(top);
|
|
397
459
|
}
|
|
398
460
|
|
|
399
461
|
private release(drag: Drag) {
|
|
@@ -434,6 +496,9 @@ export class SheetPresenter extends ModalPresenter {
|
|
|
434
496
|
this.listener.onDetentChange(index);
|
|
435
497
|
}
|
|
436
498
|
this.settle(SETTLE, velocity);
|
|
499
|
+
if (drag.refreshArmed) {
|
|
500
|
+
this.listener.onPullToRefresh();
|
|
501
|
+
}
|
|
437
502
|
}
|
|
438
503
|
|
|
439
504
|
/** Abandons the gesture without settling (the exit or the content takes over). */
|
package/src/web/dom.ts
CHANGED
|
@@ -53,6 +53,23 @@ export function readSafeAreaInsets(): Insets {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** The part of the safe area insets that `element` overlaps. */
|
|
57
|
+
export function inlineInsets(element: Element): Insets {
|
|
58
|
+
const safeArea = readSafeAreaInsets();
|
|
59
|
+
const rect = element.getBoundingClientRect();
|
|
60
|
+
const clamp = (value: number, max: number) =>
|
|
61
|
+
Math.min(Math.max(value, 0), max);
|
|
62
|
+
return {
|
|
63
|
+
top: clamp(safeArea.top - rect.top, rect.height),
|
|
64
|
+
right: clamp(rect.right - (window.innerWidth - safeArea.right), rect.width),
|
|
65
|
+
bottom: clamp(
|
|
66
|
+
rect.bottom - (window.innerHeight - safeArea.bottom),
|
|
67
|
+
rect.height
|
|
68
|
+
),
|
|
69
|
+
left: clamp(safeArea.left - rect.left, rect.width),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
/**
|
|
57
74
|
* Height of the on-screen keyboard over a layout viewport `layoutHeight` tall.
|
|
58
75
|
* Mobile browsers keep the layout viewport and shrink the visual one.
|