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.
Files changed (41) hide show
  1. package/NitroConcentricSheet.podspec +31 -0
  2. package/README.md +95 -0
  3. package/ios/Bridge.h +8 -0
  4. package/ios/SheetModalController.swift +294 -0
  5. package/lib/NativeSheetModal.d.ts +22 -0
  6. package/lib/NativeSheetModal.js +128 -0
  7. package/lib/index.d.ts +3 -0
  8. package/lib/index.js +1 -0
  9. package/lib/specs/SheetModalController.nitro.d.ts +34 -0
  10. package/lib/specs/SheetModalController.nitro.js +1 -0
  11. package/nitro.json +23 -0
  12. package/nitrogen/generated/.gitattributes +1 -0
  13. package/nitrogen/generated/ios/NitroConcentricSheet+autolinking.rb +60 -0
  14. package/nitrogen/generated/ios/NitroConcentricSheet-Swift-Cxx-Bridge.cpp +33 -0
  15. package/nitrogen/generated/ios/NitroConcentricSheet-Swift-Cxx-Bridge.hpp +184 -0
  16. package/nitrogen/generated/ios/NitroConcentricSheet-Swift-Cxx-Umbrella.hpp +63 -0
  17. package/nitrogen/generated/ios/NitroConcentricSheetAutolinking.mm +33 -0
  18. package/nitrogen/generated/ios/NitroConcentricSheetAutolinking.swift +26 -0
  19. package/nitrogen/generated/ios/c++/HybridSheetModalControllerSpecSwift.cpp +11 -0
  20. package/nitrogen/generated/ios/c++/HybridSheetModalControllerSpecSwift.hpp +108 -0
  21. package/nitrogen/generated/ios/swift/HybridSheetModalControllerSpec.swift +56 -0
  22. package/nitrogen/generated/ios/swift/HybridSheetModalControllerSpec_cxx.swift +150 -0
  23. package/nitrogen/generated/ios/swift/ModalCornerConfiguration.swift +83 -0
  24. package/nitrogen/generated/ios/swift/ModalCornerConfigurationType.swift +48 -0
  25. package/nitrogen/generated/ios/swift/ModalViewBackground.swift +40 -0
  26. package/nitrogen/generated/ios/swift/PresentedModalConfig.swift +111 -0
  27. package/nitrogen/generated/ios/swift/SheetDetentIdentifier.swift +40 -0
  28. package/nitrogen/generated/ios/swift/SheetPresentationConfig.swift +160 -0
  29. package/nitrogen/generated/shared/c++/HybridSheetModalControllerSpec.cpp +22 -0
  30. package/nitrogen/generated/shared/c++/HybridSheetModalControllerSpec.hpp +64 -0
  31. package/nitrogen/generated/shared/c++/ModalCornerConfiguration.hpp +97 -0
  32. package/nitrogen/generated/shared/c++/ModalCornerConfigurationType.hpp +84 -0
  33. package/nitrogen/generated/shared/c++/ModalViewBackground.hpp +76 -0
  34. package/nitrogen/generated/shared/c++/PresentedModalConfig.hpp +111 -0
  35. package/nitrogen/generated/shared/c++/SheetDetentIdentifier.hpp +76 -0
  36. package/nitrogen/generated/shared/c++/SheetPresentationConfig.hpp +114 -0
  37. package/package.json +103 -0
  38. package/react-native.config.js +13 -0
  39. package/src/NativeSheetModal.tsx +204 -0
  40. package/src/index.ts +15 -0
  41. 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,8 @@
1
+ //
2
+ // Bridge.h
3
+ // NitroConcentricSheet
4
+ //
5
+ // Created by Marc Rousavy on 22.07.24.
6
+ //
7
+
8
+ #pragma once
@@ -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