react-native-nitro-modal 0.2.0 → 0.3.1-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 +52 -4
- package/android/src/main/java/com/margelo/nitro/nitromodal/BottomSheetPresenter.kt +399 -34
- package/android/src/main/java/com/margelo/nitro/nitromodal/HybridNitroModal.kt +67 -22
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalConfig.kt +35 -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 +115 -20
- package/ios/ModalConfig.swift +48 -2
- package/ios/NitroModalHostView.swift +73 -8
- package/ios/SheetView.swift +647 -0
- package/ios/SheetViewController.swift +19 -450
- package/lib/module/NitroModal.js +54 -6
- package/lib/module/NitroModal.js.map +1 -1
- package/lib/module/NitroModalView.web.js +86 -26
- 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 +10 -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 +118 -34
- package/lib/module/web/SheetPresenter.js.map +1 -1
- package/lib/module/web/dom.js +16 -0
- package/lib/module/web/dom.js.map +1 -1
- package/lib/typescript/src/NitroModal.d.ts +42 -2
- package/lib/typescript/src/NitroModal.d.ts.map +1 -1
- package/lib/typescript/src/NitroModal.nitro.d.ts +37 -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 +10 -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 +13 -0
- package/lib/typescript/src/web/ModalPresenter.d.ts.map +1 -1
- package/lib/typescript/src/web/SheetPresenter.d.ts +13 -2
- package/lib/typescript/src/web/SheetPresenter.d.ts.map +1 -1
- package/lib/typescript/src/web/dom.d.ts +4 -0
- package/lib/typescript/src/web/dom.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridNitroModalSpec.cpp +78 -6
- package/nitrogen/generated/android/c++/JHybridNitroModalSpec.hpp +14 -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 +30 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/HybridNitroModalSpec.kt +52 -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 +68 -8
- package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Umbrella.hpp +4 -3
- package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.hpp +46 -5
- package/nitrogen/generated/ios/c++/views/HybridNitroModalComponent.mm +36 -0
- package/nitrogen/generated/ios/swift/HybridNitroModalSpec.swift +6 -0
- package/nitrogen/generated/ios/swift/HybridNitroModalSpec_cxx.swift +158 -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 +12 -0
- package/nitrogen/generated/shared/c++/HybridNitroModalSpec.hpp +18 -5
- package/nitrogen/generated/shared/c++/{SheetDetent.hpp → NamedSheetDetent.hpp} +17 -17
- package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.cpp +13 -1
- package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.hpp +21 -2
- package/nitrogen/generated/shared/json/NitroModalConfig.json +6 -0
- package/package.json +1 -1
- package/src/NitroModal.nitro.ts +38 -2
- package/src/NitroModal.tsx +93 -4
- package/src/NitroModalView.web.tsx +79 -12
- package/src/index.tsx +2 -0
- package/src/web/AnimatedValue.ts +4 -0
- package/src/web/ModalConfig.ts +19 -3
- package/src/web/ModalController.ts +35 -2
- package/src/web/ModalPresenter.ts +50 -11
- package/src/web/SheetPresenter.ts +169 -37
- package/src/web/dom.ts +20 -0
|
@@ -12,6 +12,9 @@ import UIKit
|
|
|
12
12
|
/// Presentation is a small state machine so that rapid `isOpen` toggles,
|
|
13
13
|
/// interactive dismissals and unmounts always settle on a consistent state,
|
|
14
14
|
/// and every accepted present request ends with exactly one `onDismiss`.
|
|
15
|
+
///
|
|
16
|
+
/// A modal is presented as a view controller; an inline sheet is a
|
|
17
|
+
/// `SheetView` shown inside `view`.
|
|
15
18
|
final class HybridNitroModal: HybridNitroModalSpec {
|
|
16
19
|
let view = NitroModalHostView()
|
|
17
20
|
|
|
@@ -29,7 +32,8 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
var mode: ModalMode = .bottomsheet
|
|
32
|
-
var
|
|
35
|
+
var isInline = false
|
|
36
|
+
var detents: [SheetDetent] = [.first(.fitcontent)]
|
|
33
37
|
var initialDetentIndex: Double = 0
|
|
34
38
|
var backdropColor: Double = 0xFF00_0000
|
|
35
39
|
var backdropOpacity: Double = 0.4
|
|
@@ -42,6 +46,9 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
42
46
|
var contentBackgroundColor: Double?
|
|
43
47
|
var keyboardBehavior: KeyboardBehavior = .pan
|
|
44
48
|
var popupAnimation: PopupAnimation = .scale
|
|
49
|
+
var pullToRefreshEnabled = false
|
|
50
|
+
var refreshing: Bool?
|
|
51
|
+
var refreshIndicatorColor: Double?
|
|
45
52
|
|
|
46
53
|
var onPresent: (() -> Void)?
|
|
47
54
|
var onDismiss: ((DismissReason) -> Void)?
|
|
@@ -49,6 +56,8 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
49
56
|
var onBackdropPress: (() -> Void)?
|
|
50
57
|
var onBackButtonPress: (() -> Void)?
|
|
51
58
|
var onContentAreaChange: ((ModalContentArea) -> Void)?
|
|
59
|
+
var onPullToRefresh: (() -> Void)?
|
|
60
|
+
var onRestingTopChange: ((Double) -> Void)?
|
|
52
61
|
|
|
53
62
|
// MARK: State
|
|
54
63
|
|
|
@@ -66,6 +75,7 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
66
75
|
private var reconcileScheduled = false
|
|
67
76
|
private var isDropped = false
|
|
68
77
|
private var presentation: ModalPresenting?
|
|
78
|
+
private var inlineSheet: SheetView?
|
|
69
79
|
private var lastContentArea: CGSize?
|
|
70
80
|
|
|
71
81
|
override init() {
|
|
@@ -73,17 +83,24 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
73
83
|
view.onWindowChange = { [weak self] in
|
|
74
84
|
self?.hostWindowDidChange()
|
|
75
85
|
}
|
|
86
|
+
view.onLayoutChange = { [weak self] in
|
|
87
|
+
self?.hostLayoutDidChange()
|
|
88
|
+
}
|
|
76
89
|
view.contentView.onContentSizeChange = { [weak self] _ in
|
|
77
90
|
self?.presentation?.contentSizeDidChange()
|
|
91
|
+
self?.inlineSheet?.contentSizeDidChange()
|
|
78
92
|
}
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
deinit {
|
|
82
96
|
// The last reference can be dropped from the JS thread; UIKit work must
|
|
83
|
-
// happen on main. `controller`
|
|
84
|
-
|
|
97
|
+
// happen on main. `controller` and `sheet` are captured strongly on purpose.
|
|
98
|
+
let controller = presentation
|
|
99
|
+
let sheet = inlineSheet
|
|
100
|
+
guard controller != nil || sheet != nil else { return }
|
|
85
101
|
DispatchQueue.main.async {
|
|
86
|
-
controller
|
|
102
|
+
controller?.presentingViewController?.dismiss(animated: false)
|
|
103
|
+
sheet?.removeFromSuperview()
|
|
87
104
|
}
|
|
88
105
|
}
|
|
89
106
|
|
|
@@ -93,8 +110,11 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
93
110
|
let next = makeConfig()
|
|
94
111
|
guard next != config else { return }
|
|
95
112
|
config = next
|
|
113
|
+
view.isInline = next.isInline
|
|
96
114
|
if let presentation {
|
|
97
115
|
presentation.apply(next)
|
|
116
|
+
} else if let inlineSheet {
|
|
117
|
+
inlineSheet.apply(next)
|
|
98
118
|
} else {
|
|
99
119
|
reportEstimatedContentArea()
|
|
100
120
|
}
|
|
@@ -104,9 +124,13 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
104
124
|
isDropped = true
|
|
105
125
|
wantsOpen = false
|
|
106
126
|
sessionActive = false
|
|
127
|
+
phase = .idle
|
|
128
|
+
if let sheet = inlineSheet {
|
|
129
|
+
inlineSheet = nil
|
|
130
|
+
sheet.removeFromSuperview()
|
|
131
|
+
}
|
|
107
132
|
guard let controller = presentation else { return }
|
|
108
133
|
presentation = nil
|
|
109
|
-
phase = .idle
|
|
110
134
|
controller.presentingViewController?.dismiss(animated: false)
|
|
111
135
|
}
|
|
112
136
|
|
|
@@ -128,6 +152,7 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
128
152
|
guard index.isFinite else { return }
|
|
129
153
|
DispatchQueue.main.async { [weak self] in
|
|
130
154
|
self?.presentation?.snapToDetent(at: Int(index))
|
|
155
|
+
self?.inlineSheet?.snapToDetent(at: Int(index))
|
|
131
156
|
}
|
|
132
157
|
}
|
|
133
158
|
|
|
@@ -178,6 +203,10 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
178
203
|
}
|
|
179
204
|
|
|
180
205
|
private func startPresent() {
|
|
206
|
+
if config.isInline {
|
|
207
|
+
startInlinePresent()
|
|
208
|
+
return
|
|
209
|
+
}
|
|
181
210
|
// Without a window there is nothing to present from; `hostWindowDidChange` retries.
|
|
182
211
|
guard let presenter = topViewController() else { return }
|
|
183
212
|
|
|
@@ -190,12 +219,7 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
190
219
|
|
|
191
220
|
presenter.present(controller, animated: isAnimated) { [weak self, weak controller] in
|
|
192
221
|
guard let self, let controller, self.presentation === controller else { return }
|
|
193
|
-
self.
|
|
194
|
-
if !self.isDropped {
|
|
195
|
-
self.onPresent?()
|
|
196
|
-
}
|
|
197
|
-
UIAccessibility.post(notification: .screenChanged, argument: controller.view)
|
|
198
|
-
self.reconcile()
|
|
222
|
+
self.didPresent(announcing: controller.view)
|
|
199
223
|
}
|
|
200
224
|
|
|
201
225
|
if controller.presentingViewController == nil {
|
|
@@ -208,7 +232,43 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
208
232
|
}
|
|
209
233
|
}
|
|
210
234
|
|
|
235
|
+
/// Slides an inline sheet up inside `view`.
|
|
236
|
+
private func startInlinePresent() {
|
|
237
|
+
// Needs its laid-out area; `hostLayoutDidChange` retries.
|
|
238
|
+
guard view.window != nil, view.bounds.width > 0, view.bounds.height > 0 else { return }
|
|
239
|
+
|
|
240
|
+
let sheet = SheetView(contentView: view.contentView, config: config, isInline: true)
|
|
241
|
+
sheet.delegate = self
|
|
242
|
+
inlineSheet = sheet
|
|
243
|
+
phase = .presenting
|
|
244
|
+
view.showInlineSheet(sheet)
|
|
245
|
+
sheet.layoutIfNeeded()
|
|
246
|
+
sheet.animateTransition(toOffscreen: false, animated: true, duration: 0.5) { [weak self, weak sheet] in
|
|
247
|
+
guard let self, let sheet, self.inlineSheet === sheet else { return }
|
|
248
|
+
self.didPresent(announcing: nil)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private func didPresent(announcing announcedView: UIView?) {
|
|
253
|
+
phase = .presented
|
|
254
|
+
if !isDropped {
|
|
255
|
+
onPresent?()
|
|
256
|
+
}
|
|
257
|
+
if let announcedView {
|
|
258
|
+
UIAccessibility.post(notification: .screenChanged, argument: announcedView)
|
|
259
|
+
}
|
|
260
|
+
reconcile()
|
|
261
|
+
}
|
|
262
|
+
|
|
211
263
|
private func startDismiss() {
|
|
264
|
+
if let sheet = inlineSheet {
|
|
265
|
+
phase = .dismissing
|
|
266
|
+
sheet.endEditing(true)
|
|
267
|
+
sheet.animateTransition(toOffscreen: true, animated: true, duration: 0.35) { [weak self] in
|
|
268
|
+
self?.finishDismiss()
|
|
269
|
+
}
|
|
270
|
+
return
|
|
271
|
+
}
|
|
212
272
|
guard let controller = presentation, let presenting = controller.presentingViewController else {
|
|
213
273
|
finishDismiss()
|
|
214
274
|
return
|
|
@@ -223,6 +283,11 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
223
283
|
private func finishDismiss() {
|
|
224
284
|
phase = .idle
|
|
225
285
|
presentation = nil
|
|
286
|
+
if let sheet = inlineSheet {
|
|
287
|
+
inlineSheet = nil
|
|
288
|
+
sheet.unlockScroll()
|
|
289
|
+
sheet.removeFromSuperview()
|
|
290
|
+
}
|
|
226
291
|
view.contentView.removeFromSuperview()
|
|
227
292
|
endSession()
|
|
228
293
|
reconcile()
|
|
@@ -246,20 +311,26 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
246
311
|
}
|
|
247
312
|
|
|
248
313
|
private func makeConfig() -> ModalConfig {
|
|
249
|
-
|
|
314
|
+
let inline = isInline && mode == .bottomsheet
|
|
315
|
+
return ModalConfig(
|
|
250
316
|
mode: mode,
|
|
251
|
-
|
|
317
|
+
inlinePresentation: isInline,
|
|
318
|
+
detents: detents.map(Detent.init),
|
|
252
319
|
initialDetentIndex: initialDetentIndex.isFinite ? Int(initialDetentIndex) : 0,
|
|
253
320
|
backdropColor: UIColor(processedColor: backdropColor),
|
|
254
321
|
backdropOpacity: CGFloat(backdropOpacity),
|
|
255
322
|
backdropBlurRadius: CGFloat(backdropBlurRadius),
|
|
256
|
-
|
|
257
|
-
|
|
323
|
+
// An inline sheet is part of the screen; the user can't dismiss it.
|
|
324
|
+
dismissOnBackdropPress: dismissOnBackdropPress && !inline,
|
|
325
|
+
dismissOnSwipe: dismissOnSwipe && !inline,
|
|
258
326
|
grabberVisible: grabberVisible,
|
|
259
327
|
cornerRadius: cornerRadius >= 0 ? CGFloat(cornerRadius) : nil,
|
|
260
328
|
contentBackgroundColor: contentBackgroundColor.map(UIColor.init(processedColor:)) ?? .systemBackground,
|
|
261
329
|
keyboardBehavior: keyboardBehavior,
|
|
262
|
-
popupAnimation: popupAnimation
|
|
330
|
+
popupAnimation: popupAnimation,
|
|
331
|
+
pullToRefreshEnabled: pullToRefreshEnabled,
|
|
332
|
+
refreshing: refreshing,
|
|
333
|
+
refreshIndicatorColor: refreshIndicatorColor.map(UIColor.init(processedColor:))
|
|
263
334
|
)
|
|
264
335
|
}
|
|
265
336
|
|
|
@@ -273,23 +344,37 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
273
344
|
|
|
274
345
|
private func hostWindowDidChange() {
|
|
275
346
|
guard view.window != nil else { return }
|
|
276
|
-
if presentation == nil {
|
|
347
|
+
if presentation == nil, inlineSheet == nil {
|
|
277
348
|
reportEstimatedContentArea()
|
|
278
349
|
}
|
|
279
350
|
scheduleReconcile()
|
|
280
351
|
}
|
|
281
352
|
|
|
353
|
+
private func hostLayoutDidChange() {
|
|
354
|
+
guard config.isInline, inlineSheet == nil else { return }
|
|
355
|
+
reportEstimatedContentArea()
|
|
356
|
+
if wantsOpen {
|
|
357
|
+
scheduleReconcile()
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
282
361
|
/// Lets React size the content before the first presentation.
|
|
283
362
|
private func reportEstimatedContentArea() {
|
|
284
363
|
guard let window = view.window else { return }
|
|
364
|
+
if config.isInline {
|
|
365
|
+
let bounds = view.bounds
|
|
366
|
+
let maximum = SheetView.maximumDetentValue(height: bounds.height, insets: view.safeAreaInsets, isInline: true)
|
|
367
|
+
reportContentArea(CGSize(width: bounds.width, height: SheetView.contentHeight(for: config.detents, maximum: maximum)))
|
|
368
|
+
return
|
|
369
|
+
}
|
|
285
370
|
switch config.mode {
|
|
286
371
|
case .popup:
|
|
287
372
|
reportContentArea(PopupViewController.availableRect(in: window.bounds, safeArea: window.safeAreaInsets).size)
|
|
288
373
|
case .bottomsheet:
|
|
289
|
-
let maximum =
|
|
374
|
+
let maximum = SheetView.maximumDetentValue(height: window.bounds.height, insets: window.safeAreaInsets, isInline: false)
|
|
290
375
|
reportContentArea(CGSize(
|
|
291
|
-
width:
|
|
292
|
-
height:
|
|
376
|
+
width: SheetView.estimatedWidth(in: window),
|
|
377
|
+
height: SheetView.contentHeight(for: config.detents, maximum: maximum)
|
|
293
378
|
))
|
|
294
379
|
}
|
|
295
380
|
}
|
|
@@ -325,4 +410,14 @@ extension HybridNitroModal: ModalPresentationDelegate {
|
|
|
325
410
|
func modalPresentationDidChangeContentArea(_ size: CGSize) {
|
|
326
411
|
reportContentArea(size)
|
|
327
412
|
}
|
|
413
|
+
|
|
414
|
+
func modalPresentationDidPullToRefresh() {
|
|
415
|
+
guard !isDropped else { return }
|
|
416
|
+
onPullToRefresh?()
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
func modalPresentationDidChangeRestingTop(_ top: CGFloat) {
|
|
420
|
+
guard !isDropped else { return }
|
|
421
|
+
onRestingTopChange?(Double(top))
|
|
422
|
+
}
|
|
328
423
|
}
|
package/ios/ModalConfig.swift
CHANGED
|
@@ -5,10 +5,41 @@
|
|
|
5
5
|
|
|
6
6
|
import UIKit
|
|
7
7
|
|
|
8
|
+
/// A resting height of a `bottomSheet`.
|
|
9
|
+
enum Detent: Equatable {
|
|
10
|
+
case small, medium, large, fitContent
|
|
11
|
+
/// Content height in points, above the bottom safe area.
|
|
12
|
+
case height(CGFloat)
|
|
13
|
+
|
|
14
|
+
init(_ detent: SheetDetent) {
|
|
15
|
+
switch detent {
|
|
16
|
+
case .first(.small): self = .small
|
|
17
|
+
case .first(.medium): self = .medium
|
|
18
|
+
case .first(.large): self = .large
|
|
19
|
+
case .first(.fitcontent): self = .fitContent
|
|
20
|
+
case .second(let points): self = points.isFinite ? .height(CGFloat(points)) : .large
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Content height at this detent, given the tallest height the content may
|
|
25
|
+
/// occupy and the measured height of the content (for `fitContent`).
|
|
26
|
+
func height(maximum: CGFloat, contentHeight: CGFloat) -> CGFloat {
|
|
27
|
+
switch self {
|
|
28
|
+
case .small: return maximum * 0.25
|
|
29
|
+
case .medium: return maximum * 0.5
|
|
30
|
+
case .large: return maximum
|
|
31
|
+
case .fitContent: return min(max(contentHeight, 1), maximum)
|
|
32
|
+
case .height(let points): return min(max(points, 1), maximum)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
8
37
|
/// Snapshot of the React props that shape a presentation.
|
|
9
38
|
struct ModalConfig: Equatable {
|
|
10
39
|
var mode: ModalMode = .bottomsheet
|
|
11
|
-
|
|
40
|
+
/// An inline `bottomSheet` (see `isInline`), as opposed to a modal one.
|
|
41
|
+
var inlinePresentation = false
|
|
42
|
+
var detents: [Detent] = [.fitContent]
|
|
12
43
|
var initialDetentIndex = 0
|
|
13
44
|
var backdropColor: UIColor = .black
|
|
14
45
|
var backdropOpacity: CGFloat = 0.4
|
|
@@ -21,19 +52,34 @@ struct ModalConfig: Equatable {
|
|
|
21
52
|
var contentBackgroundColor: UIColor = .systemBackground
|
|
22
53
|
var keyboardBehavior: KeyboardBehavior = .pan
|
|
23
54
|
var popupAnimation: PopupAnimation = .scale
|
|
55
|
+
var pullToRefreshEnabled = false
|
|
56
|
+
/// A refresh is in progress; `nil` shows no refresh indicator.
|
|
57
|
+
var refreshing: Bool?
|
|
58
|
+
/// `nil` uses the platform default.
|
|
59
|
+
var refreshIndicatorColor: UIColor?
|
|
60
|
+
|
|
61
|
+
/// A `bottomSheet` living in the host view's bounds. Popups are always modal.
|
|
62
|
+
var isInline: Bool {
|
|
63
|
+
inlinePresentation && mode == .bottomsheet
|
|
64
|
+
}
|
|
24
65
|
|
|
25
66
|
var clampedInitialDetentIndex: Int {
|
|
26
67
|
min(max(initialDetentIndex, 0), max(detents.count - 1, 0))
|
|
27
68
|
}
|
|
28
69
|
}
|
|
29
70
|
|
|
30
|
-
/// Callbacks from a presented modal controller back to
|
|
71
|
+
/// Callbacks from a presented modal controller (or an inline sheet) back to
|
|
72
|
+
/// `HybridNitroModal`.
|
|
31
73
|
protocol ModalPresentationDelegate: AnyObject {
|
|
32
74
|
func modalPresentationDidTapBackdrop()
|
|
33
75
|
/// The user swiped the sheet down far enough to dismiss it.
|
|
34
76
|
func modalPresentationDidRequestSwipeDismiss()
|
|
35
77
|
func modalPresentationDidChangeDetent(_ index: Int)
|
|
36
78
|
func modalPresentationDidChangeContentArea(_ size: CGSize)
|
|
79
|
+
/// The user pulled the sheet past its lowest detent and let go.
|
|
80
|
+
func modalPresentationDidPullToRefresh()
|
|
81
|
+
/// Inline sheets: the sheet's top at rest, in the host view.
|
|
82
|
+
func modalPresentationDidChangeRestingTop(_ top: CGFloat)
|
|
37
83
|
}
|
|
38
84
|
|
|
39
85
|
/// A view controller that presents the React content in one of the modal modes.
|
|
@@ -5,12 +5,24 @@
|
|
|
5
5
|
|
|
6
6
|
import UIKit
|
|
7
7
|
|
|
8
|
-
/// The view Nitro mounts into the React tree.
|
|
9
|
-
///
|
|
10
|
-
///
|
|
8
|
+
/// The view Nitro mounts into the React tree. React children mounted into it
|
|
9
|
+
/// (see `HybridNitroModalComponent+Children.mm`) are hosted by `contentView`.
|
|
10
|
+
///
|
|
11
|
+
/// Modal: an invisible placeholder; `contentView` travels into the presented
|
|
12
|
+
/// controller. Inline: the area the sheet lives in. It hosts the `SheetView`
|
|
13
|
+
/// and only takes touches that land on the sheet.
|
|
11
14
|
final class NitroModalHostView: UIView {
|
|
12
15
|
let contentView = NitroModalContentView()
|
|
13
16
|
var onWindowChange: (() -> Void)?
|
|
17
|
+
/// The bounds or safe area changed.
|
|
18
|
+
var onLayoutChange: (() -> Void)?
|
|
19
|
+
|
|
20
|
+
var isInline = false {
|
|
21
|
+
didSet {
|
|
22
|
+
isUserInteractionEnabled = isInline
|
|
23
|
+
contentView.isInline = isInline
|
|
24
|
+
}
|
|
25
|
+
}
|
|
14
26
|
|
|
15
27
|
override init(frame: CGRect) {
|
|
16
28
|
super.init(frame: frame)
|
|
@@ -27,6 +39,27 @@ final class NitroModalHostView: UIView {
|
|
|
27
39
|
onWindowChange?()
|
|
28
40
|
}
|
|
29
41
|
|
|
42
|
+
override func layoutSubviews() {
|
|
43
|
+
super.layoutSubviews()
|
|
44
|
+
onLayoutChange?()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override func safeAreaInsetsDidChange() {
|
|
48
|
+
super.safeAreaInsetsDidChange()
|
|
49
|
+
onLayoutChange?()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
53
|
+
let hit = super.hitTest(point, with: event)
|
|
54
|
+
return hit === self ? nil : hit
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func showInlineSheet(_ sheet: SheetView) {
|
|
58
|
+
sheet.frame = bounds
|
|
59
|
+
sheet.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
60
|
+
addSubview(sheet)
|
|
61
|
+
}
|
|
62
|
+
|
|
30
63
|
@objc(nitroModalMountChild:atIndex:)
|
|
31
64
|
func mountChild(_ child: UIView, atIndex index: Int) {
|
|
32
65
|
contentView.insertReactChild(child, at: index)
|
|
@@ -42,22 +75,44 @@ final class NitroModalHostView: UIView {
|
|
|
42
75
|
}
|
|
43
76
|
}
|
|
44
77
|
|
|
45
|
-
/// Holds the React children inside the
|
|
46
|
-
///
|
|
78
|
+
/// Holds the React children inside the modal and reports the size they
|
|
79
|
+
/// occupy, which drives the `fitContent` detent and the popup card size.
|
|
47
80
|
final class NitroModalContentView: UIView {
|
|
48
81
|
var onContentSizeChange: ((CGSize) -> Void)?
|
|
49
82
|
|
|
83
|
+
/// Hosted by an inline sheet, inside the React root view.
|
|
84
|
+
///
|
|
85
|
+
/// The Fabric touch handler attached to this view is then disabled: a modal
|
|
86
|
+
/// lives outside the root view and needs its own, but inline the root's
|
|
87
|
+
/// handler already sees these touches.
|
|
88
|
+
var isInline = false {
|
|
89
|
+
didSet {
|
|
90
|
+
updateTouchHandlers()
|
|
91
|
+
setNeedsLayout()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
50
95
|
private var reactChildren: [UIView] = []
|
|
51
96
|
private var observations: [ObjectIdentifier: [NSKeyValueObservation]] = [:]
|
|
52
97
|
private var lastReportedSize: CGSize = .zero
|
|
53
98
|
|
|
99
|
+
/// Where React placed the children. Inline, React lays the content out at
|
|
100
|
+
/// the sheet's resting top so that `measure()` matches the screen; this
|
|
101
|
+
/// view's bounds origin cancels that offset out on screen.
|
|
102
|
+
private var reactOrigin: CGPoint {
|
|
103
|
+
guard isInline else { return .zero }
|
|
104
|
+
let minY = reactChildren.map { $0.center.y - $0.bounds.height / 2 }.min() ?? 0
|
|
105
|
+
return CGPoint(x: 0, y: minY)
|
|
106
|
+
}
|
|
107
|
+
|
|
54
108
|
/// Bounding size of the React children. Fabric positions them with
|
|
55
109
|
/// `center`/`bounds`, so this stays correct even with transforms applied.
|
|
56
110
|
var contentSize: CGSize {
|
|
57
|
-
|
|
111
|
+
let origin = reactOrigin
|
|
112
|
+
return reactChildren.reduce(.zero) { size, child in
|
|
58
113
|
CGSize(
|
|
59
|
-
width: max(size.width, child.center.x + child.bounds.width / 2),
|
|
60
|
-
height: max(size.height, child.center.y + child.bounds.height / 2)
|
|
114
|
+
width: max(size.width, child.center.x + child.bounds.width / 2 - origin.x),
|
|
115
|
+
height: max(size.height, child.center.y + child.bounds.height / 2 - origin.y)
|
|
61
116
|
)
|
|
62
117
|
}
|
|
63
118
|
}
|
|
@@ -71,6 +126,8 @@ final class NitroModalContentView: UIView {
|
|
|
71
126
|
child.layer.observe(\.bounds) { [weak self] _, _ in self?.setNeedsLayout() },
|
|
72
127
|
child.layer.observe(\.position) { [weak self] _, _ in self?.setNeedsLayout() },
|
|
73
128
|
]
|
|
129
|
+
// The touch handler is attached right before the first child mounts.
|
|
130
|
+
updateTouchHandlers()
|
|
74
131
|
setNeedsLayout()
|
|
75
132
|
}
|
|
76
133
|
|
|
@@ -83,9 +140,17 @@ final class NitroModalContentView: UIView {
|
|
|
83
140
|
|
|
84
141
|
override func layoutSubviews() {
|
|
85
142
|
super.layoutSubviews()
|
|
143
|
+
let origin = reactOrigin
|
|
144
|
+
if bounds.origin != origin {
|
|
145
|
+
bounds.origin = origin
|
|
146
|
+
}
|
|
86
147
|
let size = contentSize
|
|
87
148
|
guard size != lastReportedSize else { return }
|
|
88
149
|
lastReportedSize = size
|
|
89
150
|
onContentSizeChange?(size)
|
|
90
151
|
}
|
|
152
|
+
|
|
153
|
+
private func updateTouchHandlers() {
|
|
154
|
+
gestureRecognizers?.forEach { $0.isEnabled = !isInline }
|
|
155
|
+
}
|
|
91
156
|
}
|