react-native-unified-action-sheet 0.1.1 → 0.3.0

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 (61) hide show
  1. package/CHANGELOG.md +66 -1
  2. package/README.md +38 -105
  3. package/android/build.gradle +28 -1
  4. package/android/src/main/java/com/unifiedactionsheet/ActionSheetOptions.kt +6 -2
  5. package/android/src/main/java/com/unifiedactionsheet/PromptOptions.kt +132 -0
  6. package/android/src/main/java/com/unifiedactionsheet/SheetContentBuilder.kt +31 -0
  7. package/android/src/main/java/com/unifiedactionsheet/SheetPresenters.kt +54 -0
  8. package/android/src/main/java/com/unifiedactionsheet/UnifiedActionSheetModule.kt +61 -0
  9. package/android/src/material/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
  10. package/android/src/material/java/com/unifiedactionsheet/MaterialBottomSheetPresenter.kt +90 -0
  11. package/android/src/material/res/values/unified_action_sheet_bottom_sheet.xml +22 -0
  12. package/android/src/noMaterial/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
  13. package/app.plugin.js +1 -0
  14. package/ios/BottomSheetViewController.swift +208 -0
  15. package/ios/UnifiedActionSheet.mm +73 -1
  16. package/ios/UnifiedActionSheetImpl.swift +248 -11
  17. package/jest/index.d.ts +15 -1
  18. package/jest/index.js +28 -0
  19. package/lib/commonjs/NativeUnifiedActionSheet.js.map +1 -1
  20. package/lib/commonjs/action-sheet-options.interface.js +4 -0
  21. package/lib/commonjs/common-options.interface.js +2 -0
  22. package/lib/commonjs/common-options.interface.js.map +1 -0
  23. package/lib/commonjs/index.js +57 -5
  24. package/lib/commonjs/index.js.map +1 -1
  25. package/lib/commonjs/prompt-options.interface.js +6 -0
  26. package/lib/commonjs/prompt-options.interface.js.map +1 -0
  27. package/lib/module/NativeUnifiedActionSheet.js.map +1 -1
  28. package/lib/module/action-sheet-options.interface.js +2 -0
  29. package/lib/module/common-options.interface.js +2 -0
  30. package/lib/module/common-options.interface.js.map +1 -0
  31. package/lib/module/index.js +55 -4
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/prompt-options.interface.js +4 -0
  34. package/lib/module/prompt-options.interface.js.map +1 -0
  35. package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts +23 -0
  36. package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts +5 -14
  38. package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/src/common-options.interface.d.ts +17 -0
  40. package/lib/typescript/commonjs/src/common-options.interface.d.ts.map +1 -0
  41. package/lib/typescript/commonjs/src/index.d.ts +4 -0
  42. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/src/prompt-options.interface.d.ts +19 -0
  44. package/lib/typescript/commonjs/src/prompt-options.interface.d.ts.map +1 -0
  45. package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts +23 -0
  46. package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts.map +1 -1
  47. package/lib/typescript/module/src/action-sheet-options.interface.d.ts +5 -14
  48. package/lib/typescript/module/src/action-sheet-options.interface.d.ts.map +1 -1
  49. package/lib/typescript/module/src/common-options.interface.d.ts +17 -0
  50. package/lib/typescript/module/src/common-options.interface.d.ts.map +1 -0
  51. package/lib/typescript/module/src/index.d.ts +4 -0
  52. package/lib/typescript/module/src/index.d.ts.map +1 -1
  53. package/lib/typescript/module/src/prompt-options.interface.d.ts +19 -0
  54. package/lib/typescript/module/src/prompt-options.interface.d.ts.map +1 -0
  55. package/package.json +12 -2
  56. package/plugin/withUnifiedActionSheet.js +58 -0
  57. package/src/NativeUnifiedActionSheet.ts +22 -0
  58. package/src/action-sheet-options.interface.ts +10 -20
  59. package/src/common-options.interface.ts +18 -0
  60. package/src/index.tsx +100 -6
  61. package/src/prompt-options.interface.ts +27 -0
