concentric-sheet 0.0.1
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/NitroConcentricSheet.podspec +31 -0
- package/README.md +95 -0
- package/ios/Bridge.h +8 -0
- package/ios/SheetModalController.swift +294 -0
- package/lib/NativeSheetModal.d.ts +22 -0
- package/lib/NativeSheetModal.js +128 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +1 -0
- package/lib/specs/SheetModalController.nitro.d.ts +34 -0
- package/lib/specs/SheetModalController.nitro.js +1 -0
- package/nitro.json +23 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/ios/NitroConcentricSheet+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroConcentricSheet-Swift-Cxx-Bridge.cpp +33 -0
- package/nitrogen/generated/ios/NitroConcentricSheet-Swift-Cxx-Bridge.hpp +184 -0
- package/nitrogen/generated/ios/NitroConcentricSheet-Swift-Cxx-Umbrella.hpp +63 -0
- package/nitrogen/generated/ios/NitroConcentricSheetAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroConcentricSheetAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridSheetModalControllerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridSheetModalControllerSpecSwift.hpp +108 -0
- package/nitrogen/generated/ios/swift/HybridSheetModalControllerSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridSheetModalControllerSpec_cxx.swift +150 -0
- package/nitrogen/generated/ios/swift/ModalCornerConfiguration.swift +83 -0
- package/nitrogen/generated/ios/swift/ModalCornerConfigurationType.swift +48 -0
- package/nitrogen/generated/ios/swift/ModalViewBackground.swift +40 -0
- package/nitrogen/generated/ios/swift/PresentedModalConfig.swift +111 -0
- package/nitrogen/generated/ios/swift/SheetDetentIdentifier.swift +40 -0
- package/nitrogen/generated/ios/swift/SheetPresentationConfig.swift +160 -0
- package/nitrogen/generated/shared/c++/HybridSheetModalControllerSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridSheetModalControllerSpec.hpp +64 -0
- package/nitrogen/generated/shared/c++/ModalCornerConfiguration.hpp +97 -0
- package/nitrogen/generated/shared/c++/ModalCornerConfigurationType.hpp +84 -0
- package/nitrogen/generated/shared/c++/ModalViewBackground.hpp +76 -0
- package/nitrogen/generated/shared/c++/PresentedModalConfig.hpp +111 -0
- package/nitrogen/generated/shared/c++/SheetDetentIdentifier.hpp +76 -0
- package/nitrogen/generated/shared/c++/SheetPresentationConfig.hpp +114 -0
- package/package.json +103 -0
- package/react-native.config.js +13 -0
- package/src/NativeSheetModal.tsx +204 -0
- package/src/index.ts +15 -0
- package/src/specs/SheetModalController.nitro.ts +41 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "NitroConcentricSheet"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported, :visionos => 1.0 }
|
|
14
|
+
s.source = { :git => "https://github.com/mrousavy/nitro.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = [
|
|
17
|
+
# Implementation (Swift)
|
|
18
|
+
"ios/**/*.{swift}",
|
|
19
|
+
# Autolinking/Registration (Objective-C++)
|
|
20
|
+
"ios/**/*.{m,mm}",
|
|
21
|
+
# Implementation (C++ objects)
|
|
22
|
+
"cpp/**/*.{hpp,cpp}",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
load 'nitrogen/generated/ios/NitroConcentricSheet+autolinking.rb'
|
|
26
|
+
add_nitrogen_files(s)
|
|
27
|
+
|
|
28
|
+
s.dependency 'React-jsi'
|
|
29
|
+
s.dependency 'React-callinvoker'
|
|
30
|
+
install_modules_dependencies(s)
|
|
31
|
+
end
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ConcentricSheet
|
|
2
|
+
|
|
3
|
+
Nitro-powered React Native `Modal` replacement for iOS sheet presentation.
|
|
4
|
+
|
|
5
|
+
This package keeps React Native's `Modal` behavior, and adds runtime access to
|
|
6
|
+
native `UIViewController` / `UISheetPresentationController` options such as:
|
|
7
|
+
|
|
8
|
+
- Detents (`medium`, `large`)
|
|
9
|
+
- Grabber visibility
|
|
10
|
+
- Preferred corner radius
|
|
11
|
+
- Selected detent
|
|
12
|
+
- Largest undimmed detent
|
|
13
|
+
- Edge-attached behavior
|
|
14
|
+
- Interactive dismissal lock (`isModalInPresentation`)
|
|
15
|
+
- Preferred content width/height
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
bun add concentric-sheet react-native-nitro-modules
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then install iOS pods in your app:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
cd ios && pod install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import React from 'react'
|
|
33
|
+
import { View, Text, Button } from 'react-native'
|
|
34
|
+
import { Modal } from 'concentric-sheet'
|
|
35
|
+
|
|
36
|
+
export function Example() {
|
|
37
|
+
const [visible, setVisible] = React.useState(false)
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<Button title="Open" onPress={() => setVisible(true)} />
|
|
42
|
+
<Modal
|
|
43
|
+
visible={visible}
|
|
44
|
+
presentationStyle="pageSheet"
|
|
45
|
+
onRequestClose={() => setVisible(false)}
|
|
46
|
+
detents={['medium', 'large']}
|
|
47
|
+
selectedDetentIdentifier="medium"
|
|
48
|
+
largestUndimmedDetentIdentifier="medium"
|
|
49
|
+
prefersGrabberVisible
|
|
50
|
+
preferredCornerRadius={24}
|
|
51
|
+
prefersScrollingExpandsWhenScrolledToEdge={false}
|
|
52
|
+
prefersEdgeAttachedInCompactHeight
|
|
53
|
+
widthFollowsPreferredContentSizeWhenEdgeAttached
|
|
54
|
+
isModalInPresentation={false}
|
|
55
|
+
>
|
|
56
|
+
<View style={{ flex: 1, padding: 20 }}>
|
|
57
|
+
<Text>Native sheet modal</Text>
|
|
58
|
+
<Button title="Close" onPress={() => setVisible(false)} />
|
|
59
|
+
</View>
|
|
60
|
+
</Modal>
|
|
61
|
+
</>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
68
|
+
`Modal` accepts all React Native `Modal` props, plus:
|
|
69
|
+
|
|
70
|
+
- `detents?: ('medium' | 'large')[]`
|
|
71
|
+
- `selectedDetentIdentifier?: 'medium' | 'large'`
|
|
72
|
+
- `largestUndimmedDetentIdentifier?: 'medium' | 'large'`
|
|
73
|
+
- `prefersGrabberVisible?: boolean`
|
|
74
|
+
- `preferredCornerRadius?: number`
|
|
75
|
+
- `prefersScrollingExpandsWhenScrolledToEdge?: boolean`
|
|
76
|
+
- `prefersEdgeAttachedInCompactHeight?: boolean`
|
|
77
|
+
- `widthFollowsPreferredContentSizeWhenEdgeAttached?: boolean`
|
|
78
|
+
- `isModalInPresentation?: boolean`
|
|
79
|
+
- `preferredContentWidth?: number`
|
|
80
|
+
- `preferredContentHeight?: number`
|
|
81
|
+
- `modalViewBackground?: 'clear' | 'systemBackground'`
|
|
82
|
+
- `cornerConfiguration?: { type: 'none' | 'fixed' | 'containerConcentric' | 'capsule', radius?: number, minimumRadius?: number, maximumRadius?: number }`
|
|
83
|
+
|
|
84
|
+
Imperative helpers:
|
|
85
|
+
|
|
86
|
+
- `applyPresentedModalConfig(config)`
|
|
87
|
+
- `dismissPresentedNativeModal(animated?)`
|
|
88
|
+
|
|
89
|
+
## Notes
|
|
90
|
+
|
|
91
|
+
- iOS-only native implementation.
|
|
92
|
+
- `UISheetPresentationController` options require iOS 15+.
|
|
93
|
+
- `cornerConfiguration` requires iOS 26+.
|
|
94
|
+
- For sheet behavior, use `presentationStyle="pageSheet"` or `formSheet`.
|
|
95
|
+
- Setting `largestUndimmedDetentIdentifier` can disable outside-tap dismissal at/under that detent because the dimming view is removed.
|
package/ios/Bridge.h
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
public final class SheetModalController: HybridSheetModalControllerSpec {
|
|
5
|
+
public override init() {
|
|
6
|
+
super.init()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
public func applyPresentedModalConfig(config: PresentedModalConfig) throws -> Bool {
|
|
10
|
+
return try onMainThread {
|
|
11
|
+
guard let controller = topPresentedViewController() else {
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var didApply = false
|
|
16
|
+
if let isModalInPresentation = config.isModalInPresentation {
|
|
17
|
+
controller.isModalInPresentation = isModalInPresentation
|
|
18
|
+
didApply = true
|
|
19
|
+
}
|
|
20
|
+
if config.preferredContentWidth != nil || config.preferredContentHeight != nil {
|
|
21
|
+
var preferredContentSize = controller.preferredContentSize
|
|
22
|
+
if let width = config.preferredContentWidth {
|
|
23
|
+
preferredContentSize.width = CGFloat(width)
|
|
24
|
+
}
|
|
25
|
+
if let height = config.preferredContentHeight {
|
|
26
|
+
preferredContentSize.height = CGFloat(height)
|
|
27
|
+
}
|
|
28
|
+
controller.preferredContentSize = preferredContentSize
|
|
29
|
+
didApply = true
|
|
30
|
+
}
|
|
31
|
+
if let modalViewBackground = config.modalViewBackground {
|
|
32
|
+
applyModalViewBackground(modalViewBackground, to: controller)
|
|
33
|
+
didApply = true
|
|
34
|
+
}
|
|
35
|
+
if let cornerConfiguration = config.cornerConfiguration {
|
|
36
|
+
if #available(iOS 26.0, *) {
|
|
37
|
+
let didFullyApply = applyCornerConfiguration(cornerConfiguration, to: controller)
|
|
38
|
+
didApply = didApply || didFullyApply
|
|
39
|
+
} else {
|
|
40
|
+
didApply = true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if let sheetConfig = config.sheet {
|
|
45
|
+
if #available(iOS 15.0, *), let sheet = controller.sheetPresentationController {
|
|
46
|
+
applySheetConfig(sheetConfig, to: sheet)
|
|
47
|
+
if #available(iOS 26.0, *),
|
|
48
|
+
let cornerConfiguration = config.cornerConfiguration
|
|
49
|
+
{
|
|
50
|
+
// Corner configuration is authoritative when provided.
|
|
51
|
+
// For concentric/capsule we clear preferredCornerRadius so UIKit can
|
|
52
|
+
// use its own sheet geometry instead of a forced fixed radius.
|
|
53
|
+
sheet.preferredCornerRadius = derivedSheetCornerRadius(from: cornerConfiguration)
|
|
54
|
+
} else if sheetConfig.preferredCornerRadius == nil {
|
|
55
|
+
sheet.preferredCornerRadius = nil
|
|
56
|
+
}
|
|
57
|
+
didApply = true
|
|
58
|
+
} else {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return didApply || config.sheet == nil
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public func dismissPresentedModal(animated: Bool) throws -> Bool {
|
|
68
|
+
return try onMainThread {
|
|
69
|
+
guard let topController = topPresentedViewController(),
|
|
70
|
+
topController.presentingViewController != nil
|
|
71
|
+
else {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
topController.dismiss(animated: animated)
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@available(iOS 15.0, *)
|
|
81
|
+
private func applySheetConfig(
|
|
82
|
+
_ config: SheetPresentationConfig,
|
|
83
|
+
to sheet: UISheetPresentationController
|
|
84
|
+
) {
|
|
85
|
+
if let detents = config.detents, !detents.isEmpty {
|
|
86
|
+
sheet.detents = detents.map(detent(for:))
|
|
87
|
+
}
|
|
88
|
+
if let selectedDetentIdentifier = config.selectedDetentIdentifier {
|
|
89
|
+
sheet.selectedDetentIdentifier = detentIdentifier(for: selectedDetentIdentifier)
|
|
90
|
+
}
|
|
91
|
+
if let largestUndimmedDetentIdentifier = config.largestUndimmedDetentIdentifier {
|
|
92
|
+
sheet.largestUndimmedDetentIdentifier = detentIdentifier(for: largestUndimmedDetentIdentifier)
|
|
93
|
+
}
|
|
94
|
+
if let prefersGrabberVisible = config.prefersGrabberVisible {
|
|
95
|
+
sheet.prefersGrabberVisible = prefersGrabberVisible
|
|
96
|
+
}
|
|
97
|
+
if let preferredCornerRadius = config.preferredCornerRadius {
|
|
98
|
+
sheet.preferredCornerRadius = CGFloat(preferredCornerRadius)
|
|
99
|
+
}
|
|
100
|
+
if let prefersScrollingExpandsWhenScrolledToEdge =
|
|
101
|
+
config.prefersScrollingExpandsWhenScrolledToEdge
|
|
102
|
+
{
|
|
103
|
+
sheet.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
|
|
104
|
+
}
|
|
105
|
+
if let prefersEdgeAttachedInCompactHeight = config.prefersEdgeAttachedInCompactHeight {
|
|
106
|
+
sheet.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
|
|
107
|
+
}
|
|
108
|
+
if let widthFollowsPreferredContentSizeWhenEdgeAttached =
|
|
109
|
+
config.widthFollowsPreferredContentSizeWhenEdgeAttached
|
|
110
|
+
{
|
|
111
|
+
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached =
|
|
112
|
+
widthFollowsPreferredContentSizeWhenEdgeAttached
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@available(iOS 15.0, *)
|
|
117
|
+
private func detent(for identifier: SheetDetentIdentifier)
|
|
118
|
+
-> UISheetPresentationController.Detent
|
|
119
|
+
{
|
|
120
|
+
switch identifier {
|
|
121
|
+
case .medium:
|
|
122
|
+
return .medium()
|
|
123
|
+
case .large:
|
|
124
|
+
return .large()
|
|
125
|
+
@unknown default:
|
|
126
|
+
return .large()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@available(iOS 15.0, *)
|
|
131
|
+
private func detentIdentifier(
|
|
132
|
+
for identifier: SheetDetentIdentifier
|
|
133
|
+
) -> UISheetPresentationController.Detent.Identifier {
|
|
134
|
+
switch identifier {
|
|
135
|
+
case .medium:
|
|
136
|
+
return .medium
|
|
137
|
+
case .large:
|
|
138
|
+
return .large
|
|
139
|
+
@unknown default:
|
|
140
|
+
return .large
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private func applyModalViewBackground(
|
|
145
|
+
_ modalViewBackground: ModalViewBackground,
|
|
146
|
+
to controller: UIViewController
|
|
147
|
+
) {
|
|
148
|
+
let color: UIColor
|
|
149
|
+
switch modalViewBackground {
|
|
150
|
+
case .clear:
|
|
151
|
+
color = .clear
|
|
152
|
+
case .systembackground:
|
|
153
|
+
color = .systemBackground
|
|
154
|
+
@unknown default:
|
|
155
|
+
color = .systemBackground
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
controller.view.backgroundColor = color
|
|
159
|
+
controller.view.isOpaque = color != .clear
|
|
160
|
+
|
|
161
|
+
// React Native mounts its modal content as a subview of the presented controller's root view.
|
|
162
|
+
// Keep those roots aligned with the requested background mode to avoid accidental opaque fills.
|
|
163
|
+
for subview in controller.view.subviews {
|
|
164
|
+
subview.backgroundColor = color
|
|
165
|
+
subview.isOpaque = color != .clear
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@available(iOS 26.0, *)
|
|
170
|
+
private func applyCornerConfiguration(
|
|
171
|
+
_ configuration: ModalCornerConfiguration,
|
|
172
|
+
to controller: UIViewController
|
|
173
|
+
) -> Bool {
|
|
174
|
+
let appliedConfiguration: UICornerConfiguration
|
|
175
|
+
|
|
176
|
+
switch configuration.type {
|
|
177
|
+
case .none:
|
|
178
|
+
appliedConfiguration = .corners(radius: .fixed(0))
|
|
179
|
+
case .fixed:
|
|
180
|
+
let radius = max(0, configuration.radius ?? 0)
|
|
181
|
+
appliedConfiguration = .corners(radius: .fixed(radius))
|
|
182
|
+
case .containerconcentric:
|
|
183
|
+
if let minimumRadius = configuration.minimumRadius {
|
|
184
|
+
appliedConfiguration = .corners(
|
|
185
|
+
radius: .containerConcentric(minimum: max(0, minimumRadius))
|
|
186
|
+
)
|
|
187
|
+
} else {
|
|
188
|
+
appliedConfiguration = .corners(radius: .containerConcentric())
|
|
189
|
+
}
|
|
190
|
+
case .capsule:
|
|
191
|
+
if let maximumRadius = configuration.maximumRadius {
|
|
192
|
+
appliedConfiguration = .capsule(maximumRadius: max(0, maximumRadius))
|
|
193
|
+
} else {
|
|
194
|
+
appliedConfiguration = .capsule()
|
|
195
|
+
}
|
|
196
|
+
@unknown default:
|
|
197
|
+
appliedConfiguration = .corners(radius: .fixed(0))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return applyCornerConfigurationNow(
|
|
201
|
+
appliedConfiguration,
|
|
202
|
+
cornerType: configuration.type,
|
|
203
|
+
to: controller
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@available(iOS 26.0, *)
|
|
208
|
+
private func applyCornerConfigurationNow(
|
|
209
|
+
_ cornerConfiguration: UICornerConfiguration,
|
|
210
|
+
cornerType: ModalCornerConfigurationType,
|
|
211
|
+
to controller: UIViewController
|
|
212
|
+
) -> Bool {
|
|
213
|
+
let contentView = controller.view!
|
|
214
|
+
let reactRootSubview = contentView.subviews.first
|
|
215
|
+
|
|
216
|
+
CATransaction.begin()
|
|
217
|
+
CATransaction.setDisableActions(true)
|
|
218
|
+
contentView.cornerConfiguration = cornerConfiguration
|
|
219
|
+
reactRootSubview?.cornerConfiguration = cornerConfiguration
|
|
220
|
+
|
|
221
|
+
let shouldClipContents = cornerType != .none
|
|
222
|
+
contentView.clipsToBounds = shouldClipContents
|
|
223
|
+
contentView.layer.masksToBounds = shouldClipContents
|
|
224
|
+
reactRootSubview?.clipsToBounds = shouldClipContents
|
|
225
|
+
reactRootSubview?.layer.masksToBounds = shouldClipContents
|
|
226
|
+
CATransaction.commit()
|
|
227
|
+
|
|
228
|
+
// React content can mount after present; return false to trigger JS retry until mounted.
|
|
229
|
+
return reactRootSubview != nil
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private func derivedSheetCornerRadius(
|
|
233
|
+
from configuration: ModalCornerConfiguration
|
|
234
|
+
) -> CGFloat? {
|
|
235
|
+
switch configuration.type {
|
|
236
|
+
case .none:
|
|
237
|
+
return 0
|
|
238
|
+
case .fixed:
|
|
239
|
+
return CGFloat(max(0, configuration.radius ?? 0))
|
|
240
|
+
case .containerconcentric, .capsule:
|
|
241
|
+
return nil
|
|
242
|
+
@unknown default:
|
|
243
|
+
return nil
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private func onMainThread<T>(_ block: () throws -> T) throws -> T {
|
|
248
|
+
if Thread.isMainThread {
|
|
249
|
+
return try block()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
var result: Result<T, Error>?
|
|
253
|
+
DispatchQueue.main.sync {
|
|
254
|
+
result = Result(catching: block)
|
|
255
|
+
}
|
|
256
|
+
return try result!.get()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private func topPresentedViewController() -> UIViewController? {
|
|
260
|
+
guard let rootViewController = activeRootViewController() else { return nil }
|
|
261
|
+
var controller = rootViewController
|
|
262
|
+
var hasPresentedController = false
|
|
263
|
+
|
|
264
|
+
while let presented = controller.presentedViewController {
|
|
265
|
+
controller = presented
|
|
266
|
+
hasPresentedController = true
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return hasPresentedController ? controller : nil
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private func activeRootViewController() -> UIViewController? {
|
|
273
|
+
let connectedScenes = UIApplication.shared.connectedScenes.compactMap {
|
|
274
|
+
$0 as? UIWindowScene
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let activeScenes = connectedScenes.filter {
|
|
278
|
+
$0.activationState == .foregroundActive || $0.activationState == .foregroundInactive
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for scene in activeScenes {
|
|
282
|
+
if let keyWindow = scene.windows.first(where: \.isKeyWindow),
|
|
283
|
+
let rootViewController = keyWindow.rootViewController
|
|
284
|
+
{
|
|
285
|
+
return rootViewController
|
|
286
|
+
}
|
|
287
|
+
if let rootViewController = scene.windows.first?.rootViewController {
|
|
288
|
+
return rootViewController
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return nil
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type ModalProps as RNModalProps } from 'react-native';
|
|
3
|
+
import type { ModalCornerConfiguration, ModalViewBackground, PresentedModalConfig, SheetDetentIdentifier } from './specs/SheetModalController.nitro';
|
|
4
|
+
export interface NativeSheetModalProps extends RNModalProps {
|
|
5
|
+
detents?: SheetDetentIdentifier[];
|
|
6
|
+
selectedDetentIdentifier?: SheetDetentIdentifier;
|
|
7
|
+
largestUndimmedDetentIdentifier?: SheetDetentIdentifier;
|
|
8
|
+
prefersGrabberVisible?: boolean;
|
|
9
|
+
preferredCornerRadius?: number;
|
|
10
|
+
prefersScrollingExpandsWhenScrolledToEdge?: boolean;
|
|
11
|
+
prefersEdgeAttachedInCompactHeight?: boolean;
|
|
12
|
+
widthFollowsPreferredContentSizeWhenEdgeAttached?: boolean;
|
|
13
|
+
isModalInPresentation?: boolean;
|
|
14
|
+
preferredContentWidth?: number;
|
|
15
|
+
preferredContentHeight?: number;
|
|
16
|
+
modalViewBackground?: ModalViewBackground;
|
|
17
|
+
cornerConfiguration?: ModalCornerConfiguration;
|
|
18
|
+
}
|
|
19
|
+
export declare function applyPresentedModalConfig(config: PresentedModalConfig): boolean;
|
|
20
|
+
export declare function dismissPresentedNativeModal(animated?: boolean): boolean;
|
|
21
|
+
export declare function Modal(props: NativeSheetModalProps): React.JSX.Element;
|
|
22
|
+
export default Modal;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { Modal as RNModal, Platform, } from 'react-native';
|
|
3
|
+
import { getHybridObjectConstructor } from 'react-native-nitro-modules';
|
|
4
|
+
const APPLY_RETRY_LIMIT = 8;
|
|
5
|
+
const APPLY_RETRY_DELAY_MS = 16;
|
|
6
|
+
let cachedController = null;
|
|
7
|
+
let didFailToCreateController = false;
|
|
8
|
+
function getSheetModalController() {
|
|
9
|
+
if (Platform.OS !== 'ios')
|
|
10
|
+
return null;
|
|
11
|
+
if (cachedController != null)
|
|
12
|
+
return cachedController;
|
|
13
|
+
if (didFailToCreateController)
|
|
14
|
+
return null;
|
|
15
|
+
try {
|
|
16
|
+
const SheetModalControllerCtor = getHybridObjectConstructor('SheetModalController');
|
|
17
|
+
cachedController = new SheetModalControllerCtor();
|
|
18
|
+
return cachedController;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
didFailToCreateController = true;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function applyPresentedModalConfig(config) {
|
|
26
|
+
const controller = getSheetModalController();
|
|
27
|
+
if (controller == null)
|
|
28
|
+
return false;
|
|
29
|
+
return controller.applyPresentedModalConfig(config);
|
|
30
|
+
}
|
|
31
|
+
export function dismissPresentedNativeModal(animated = true) {
|
|
32
|
+
const controller = getSheetModalController();
|
|
33
|
+
if (controller == null)
|
|
34
|
+
return false;
|
|
35
|
+
return controller.dismissPresentedModal(animated);
|
|
36
|
+
}
|
|
37
|
+
export function Modal(props) {
|
|
38
|
+
const { detents, selectedDetentIdentifier, largestUndimmedDetentIdentifier, prefersGrabberVisible, preferredCornerRadius, prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight, widthFollowsPreferredContentSizeWhenEdgeAttached, isModalInPresentation, preferredContentWidth, preferredContentHeight, modalViewBackground, cornerConfiguration, allowSwipeDismissal, onShow: onShowProp, visible, ...modalProps } = props;
|
|
39
|
+
const resolvedAllowSwipeDismissal = allowSwipeDismissal ??
|
|
40
|
+
(isModalInPresentation == null ? true : !isModalInPresentation);
|
|
41
|
+
const timeoutRef = useRef(null);
|
|
42
|
+
const isVisibleRef = useRef(visible);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
isVisibleRef.current = visible;
|
|
45
|
+
}, [visible]);
|
|
46
|
+
const nativeConfig = useMemo(() => {
|
|
47
|
+
const sheet = detents == null &&
|
|
48
|
+
selectedDetentIdentifier == null &&
|
|
49
|
+
largestUndimmedDetentIdentifier == null &&
|
|
50
|
+
prefersGrabberVisible == null &&
|
|
51
|
+
preferredCornerRadius == null &&
|
|
52
|
+
prefersScrollingExpandsWhenScrolledToEdge == null &&
|
|
53
|
+
prefersEdgeAttachedInCompactHeight == null &&
|
|
54
|
+
widthFollowsPreferredContentSizeWhenEdgeAttached == null
|
|
55
|
+
? undefined
|
|
56
|
+
: {
|
|
57
|
+
detents,
|
|
58
|
+
selectedDetentIdentifier,
|
|
59
|
+
largestUndimmedDetentIdentifier,
|
|
60
|
+
prefersGrabberVisible,
|
|
61
|
+
preferredCornerRadius,
|
|
62
|
+
prefersScrollingExpandsWhenScrolledToEdge,
|
|
63
|
+
prefersEdgeAttachedInCompactHeight,
|
|
64
|
+
widthFollowsPreferredContentSizeWhenEdgeAttached,
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
isModalInPresentation,
|
|
68
|
+
preferredContentWidth,
|
|
69
|
+
preferredContentHeight,
|
|
70
|
+
modalViewBackground,
|
|
71
|
+
cornerConfiguration,
|
|
72
|
+
sheet,
|
|
73
|
+
};
|
|
74
|
+
}, [
|
|
75
|
+
detents,
|
|
76
|
+
selectedDetentIdentifier,
|
|
77
|
+
largestUndimmedDetentIdentifier,
|
|
78
|
+
prefersGrabberVisible,
|
|
79
|
+
preferredCornerRadius,
|
|
80
|
+
prefersScrollingExpandsWhenScrolledToEdge,
|
|
81
|
+
prefersEdgeAttachedInCompactHeight,
|
|
82
|
+
widthFollowsPreferredContentSizeWhenEdgeAttached,
|
|
83
|
+
isModalInPresentation,
|
|
84
|
+
preferredContentWidth,
|
|
85
|
+
preferredContentHeight,
|
|
86
|
+
modalViewBackground,
|
|
87
|
+
cornerConfiguration,
|
|
88
|
+
]);
|
|
89
|
+
const clearRetryTimeout = useCallback(() => {
|
|
90
|
+
if (timeoutRef.current != null) {
|
|
91
|
+
clearTimeout(timeoutRef.current);
|
|
92
|
+
timeoutRef.current = null;
|
|
93
|
+
}
|
|
94
|
+
}, []);
|
|
95
|
+
const applyNativeConfigWithRetry = useCallback(() => {
|
|
96
|
+
if (Platform.OS !== 'ios' || !visible)
|
|
97
|
+
return;
|
|
98
|
+
const controller = getSheetModalController();
|
|
99
|
+
if (controller == null)
|
|
100
|
+
return;
|
|
101
|
+
clearRetryTimeout();
|
|
102
|
+
let attempts = 0;
|
|
103
|
+
const attempt = () => {
|
|
104
|
+
if (!isVisibleRef.current)
|
|
105
|
+
return;
|
|
106
|
+
const didApply = controller.applyPresentedModalConfig(nativeConfig);
|
|
107
|
+
attempts += 1;
|
|
108
|
+
if (!didApply && attempts < APPLY_RETRY_LIMIT) {
|
|
109
|
+
timeoutRef.current = setTimeout(attempt, APPLY_RETRY_DELAY_MS);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
attempt();
|
|
113
|
+
}, [clearRetryTimeout, nativeConfig, visible]);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
applyNativeConfigWithRetry();
|
|
116
|
+
}, [applyNativeConfigWithRetry]);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
return () => {
|
|
119
|
+
clearRetryTimeout();
|
|
120
|
+
};
|
|
121
|
+
}, [clearRetryTimeout]);
|
|
122
|
+
const onShow = useCallback((event) => {
|
|
123
|
+
applyNativeConfigWithRetry();
|
|
124
|
+
onShowProp?.(event);
|
|
125
|
+
}, [applyNativeConfigWithRetry, onShowProp]);
|
|
126
|
+
return (React.createElement(RNModal, { ...modalProps, allowSwipeDismissal: resolvedAllowSwipeDismissal, onShow: onShow, visible: visible }));
|
|
127
|
+
}
|
|
128
|
+
export default Modal;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { Modal, Modal as NativeSheetModal, applyPresentedModalConfig, dismissPresentedNativeModal, } from './NativeSheetModal';
|
|
2
|
+
export type { NativeSheetModalProps } from './NativeSheetModal';
|
|
3
|
+
export type { ModalCornerConfiguration, ModalCornerConfigurationType, ModalViewBackground, PresentedModalConfig, SheetDetentIdentifier, SheetPresentationConfig, } from './specs/SheetModalController.nitro';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Modal, Modal as NativeSheetModal, applyPresentedModalConfig, dismissPresentedNativeModal, } from './NativeSheetModal';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
+
export type SheetDetentIdentifier = 'medium' | 'large';
|
|
3
|
+
export type ModalViewBackground = 'clear' | 'systemBackground';
|
|
4
|
+
export type ModalCornerConfigurationType = 'none' | 'fixed' | 'containerConcentric' | 'capsule';
|
|
5
|
+
export interface ModalCornerConfiguration {
|
|
6
|
+
type: ModalCornerConfigurationType;
|
|
7
|
+
radius?: number;
|
|
8
|
+
minimumRadius?: number;
|
|
9
|
+
maximumRadius?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SheetPresentationConfig {
|
|
12
|
+
detents?: SheetDetentIdentifier[];
|
|
13
|
+
selectedDetentIdentifier?: SheetDetentIdentifier;
|
|
14
|
+
largestUndimmedDetentIdentifier?: SheetDetentIdentifier;
|
|
15
|
+
prefersGrabberVisible?: boolean;
|
|
16
|
+
preferredCornerRadius?: number;
|
|
17
|
+
prefersScrollingExpandsWhenScrolledToEdge?: boolean;
|
|
18
|
+
prefersEdgeAttachedInCompactHeight?: boolean;
|
|
19
|
+
widthFollowsPreferredContentSizeWhenEdgeAttached?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface PresentedModalConfig {
|
|
22
|
+
isModalInPresentation?: boolean;
|
|
23
|
+
preferredContentWidth?: number;
|
|
24
|
+
preferredContentHeight?: number;
|
|
25
|
+
modalViewBackground?: ModalViewBackground;
|
|
26
|
+
cornerConfiguration?: ModalCornerConfiguration;
|
|
27
|
+
sheet?: SheetPresentationConfig;
|
|
28
|
+
}
|
|
29
|
+
export interface SheetModalController extends HybridObject<{
|
|
30
|
+
ios: 'swift';
|
|
31
|
+
}> {
|
|
32
|
+
applyPresentedModalConfig(config: PresentedModalConfig): boolean;
|
|
33
|
+
dismissPresentedModal(animated: boolean): boolean;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/nitro.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://nitro.margelo.com/nitro.schema.json",
|
|
3
|
+
"cxxNamespace": [
|
|
4
|
+
"concentricsheet"
|
|
5
|
+
],
|
|
6
|
+
"ios": {
|
|
7
|
+
"iosModuleName": "NitroConcentricSheet"
|
|
8
|
+
},
|
|
9
|
+
"android": {
|
|
10
|
+
"androidNamespace": [
|
|
11
|
+
"concentricsheet"
|
|
12
|
+
],
|
|
13
|
+
"androidCxxLibName": "NitroConcentricSheet"
|
|
14
|
+
},
|
|
15
|
+
"autolinking": {
|
|
16
|
+
"SheetModalController": {
|
|
17
|
+
"swift": "SheetModalController"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"ignorePaths": [
|
|
21
|
+
"**/node_modules"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
** linguist-generated=true
|