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.
- package/CHANGELOG.md +66 -1
- package/README.md +38 -105
- package/android/build.gradle +28 -1
- package/android/src/main/java/com/unifiedactionsheet/ActionSheetOptions.kt +6 -2
- package/android/src/main/java/com/unifiedactionsheet/PromptOptions.kt +132 -0
- package/android/src/main/java/com/unifiedactionsheet/SheetContentBuilder.kt +31 -0
- package/android/src/main/java/com/unifiedactionsheet/SheetPresenters.kt +54 -0
- package/android/src/main/java/com/unifiedactionsheet/UnifiedActionSheetModule.kt +61 -0
- package/android/src/material/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
- package/android/src/material/java/com/unifiedactionsheet/MaterialBottomSheetPresenter.kt +90 -0
- package/android/src/material/res/values/unified_action_sheet_bottom_sheet.xml +22 -0
- package/android/src/noMaterial/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
- package/app.plugin.js +1 -0
- package/ios/BottomSheetViewController.swift +208 -0
- package/ios/UnifiedActionSheet.mm +73 -1
- package/ios/UnifiedActionSheetImpl.swift +248 -11
- package/jest/index.d.ts +15 -1
- package/jest/index.js +28 -0
- package/lib/commonjs/NativeUnifiedActionSheet.js.map +1 -1
- package/lib/commonjs/action-sheet-options.interface.js +4 -0
- package/lib/commonjs/common-options.interface.js +2 -0
- package/lib/commonjs/common-options.interface.js.map +1 -0
- package/lib/commonjs/index.js +57 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/prompt-options.interface.js +6 -0
- package/lib/commonjs/prompt-options.interface.js.map +1 -0
- package/lib/module/NativeUnifiedActionSheet.js.map +1 -1
- package/lib/module/action-sheet-options.interface.js +2 -0
- package/lib/module/common-options.interface.js +2 -0
- package/lib/module/common-options.interface.js.map +1 -0
- package/lib/module/index.js +55 -4
- package/lib/module/index.js.map +1 -1
- package/lib/module/prompt-options.interface.js +4 -0
- package/lib/module/prompt-options.interface.js.map +1 -0
- package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts +23 -0
- package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts +5 -14
- package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/common-options.interface.d.ts +17 -0
- package/lib/typescript/commonjs/src/common-options.interface.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +4 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/prompt-options.interface.d.ts +19 -0
- package/lib/typescript/commonjs/src/prompt-options.interface.d.ts.map +1 -0
- package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts +23 -0
- package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts.map +1 -1
- package/lib/typescript/module/src/action-sheet-options.interface.d.ts +5 -14
- package/lib/typescript/module/src/action-sheet-options.interface.d.ts.map +1 -1
- package/lib/typescript/module/src/common-options.interface.d.ts +17 -0
- package/lib/typescript/module/src/common-options.interface.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +4 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/prompt-options.interface.d.ts +19 -0
- package/lib/typescript/module/src/prompt-options.interface.d.ts.map +1 -0
- package/package.json +12 -2
- package/plugin/withUnifiedActionSheet.js +58 -0
- package/src/NativeUnifiedActionSheet.ts +22 -0
- package/src/action-sheet-options.interface.ts +10 -20
- package/src/common-options.interface.ts +18 -0
- package/src/index.tsx +100 -6
- 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(), ^{
|