@@ -2,6 +2,7 @@ package com.unifiedactionsheet
2
2
 
3
3
  import android.app.Activity
4
4
  import android.app.Dialog
5
+ import com.facebook.react.bridge.Arguments
5
6
  import com.facebook.react.bridge.LifecycleEventListener
6
7
  import com.facebook.react.bridge.Promise
7
8
  import com.facebook.react.bridge.ReactApplicationContext
@@ -37,6 +38,22 @@ class UnifiedActionSheetModule(reactContext: ReactApplicationContext) :
37
38
  }
38
39
  }
39
40
 
41
+ override fun showPromptWithOptions(options: ReadableMap, promise: Promise) {
42
+ val parsed = PromptOptions.fromReadableMap(options)
43
+ val activity = reactApplicationContext.currentActivity
44
+ ?: return promise.reject(
45
+ "E_NO_ACTIVITY",
46
+ "No current activity to attach the prompt to.",
47
+ )
48
+
49
+ UiThreadUtil.runOnUiThread {
50
+ presentPrompt(activity, parsed, promise)
51
+ }
52
+ }
53
+
54
+ override fun getTypedExportedConstants(): MutableMap<String, Any> =
55
+ mutableMapOf("isMaterialEnabled" to BottomSheetPresenterFactory.isAvailable)
56
+
40
57
  override fun dismissActionSheet() {
41
58
  UiThreadUtil.runOnUiThread {
42
59
  openDialogs.lastOrNull()?.let { dialog ->
@@ -97,6 +114,9 @@ class UnifiedActionSheetModule(reactContext: ReactApplicationContext) :
97
114
  CenteredDialogPresenter
98
115
  }
99
116
  }
117
+ // Without Material compiled in there is no bottom sheet; JS has already
118
+ // warned in development, from the isMaterialEnabled constant.
119
+ PresentationStyle.BOTTOM -> BottomSheetPresenterFactory.create() ?: CenteredDialogPresenter
100
120
  }
101
121
 
102
122
  val dialog = presenter.build(activity, options) { presented, index ->
@@ -118,6 +138,47 @@ class UnifiedActionSheetModule(reactContext: ReactApplicationContext) :
118
138
  dialog.show()
119
139
  }
120
140
 
