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
|
@@ -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,7 @@ 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
|
|
45
50
|
|
|
46
51
|
var onPresent: (() -> Void)?
|
|
47
52
|
var onDismiss: ((DismissReason) -> Void)?
|
|
@@ -49,6 +54,8 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
49
54
|
var onBackdropPress: (() -> Void)?
|
|
50
55
|
var onBackButtonPress: (() -> Void)?
|
|
51
56
|
var onContentAreaChange: ((ModalContentArea) -> Void)?
|
|
57
|
+
var onPullToRefresh: (() -> Void)?
|
|
58
|
+
var onRestingTopChange: ((Double) -> Void)?
|
|
52
59
|
|
|
53
60
|
// MARK: State
|
|
54
61
|
|
|
@@ -66,6 +73,7 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
66
73
|
private var reconcileScheduled = false
|
|
67
74
|
private var isDropped = false
|
|
68
75
|
private var presentation: ModalPresenting?
|
|
76
|
+
private var inlineSheet: SheetView?
|
|
69
77
|
private var lastContentArea: CGSize?
|
|
70
78
|
|
|
71
79
|
override init() {
|
|
@@ -73,17 +81,24 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
73
81
|
view.onWindowChange = { [weak self] in
|
|
74
82
|
self?.hostWindowDidChange()
|
|
75
83
|
}
|
|
84
|
+
view.onLayoutChange = { [weak self] in
|
|
85
|
+
self?.hostLayoutDidChange()
|
|
86
|
+
}
|
|
76
87
|
view.contentView.onContentSizeChange = { [weak self] _ in
|
|
77
88
|
self?.presentation?.contentSizeDidChange()
|
|
89
|
+
self?.inlineSheet?.contentSizeDidChange()
|
|
78
90
|
}
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
deinit {
|
|
82
94
|
// The last reference can be dropped from the JS thread; UIKit work must
|
|
83
|
-
// happen on main. `controller`
|
|
84
|
-
|
|
95
|
+
// happen on main. `controller` and `sheet` are captured strongly on purpose.
|
|
96
|
+
let controller = presentation
|
|
97
|
+
let sheet = inlineSheet
|
|
98
|
+
guard controller != nil || sheet != nil else { return }
|
|
85
99
|
DispatchQueue.main.async {
|
|
86
|
-
controller
|
|
100
|
+
controller?.presentingViewController?.dismiss(animated: false)
|
|
101
|
+
sheet?.removeFromSuperview()
|
|
87
102
|
}
|
|
88
103
|
}
|
|
89
104
|
|
|
@@ -93,8 +108,11 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
93
108
|
let next = makeConfig()
|
|
94
109
|
guard next != config else { return }
|
|
95
110
|
config = next
|
|
111
|
+
view.isInline = next.isInline
|
|
96
112
|
if let presentation {
|
|
97
113
|
presentation.apply(next)
|
|
114
|
+
} else if let inlineSheet {
|
|
115
|
+
inlineSheet.apply(next)
|
|
98
116
|
} else {
|
|
99
117
|
reportEstimatedContentArea()
|
|
100
118
|
}
|
|
@@ -104,9 +122,13 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
104
122
|
isDropped = true
|
|
105
123
|
wantsOpen = false
|
|
106
124
|
sessionActive = false
|
|
125
|
+
phase = .idle
|
|
126
|
+
if let sheet = inlineSheet {
|
|
127
|
+
inlineSheet = nil
|
|
128
|
+
sheet.removeFromSuperview()
|
|
129
|
+
}
|
|
107
130
|
guard let controller = presentation else { return }
|
|
108
131
|
presentation = nil
|
|
109
|
-
phase = .idle
|
|
110
132
|
controller.presentingViewController?.dismiss(animated: false)
|
|
111
133
|
}
|
|
112
134
|
|
|
@@ -128,6 +150,7 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
128
150
|
guard index.isFinite else { return }
|
|
129
151
|
DispatchQueue.main.async { [weak self] in
|
|
130
152
|
self?.presentation?.snapToDetent(at: Int(index))
|
|
153
|
+
self?.inlineSheet?.snapToDetent(at: Int(index))
|
|
131
154
|
}
|
|
132
155
|
}
|
|
133
156
|
|
|
@@ -178,6 +201,10 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
178
201
|
}
|
|
179
202
|
|
|
180
203
|
private func startPresent() {
|
|
204
|
+
if config.isInline {
|
|
205
|
+
startInlinePresent()
|
|
206
|
+
return
|
|
207
|
+
}
|
|
181
208
|
// Without a window there is nothing to present from; `hostWindowDidChange` retries.
|
|
182
209
|
guard let presenter = topViewController() else { return }
|
|
183
210
|
|
|
@@ -190,12 +217,7 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
190
217
|
|
|
191
218
|
presenter.present(controller, animated: isAnimated) { [weak self, weak controller] in
|
|
192
219
|
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()
|
|
220
|
+
self.didPresent(announcing: controller.view)
|
|
199
221
|
}
|
|
200
222
|
|
|
201
223
|
if controller.presentingViewController == nil {
|
|
@@ -208,7 +230,43 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
208
230
|
}
|
|
209
231
|
}
|
|
210
232
|
|
|
233
|
+
/// Slides an inline sheet up inside `view`.
|
|
234
|
+
private func startInlinePresent() {
|
|
235
|
+
// Needs its laid-out area; `hostLayoutDidChange` retries.
|
|
236
|
+
guard view.window != nil, view.bounds.width > 0, view.bounds.height > 0 else { return }
|
|
237
|
+
|
|
238
|
+
let sheet = SheetView(contentView: view.contentView, config: config, isInline: true)
|
|
239
|
+
sheet.delegate = self
|
|
240
|
+
inlineSheet = sheet
|
|
241
|
+
phase = .presenting
|
|
242
|
+
view.showInlineSheet(sheet)
|
|
243
|
+
sheet.layoutIfNeeded()
|
|
244
|
+
sheet.animateTransition(toOffscreen: false, animated: true, duration: 0.5) { [weak self, weak sheet] in
|
|
245
|
+
guard let self, let sheet, self.inlineSheet === sheet else { return }
|
|
246
|
+
self.didPresent(announcing: nil)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private func didPresent(announcing announcedView: UIView?) {
|
|
251
|
+
phase = .presented
|
|
252
|
+
if !isDropped {
|
|
253
|
+
onPresent?()
|
|
254
|
+
}
|
|
255
|
+
if let announcedView {
|
|
256
|
+
UIAccessibility.post(notification: .screenChanged, argument: announcedView)
|
|
257
|
+
}
|
|
258
|
+
reconcile()
|
|
259
|
+
}
|
|
260
|
+
|
|
211
261
|
private func startDismiss() {
|
|
262
|
+
if let sheet = inlineSheet {
|
|
263
|
+
phase = .dismissing
|
|
264
|
+
sheet.endEditing(true)
|
|
265
|
+
sheet.animateTransition(toOffscreen: true, animated: true, duration: 0.35) { [weak self] in
|
|
266
|
+
self?.finishDismiss()
|
|
267
|
+
}
|
|
268
|
+
return
|
|
269
|
+
}
|
|
212
270
|
guard let controller = presentation, let presenting = controller.presentingViewController else {
|
|
213
271
|
finishDismiss()
|
|
214
272
|
return
|
|
@@ -223,6 +281,11 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
223
281
|
private func finishDismiss() {
|
|
224
282
|
phase = .idle
|
|
225
283
|
presentation = nil
|
|
284
|
+
if let sheet = inlineSheet {
|
|
285
|
+
inlineSheet = nil
|
|
286
|
+
sheet.unlockScroll()
|
|
287
|
+
sheet.removeFromSuperview()
|
|
288
|
+
}
|
|
226
289
|
view.contentView.removeFromSuperview()
|
|
227
290
|
endSession()
|
|
228
291
|
reconcile()
|
|
@@ -246,20 +309,24 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
246
309
|
}
|
|
247
310
|
|
|
248
311
|
private func makeConfig() -> ModalConfig {
|
|
249
|
-
|
|
312
|
+
let inline = isInline && mode == .bottomsheet
|
|
313
|
+
return ModalConfig(
|
|
250
314
|
mode: mode,
|
|
251
|
-
|
|
315
|
+
inlinePresentation: isInline,
|
|
316
|
+
detents: detents.map(Detent.init),
|
|
252
317
|
initialDetentIndex: initialDetentIndex.isFinite ? Int(initialDetentIndex) : 0,
|
|
253
318
|
backdropColor: UIColor(processedColor: backdropColor),
|
|
254
319
|
backdropOpacity: CGFloat(backdropOpacity),
|
|
255
320
|
backdropBlurRadius: CGFloat(backdropBlurRadius),
|
|
256
|
-
|
|
257
|
-
|
|
321
|
+
// An inline sheet is part of the screen; the user can't dismiss it.
|
|
322
|
+
dismissOnBackdropPress: dismissOnBackdropPress && !inline,
|
|
323
|
+
dismissOnSwipe: dismissOnSwipe && !inline,
|
|
258
324
|
grabberVisible: grabberVisible,
|
|
259
325
|
cornerRadius: cornerRadius >= 0 ? CGFloat(cornerRadius) : nil,
|
|
260
326
|
contentBackgroundColor: contentBackgroundColor.map(UIColor.init(processedColor:)) ?? .systemBackground,
|
|
261
327
|
keyboardBehavior: keyboardBehavior,
|
|
262
|
-
popupAnimation: popupAnimation
|
|
328
|
+
popupAnimation: popupAnimation,
|
|
329
|
+
pullToRefreshEnabled: pullToRefreshEnabled
|
|
263
330
|
)
|
|
264
331
|
}
|
|
265
332
|
|
|
@@ -273,23 +340,37 @@ final class HybridNitroModal: HybridNitroModalSpec {
|
|
|
273
340
|
|
|
274
341
|
private func hostWindowDidChange() {
|
|
275
342
|
guard view.window != nil else { return }
|
|
276
|
-
if presentation == nil {
|
|
343
|
+
if presentation == nil, inlineSheet == nil {
|
|
277
344
|
reportEstimatedContentArea()
|
|
278
345
|
}
|
|
279
346
|
scheduleReconcile()
|
|
280
347
|
}
|
|
281
348
|
|
|
349
|
+
private func hostLayoutDidChange() {
|
|
350
|
+
guard config.isInline, inlineSheet == nil else { return }
|
|
351
|
+
reportEstimatedContentArea()
|
|
352
|
+
if wantsOpen {
|
|
353
|
+
scheduleReconcile()
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
282
357
|
/// Lets React size the content before the first presentation.
|
|
283
358
|
private func reportEstimatedContentArea() {
|
|
284
359
|
guard let window = view.window else { return }
|
|
360
|
+
if config.isInline {
|
|
361
|
+
let bounds = view.bounds
|
|
362
|
+
let maximum = SheetView.maximumDetentValue(height: bounds.height, insets: view.safeAreaInsets, isInline: true)
|
|
363
|
+
reportContentArea(CGSize(width: bounds.width, height: SheetView.contentHeight(for: config.detents, maximum: maximum)))
|
|
364
|
+
return
|
|
365
|
+
}
|
|
285
366
|
switch config.mode {
|
|
286
367
|
case .popup:
|
|
287
368
|
reportContentArea(PopupViewController.availableRect(in: window.bounds, safeArea: window.safeAreaInsets).size)
|
|
288
369
|
case .bottomsheet:
|
|
289
|
-
let maximum =
|
|
370
|
+
let maximum = SheetView.maximumDetentValue(height: window.bounds.height, insets: window.safeAreaInsets, isInline: false)
|
|
290
371
|
reportContentArea(CGSize(
|
|
291
|
-
width:
|
|
292
|
-
height:
|
|
372
|
+
width: SheetView.estimatedWidth(in: window),
|
|
373
|
+
height: SheetView.contentHeight(for: config.detents, maximum: maximum)
|
|
293
374
|
))
|
|
294
375
|
}
|
|
295
376
|
}
|
|
@@ -325,4 +406,14 @@ extension HybridNitroModal: ModalPresentationDelegate {
|
|
|
325
406
|
func modalPresentationDidChangeContentArea(_ size: CGSize) {
|
|
326
407
|
reportContentArea(size)
|
|
327
408
|
}
|
|
409
|
+
|
|
410
|
+
func modalPresentationDidPullToRefresh() {
|
|
411
|
+
guard !isDropped else { return }
|
|
412
|
+
onPullToRefresh?()
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
func modalPresentationDidChangeRestingTop(_ top: CGFloat) {
|
|
416
|
+
guard !isDropped else { return }
|
|
417
|
+
onRestingTopChange?(Double(top))
|
|
418
|
+
}
|
|
328
419
|
}
|
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,30 @@ 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
|
+
|
|
57
|
+
/// A `bottomSheet` living in the host view's bounds. Popups are always modal.
|
|
58
|
+
var isInline: Bool {
|
|
59
|
+
inlinePresentation && mode == .bottomsheet
|
|
60
|
+
}
|
|
24
61
|
|
|
25
62
|
var clampedInitialDetentIndex: Int {
|
|
26
63
|
min(max(initialDetentIndex, 0), max(detents.count - 1, 0))
|
|
27
64
|
}
|
|
28
65
|
}
|
|
29
66
|
|
|
30
|
-
/// Callbacks from a presented modal controller back to
|
|
67
|
+
/// Callbacks from a presented modal controller (or an inline sheet) back to
|
|
68
|
+
/// `HybridNitroModal`.
|
|
31
69
|
protocol ModalPresentationDelegate: AnyObject {
|
|
32
70
|
func modalPresentationDidTapBackdrop()
|
|
33
71
|
/// The user swiped the sheet down far enough to dismiss it.
|
|
34
72
|
func modalPresentationDidRequestSwipeDismiss()
|
|
35
73
|
func modalPresentationDidChangeDetent(_ index: Int)
|
|
36
74
|
func modalPresentationDidChangeContentArea(_ size: CGSize)
|
|
75
|
+
/// The user pulled the sheet past its lowest detent and let go.
|
|
76
|
+
func modalPresentationDidPullToRefresh()
|
|
77
|
+
/// Inline sheets: the sheet's top at rest, in the host view.
|
|
78
|
+
func modalPresentationDidChangeRestingTop(_ top: CGFloat)
|
|
37
79
|
}
|
|
38
80
|
|
|
39
81
|
/// 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
|
}
|