141
+ /// Mirrors presentSheet exactly, except the resolved value carries the text
142
+ /// as well as the index. The dialog joins the same openDialogs registry, so
143
+ /// dismissActionSheet()/dismissAllActionSheets() close prompts too.
144
+ private fun presentPrompt(
145
+ activity: Activity,
146
+ options: PromptOptions,
147
+ promise: Promise,
148
+ ) {
149
+ val resolved = AtomicBoolean(false)
150
+ val resolveOnce: (Int?, String) -> Unit = { index, text ->
151
+ if (resolved.compareAndSet(false, true)) {
152
+ promise.resolve(
153
+ Arguments.createMap().apply {
154
+ putInt("buttonIndex", index ?: -1)
155
+ putString("text", text)
156
+ },
157
+ )
158
+ }
159
+ }
160
+
161
+ val (dialog, currentText) = buildPromptDialog(activity, options) { presented, index, text ->
162
+ presented.dismiss()
163
+ resolveOnce(index, text)
164
+ }
165
+ openDialogs.add(dialog)
166
+
167
+ // Read the field at dismissal time: what the user typed survives a backdrop
168
+ // tap, so a caller can still recover a draft.
169
+ dialog.setOnCancelListener { resolveOnce(options.cancelButtonIndex, currentText()) }
170
+ dialog.setOnDismissListener {
171
+ openDialogs.remove(dialog)
172
+ if (dismissedByApi.remove(dialog)) {
173
+ resolveOnce(DISMISSED_BY_API, currentText())
174
+ } else {
175
+ resolveOnce(options.cancelButtonIndex, currentText())
176
+ }
177
+ }
178
+
179
+ dialog.show()
180
+ }
181
+
121
182
  companion object {
122
183
  const val NAME = NativeUnifiedActionSheetSpec.NAME
123
184
 
@@ -0,0 +1,10 @@
1
+ package com.unifiedactionsheet
2
+
3
+ /// Compiled only when the app sets unifiedActionSheet.material=true, which also
4
+ /// adds the Material dependency. The noMaterial source set defines the same
5
+ /// object, so src/main compiles against either.
6
+ internal object BottomSheetPresenterFactory {
7
+ const val isAvailable = true
8
+
9
+ fun create(): SheetPresenter? = MaterialBottomSheetPresenter
10
+ }
@@ -0,0 +1,90 @@
1
+ package com.unifiedactionsheet
2
+
3
+ import android.app.Activity
4
+ import android.app.Dialog
5
+ import android.content.res.ColorStateList
6
+ import android.view.View
7
+ import android.widget.LinearLayout
8
+ import androidx.core.view.ViewCompat
9
+ import androidx.core.view.WindowCompat
10
+ import androidx.core.view.WindowInsetsCompat
11
+ import com.google.android.material.bottomsheet.BottomSheetDialog
12
+ import com.google.android.material.bottomsheet.BottomSheetDragHandleView
13
+ import com.google.android.material.R as MaterialR
14
+
15
+ /// presentationStyle 'bottom': a Material 3 modal bottom sheet. Material's own
16
+ /// behavior does the rest: a long list opens at the peek height and drags up to
17
+ /// expand, a short one opens fully, and swiping it away cancels the dialog, so
18
+ /// it resolves through the same cancel path as a backdrop tap.
19
+ internal object MaterialBottomSheetPresenter : SheetPresenter {
20
+ override fun build(
21
+ activity: Activity,
22
+ options: ActionSheetOptions,
23
+ onSelect: (Dialog, Int) -> Unit,
24
+ ): Dialog {
25
+ val isDark = isDarkAppearance(activity, options.userInterfaceStyle)
26
+ val palette = paletteFor(isDark)
27
+ // A full Material 3 theme rather than an overlay, so the sheet works inside
28
+ // an app whose own theme is plain AppCompat.
29
+ val dialog = BottomSheetDialog(activity, bottomSheetTheme(isDark))
30
+
31
+ val context = dialog.context
32
+ // The cancel row stays the last row, as on the centered dialog. With a long
33
+ // list it starts below the peek height, which Material users expect:
34
+ // swipe down, back and a backdrop tap all resolve the cancel index anyway.
35
+ val content = buildContent(context, options, palette) { index -> onSelect(dialog, index) }
36
+
37
+ val root = LinearLayout(context).apply {
38
+ orientation = LinearLayout.VERTICAL
39
+ addView(
40
+ BottomSheetDragHandleView(context),
41
+ LinearLayout.LayoutParams(
42
+ LinearLayout.LayoutParams.MATCH_PARENT,
43
+ LinearLayout.LayoutParams.WRAP_CONTENT,
44
+ ),
45
+ )
46
+ addView(
47
+ content,
48
+ LinearLayout.LayoutParams(
49
+ LinearLayout.LayoutParams.MATCH_PARENT,
50
+ LinearLayout.LayoutParams.WRAP_CONTENT,
51
+ ),
52
+ )
53
+ }
54
+ dialog.setContentView(root)
55
+ dialog.setCanceledOnTouchOutside(true)
56
+
57
+ // Tint Material's own sheet background instead of replacing it, so its
58
+ // rounded top corners and expansion animation stay intact, while the color
59
+ // matches the palette the centered and anchored styles use.
60
+ dialog.findViewById<View>(MaterialR.id.design_bottom_sheet)?.let { sheet ->
61
+ ViewCompat.setBackgroundTintList(sheet, ColorStateList.valueOf(palette.surface))
62
+ }
63
+
64
+ // The sheet now sits behind the navigation bar, so its buttons or gesture
65
+ // handle need to contrast with the sheet, not with the app underneath.
66
+ dialog.window?.let { window ->
67
+ WindowCompat.getInsetsController(window, window.decorView)
68
+ .isAppearanceLightNavigationBars = !isDark
69
+ }
70
+
71
+ // Edge to edge is for the bottom only. It would also let a long list
72
+ // expand behind the status bar, so cap the sheet just below it (and any
73
+ // display cutout); the rows scroll inside it instead.
74
+ val decor = activity.window.decorView
75
+ val topInset = ViewCompat.getRootWindowInsets(decor)
76
+ ?.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
77
+ ?.top ?: 0
78
+ if (decor.height > topInset) {
79
+ dialog.behavior.maxHeight = decor.height - topInset
80
+ }
81
+
82
+ return dialog
83
+ }
84
+
85
+ private fun bottomSheetTheme(isDark: Boolean): Int = if (isDark) {
86
+ R.style.UnifiedActionSheet_BottomSheetDialog_Dark
87
+ } else {
88
+ R.style.UnifiedActionSheet_BottomSheetDialog_Light
89
+ }
90
+ }
@@ -0,0 +1,22 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <!-- Full Material 3 themes, not overlays: Material components refuse to
4
+ inflate under a non-Material theme, and the host app's may be plain
5
+ AppCompat. userInterfaceStyle picks light or dark explicitly, so neither
6
+ follows DayNight.
7
+
8
+ Edge to edge, so the sheet runs down behind the navigation bar instead
9
+ of stopping above it and leaving a strip of scrim there. Material only
10
+ does this when enableEdgeToEdge is set and the navigation bar is
11
+ translucent; the sheet then pads its content by the bar's inset, so the
12
+ last row stays clear of it. -->
13
+ <style name="UnifiedActionSheet.BottomSheetDialog.Light" parent="Theme.Material3.Light.BottomSheetDialog">
14
+ <item name="enableEdgeToEdge">true</item>
15
+ <item name="android:navigationBarColor">@android:color/transparent</item>
16
+ </style>
17
+
18
+ <style name="UnifiedActionSheet.BottomSheetDialog.Dark" parent="Theme.Material3.Dark.BottomSheetDialog">
19
+ <item name="enableEdgeToEdge">true</item>
20
+ <item name="android:navigationBarColor">@android:color/transparent</item>
21
+ </style>
22
+ </resources>
@@ -0,0 +1,10 @@
1
+ package com.unifiedactionsheet
2
+
3
+ /// Compiled when the app has not set unifiedActionSheet.material=true. There is
4
+ /// no bottom sheet without Material, so the module falls back to the centered
5
+ /// dialog, and JS warns in development (see getTypedExportedConstants).
6
+ internal object BottomSheetPresenterFactory {
7
+ const val isAvailable = false
8
+
9
+ fun create(): SheetPresenter? = null
10
+ }
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./plugin/withUnifiedActionSheet');
@@ -0,0 +1,208 @@
1
+ import UIKit
2
+
3
+ /// presentationStyle 'bottom' on iPhone: the rows of an action sheet inside a
4
+ /// UISheetPresentationController, which UIAlertController cannot be. Styled
5
+ /// like the rest of iOS rather than like the Android sheet: system fonts, the
6
+ /// label color (or tintColor), system red for destructive rows.
7
+ final class BottomSheetViewController: UIViewController {
8
+ struct Row {
9
+ let index: Int
10
+ let label: String
11
+ let isDestructive: Bool
12
+ let isEnabled: Bool
13
+ }
14
+
15
+ private let sheetTitle: String?
16
+ private let message: String?
17
+ private let rows: [Row]
18
+ private let cancelRow: Row?
19
+ private let tintColor: UIColor?
20
+ private let cancelButtonTintColor: UIColor?
21
+ private let destructiveColor: UIColor?
22
+ /// Set by the presenter once it can resolve the sheet.
23
+ var onSelect: ((Int) -> Void)?
24
+
25
+ private let scrollView = UIScrollView()
26
+ private let stack = UIStackView()
27
+
28
+ init(
29
+ title: String?,
30
+ message: String?,
31
+ rows: [Row],
32
+ cancelRow: Row?,
33
+ tintColor: UIColor?,
34
+ cancelButtonTintColor: UIColor?,
35
+ destructiveColor: UIColor?
36
+ ) {
37
+ self.sheetTitle = title
38
+ self.message = message
39
+ self.rows = rows
40
+ self.cancelRow = cancelRow
41
+ self.tintColor = tintColor
42
+ self.cancelButtonTintColor = cancelButtonTintColor
43
+ self.destructiveColor = destructiveColor
44
+ super.init(nibName: nil, bundle: nil)
45
+ }
46
+
47
+ @available(*, unavailable)
48
+ required init?(coder: NSCoder) {
49
+ fatalError("init(coder:) is not supported")
50
+ }
51
+
52
+ override func viewDidLoad() {
53
+ super.viewDidLoad()
54
+
55
+ // From iOS 26 the system draws the sheet's Liquid Glass background; an
56
+ // opaque color here would cover it.
57
+ if #available(iOS 26.0, *) {
58
+ view.backgroundColor = .clear
59
+ } else {
60
+ view.backgroundColor = .systemBackground
61
+ }
62
+
63
+ stack.axis = .vertical
64
+ stack.translatesAutoresizingMaskIntoConstraints = false
65
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
66
+ scrollView.alwaysBounceVertical = false
67
+ scrollView.addSubview(stack)
68
+ view.addSubview(scrollView)
69
+
70
+ NSLayoutConstraint.activate([
71
+ scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
72
+ scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
73
+ scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
74
+ scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
75
+ stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
76
+ stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
77
+ stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
78
+ stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
79
+ stack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
80
+ ])
81
+
82
+ // The last row stays clear of the home indicator once scrolled to it.
83
+ scrollView.contentInsetAdjustmentBehavior = .always
84
+
85
+ buildContent()
86
+ }
87
+
88
+ /// The height the content wants at the given width, for the fitted detent.
89
+ func fittingHeight(width: CGFloat) -> CGFloat {
90
+ loadViewIfNeeded()
91
+
92
+ return stack.systemLayoutSizeFitting(
93
+ CGSize(width: width, height: UIView.layoutFittingCompressedSize.height),
94
+ withHorizontalFittingPriority: .required,
95
+ verticalFittingPriority: .fittingSizeLevel
96
+ ).height
97
+ }
98
+
99
+ private func buildContent() {
100
+ // Room for the grabber, which the sheet draws over the top of the view.
101
+ stack.addArrangedSubview(spacer(height: 20))
102
+
103
+ if let sheetTitle {
104
+ stack.addArrangedSubview(header(sheetTitle, style: .subheadline, weight: .semibold))
105
+ }
106
+ if let message {
107
+ stack.addArrangedSubview(header(message, style: .footnote, weight: .regular))
108
+ }
109
+ if sheetTitle != nil || message != nil {
110
+ stack.addArrangedSubview(spacer(height: 8))
111
+ }
112
+
113
+ for row in rows {
114
+ stack.addArrangedSubview(button(for: row, isCancel: false))
115
+ }
116
+
117
+ if let cancelRow {
118
+ let separator = UIView()
119
+ separator.backgroundColor = .separator
120
+ separator.heightAnchor.constraint(equalToConstant: 1 / traitCollection.displayScale).isActive = true
121
+ stack.addArrangedSubview(spacer(height: 8))
122
+ stack.addArrangedSubview(separator)
123
+ stack.addArrangedSubview(button(for: cancelRow, isCancel: true))
124
+ }
125
+
126
+ stack.addArrangedSubview(spacer(height: 8))
127
+ }
128
+
129
+ private func header(_ text: String, style: UIFont.TextStyle, weight: UIFont.Weight) -> UIView {
130
+ let label = UILabel()
131
+ label.text = text
132
+ label.numberOfLines = 0
133
+ label.textAlignment = .center
134
+ label.textColor = .secondaryLabel
135
+ label.adjustsFontForContentSizeCategory = true
136
+ label.font = UIFontMetrics(forTextStyle: style).scaledFont(
137
+ for: .systemFont(ofSize: UIFont.preferredFont(forTextStyle: style).pointSize, weight: weight)
138
+ )
139
+ if weight == .semibold {
140
+ label.accessibilityTraits.insert(.header)
141
+ }
142
+
143
+ let container = UIView()
144
+ label.translatesAutoresizingMaskIntoConstraints = false
145
+ container.addSubview(label)
146
+ NSLayoutConstraint.activate([
147
+ label.topAnchor.constraint(equalTo: container.topAnchor, constant: 4),
148
+ label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -4),
149
+ label.leadingAnchor.constraint(equalTo: container.layoutMarginsGuide.leadingAnchor),
150
+ label.trailingAnchor.constraint(equalTo: container.layoutMarginsGuide.trailingAnchor),
151
+ ])
152
+ container.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)
153
+
154
+ return container
155
+ }
156
+
157
+ private func button(for row: Row, isCancel: Bool) -> UIButton {
158
+ // Precedence matches the alert: destructive > cancel tint > tint > default.
159
+ let color: UIColor =
160
+ row.isDestructive
161
+ ? (destructiveColor ?? .systemRed)
162
+ : (isCancel ? (cancelButtonTintColor ?? tintColor ?? .label) : (tintColor ?? .label))
163
+
164
+ var config = UIButton.Configuration.plain()
165
+ config.title = row.label
166
+ config.baseForegroundColor = color
167
+ config.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20)
168
+ config.titleAlignment = .leading
169
+ config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
170
+ var attributes = attributes
171
+ attributes.font = UIFont.preferredFont(forTextStyle: .body).withWeight(isCancel ? .semibold : .regular)
172
+
173
+ return attributes
174
+ }
175
+ config.background.cornerRadius = 0
176
+
177
+ let button = UIButton(configuration: config)
178
+ button.contentHorizontalAlignment = .leading
179
+ button.isEnabled = row.isEnabled
180
+ // A full-width row highlight, as in a list, rather than a dimmed title.
181
+ button.configurationUpdateHandler = { button in
182
+ var updated = button.configuration
183
+ updated?.background.backgroundColor = button.isHighlighted ? .systemFill : .clear
184
+ updated?.baseForegroundColor = button.isEnabled ? color : .tertiaryLabel
185
+ button.configuration = updated
186
+ }
187
+ button.addAction(UIAction { [weak self] _ in self?.onSelect?(row.index) }, for: .touchUpInside)
188
+
189
+ return button
190
+ }
191
+
192
+ private func spacer(height: CGFloat) -> UIView {
193
+ let view = UIView()
194
+ view.heightAnchor.constraint(equalToConstant: height).isActive = true
195
+
196
+ return view
197
+ }
198
+ }
199
+
200
+ private extension UIFont {
201
+ func withWeight(_ weight: UIFont.Weight) -> UIFont {
202
+ let descriptor = fontDescriptor.addingAttributes([
203
+ .traits: [UIFontDescriptor.TraitKey.weight: weight],
204
+ ])
205
+
206
+ return UIFont(descriptor: descriptor, size: pointSize)
207
+ }
208
+ }
@@ -1,11 +1,12 @@
1
1
  #import "UnifiedActionSheet.h"
2
2
 
3
+ #import <React/RCTInvalidating.h>
3
4
  #import <RCTTypeSafety/RCTConvertHelpers.h>
4
5
  #import <UnifiedActionSheetSpec/UnifiedActionSheetSpec.h>
5
6
 
6
7
  #import "react_native_unified_action_sheet-Swift.h"
7
8
 
8
- @interface UnifiedActionSheet () <NativeUnifiedActionSheetSpec>
9
+ @interface UnifiedActionSheet () <NativeUnifiedActionSheetSpec, RCTInvalidating>
9
10
  @end
10
11
 
11
12
  @implementation UnifiedActionSheet
@@ -17,6 +18,20 @@ RCT_EXPORT_MODULE()
17
18
  return NO;
18
19
  }
19
20
 
21
+ /// Material is an Android opt-in; iOS presents 'bottom' as the standard action
22
+ /// sheet with nothing to enable.
23
+ - (facebook::react::ModuleConstants<JS::NativeUnifiedActionSheet::Constants::Builder>)constantsToExport
24
+ {
25
+ return [self getConstants];
26
+ }
27
+
28
+ - (facebook::react::ModuleConstants<JS::NativeUnifiedActionSheet::Constants::Builder>)getConstants
29
+ {
30
+ return facebook::react::typedConstants<JS::NativeUnifiedActionSheet::Constants::Builder>({
31
+ .isMaterialEnabled = false,
32
+ });
33
+ }
34
+
20
35
  /// Only the keys the iOS presentation understands are forwarded; the
21
36
  /// Android-only keys in the shared spec are ignored here.
22
37
  RCT_EXPORT_METHOD(showActionSheetWithOptions
@@ -73,6 +88,63 @@ RCT_EXPORT_METHOD(showActionSheetWithOptions
73
88
  });
74
89
  }
75
90
 
91
+ RCT_EXPORT_METHOD(showPromptWithOptions
92
+ : (JS::NativeUnifiedActionSheet::SpecShowPromptWithOptionsOptions &)options resolve
93
+ : (RCTPromiseResolveBlock)resolve reject
94
+ : (RCTPromiseRejectBlock)reject)
95
+ {
96
+ NSMutableDictionary *payload = [NSMutableDictionary new];
97
+
98
+ payload[@"options"] = RCTConvertVecToArray(options.options(), ^id(NSString *element) {
99
+ return element;
100
+ });
101
+
102
+ if (options.cancelButtonIndex()) {
103
+ payload[@"cancelButtonIndex"] = @(*options.cancelButtonIndex());
104
+ }
105
+ if (options.destructiveButtonIndices()) {
106
+ payload[@"destructiveButtonIndices"] =
107
+ RCTConvertVecToArray(*options.destructiveButtonIndices(), ^id(double element) {
108
+ return @(element);
109
+ });
110
+ }
111
+ if (options.disabledButtonIndices()) {
112
+ payload[@"disabledButtonIndices"] = RCTConvertVecToArray(*options.disabledButtonIndices(), ^id(double element) {
113
+ return @(element);
114
+ });
115
+ }
116
+ if (options.secureTextEntry()) {
117
+ payload[@"secureTextEntry"] = @(*options.secureTextEntry());
118
+ }
119
+
120
+ payload[@"title"] = options.title();
121
+ payload[@"message"] = options.message();
122
+ payload[@"placeholder"] = options.placeholder();
123
+ payload[@"defaultValue"] = options.defaultValue();
124
+ payload[@"keyboardType"] = options.keyboardType();
125
+ payload[@"tintColor"] = options.tintColor();
126
+ payload[@"cancelButtonTintColor"] = options.cancelButtonTintColor();
127
+ payload[@"destructiveColor"] = options.destructiveColor();
128
+ payload[@"userInterfaceStyle"] = options.userInterfaceStyle();
129
+
130
+ dispatch_async(dispatch_get_main_queue(), ^{
131
+ [UnifiedActionSheetImpl.shared showPromptWithOptions:payload
132
+ completion:^(NSInteger buttonIndex, NSString *text) {
133
+ resolve(@{@"buttonIndex" : @(buttonIndex), @"text" : text});
134
+ }];
135
+ });
136
+ }
137
+
138
+ /// A reload (or any teardown of the JS runtime) replaces the app's root view,
139
+ /// but the sheets are presented over it and would otherwise stay on screen.
140
+ /// Android closes its dialogs in its own invalidate().
141
+ - (void)invalidate
142
+ {
143
+ dispatch_async(dispatch_get_main_queue(), ^{
144
+ [UnifiedActionSheetImpl.shared invalidate];
145
+ });
146
+ }
147
+
76
148
  RCT_EXPORT_METHOD(dismissActionSheet)
77
149
  {
78
150
  dispatch_async(dispatch_get_main_queue(), ^{