react-native-unified-action-sheet 0.2.0 → 0.4.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 +87 -1
- package/README.md +44 -112
- package/android/build.gradle +28 -1
- package/android/src/main/java/com/unifiedactionsheet/ActionSheetOptions.kt +41 -2
- package/android/src/main/java/com/unifiedactionsheet/PromptOptions.kt +27 -5
- package/android/src/main/java/com/unifiedactionsheet/SheetContentBuilder.kt +75 -9
- package/android/src/main/java/com/unifiedactionsheet/SheetPresenters.kt +61 -10
- package/android/src/main/java/com/unifiedactionsheet/UnifiedActionSheetModule.kt +14 -7
- package/android/src/material/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
- package/android/src/material/java/com/unifiedactionsheet/MaterialBottomSheetPresenter.kt +153 -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 +210 -0
- package/ios/UnifiedActionSheet.mm +51 -5
- package/ios/UnifiedActionSheetImpl.swift +259 -22
- package/jest/index.d.ts +12 -9
- package/jest/index.js +37 -18
- package/lib/commonjs/NativeUnifiedActionSheet.js.map +1 -1
- package/lib/commonjs/index.js +102 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/NativeUnifiedActionSheet.js.map +1 -1
- package/lib/module/index.js +102 -18
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts +10 -1
- package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts +5 -2
- package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/common-options.interface.d.ts +9 -0
- package/lib/typescript/commonjs/src/common-options.interface.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +6 -6
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/prompt-options.interface.d.ts +10 -6
- package/lib/typescript/commonjs/src/prompt-options.interface.d.ts.map +1 -1
- package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts +10 -1
- package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts.map +1 -1
- package/lib/typescript/module/src/action-sheet-options.interface.d.ts +5 -2
- package/lib/typescript/module/src/action-sheet-options.interface.d.ts.map +1 -1
- package/lib/typescript/module/src/common-options.interface.d.ts +9 -0
- package/lib/typescript/module/src/common-options.interface.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +6 -6
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/prompt-options.interface.d.ts +10 -6
- package/lib/typescript/module/src/prompt-options.interface.d.ts.map +1 -1
- package/package.json +12 -2
- package/plugin/withUnifiedActionSheet.js +58 -0
- package/src/NativeUnifiedActionSheet.ts +12 -2
- package/src/action-sheet-options.interface.ts +12 -1
- package/src/common-options.interface.ts +18 -0
- package/src/index.tsx +147 -42
- package/src/prompt-options.interface.ts +21 -5
|
@@ -0,0 +1,210 @@
|
|
|
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
|
+
/// Semibold, like the preferred action of an alert.
|
|
14
|
+
let isPreferred: Bool
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private let sheetTitle: String?
|
|
18
|
+
private let message: String?
|
|
19
|
+
private let rows: [Row]
|
|
20
|
+
private let cancelRow: Row?
|
|
21
|
+
private let tintColor: UIColor?
|
|
22
|
+
private let cancelButtonTintColor: UIColor?
|
|
23
|
+
private let destructiveColor: UIColor?
|
|
24
|
+
/// Set by the presenter once it can resolve the sheet.
|
|
25
|
+
var onSelect: ((Int) -> Void)?
|
|
26
|
+
|
|
27
|
+
private let scrollView = UIScrollView()
|
|
28
|
+
private let stack = UIStackView()
|
|
29
|
+
|
|
30
|
+
init(
|
|
31
|
+
title: String?,
|
|
32
|
+
message: String?,
|
|
33
|
+
rows: [Row],
|
|
34
|
+
cancelRow: Row?,
|
|
35
|
+
tintColor: UIColor?,
|
|
36
|
+
cancelButtonTintColor: UIColor?,
|
|
37
|
+
destructiveColor: UIColor?
|
|
38
|
+
) {
|
|
39
|
+
self.sheetTitle = title
|
|
40
|
+
self.message = message
|
|
41
|
+
self.rows = rows
|
|
42
|
+
self.cancelRow = cancelRow
|
|
43
|
+
self.tintColor = tintColor
|
|
44
|
+
self.cancelButtonTintColor = cancelButtonTintColor
|
|
45
|
+
self.destructiveColor = destructiveColor
|
|
46
|
+
super.init(nibName: nil, bundle: nil)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@available(*, unavailable)
|
|
50
|
+
required init?(coder: NSCoder) {
|
|
51
|
+
fatalError("init(coder:) is not supported")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override func viewDidLoad() {
|
|
55
|
+
super.viewDidLoad()
|
|
56
|
+
|
|
57
|
+
// From iOS 26 the system draws the sheet's Liquid Glass background; an
|
|
58
|
+
// opaque color here would cover it.
|
|
59
|
+
if #available(iOS 26.0, *) {
|
|
60
|
+
view.backgroundColor = .clear
|
|
61
|
+
} else {
|
|
62
|
+
view.backgroundColor = .systemBackground
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
stack.axis = .vertical
|
|
66
|
+
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
67
|
+
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
68
|
+
scrollView.alwaysBounceVertical = false
|
|
69
|
+
scrollView.addSubview(stack)
|
|
70
|
+
view.addSubview(scrollView)
|
|
71
|
+
|
|
72
|
+
NSLayoutConstraint.activate([
|
|
73
|
+
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
|
74
|
+
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
75
|
+
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
76
|
+
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
77
|
+
stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
|
|
78
|
+
stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
|
|
79
|
+
stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
|
|
80
|
+
stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
|
|
81
|
+
stack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
// The last row stays clear of the home indicator once scrolled to it.
|
|
85
|
+
scrollView.contentInsetAdjustmentBehavior = .always
|
|
86
|
+
|
|
87
|
+
buildContent()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// The height the content wants at the given width, for the fitted detent.
|
|
91
|
+
func fittingHeight(width: CGFloat) -> CGFloat {
|
|
92
|
+
loadViewIfNeeded()
|
|
93
|
+
|
|
94
|
+
return stack.systemLayoutSizeFitting(
|
|
95
|
+
CGSize(width: width, height: UIView.layoutFittingCompressedSize.height),
|
|
96
|
+
withHorizontalFittingPriority: .required,
|
|
97
|
+
verticalFittingPriority: .fittingSizeLevel
|
|
98
|
+
).height
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private func buildContent() {
|
|
102
|
+
// Room for the grabber, which the sheet draws over the top of the view.
|
|
103
|
+
stack.addArrangedSubview(spacer(height: 20))
|
|
104
|
+
|
|
105
|
+
if let sheetTitle {
|
|
106
|
+
stack.addArrangedSubview(header(sheetTitle, style: .subheadline, weight: .semibold))
|
|
107
|
+
}
|
|
108
|
+
if let message {
|
|
109
|
+
stack.addArrangedSubview(header(message, style: .footnote, weight: .regular))
|
|
110
|
+
}
|
|
111
|
+
if sheetTitle != nil || message != nil {
|
|
112
|
+
stack.addArrangedSubview(spacer(height: 8))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for row in rows {
|
|
116
|
+
stack.addArrangedSubview(button(for: row, isCancel: false))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if let cancelRow {
|
|
120
|
+
let separator = UIView()
|
|
121
|
+
separator.backgroundColor = .separator
|
|
122
|
+
separator.heightAnchor.constraint(equalToConstant: 1 / traitCollection.displayScale).isActive = true
|
|
123
|
+
stack.addArrangedSubview(spacer(height: 8))
|
|
124
|
+
stack.addArrangedSubview(separator)
|
|
125
|
+
stack.addArrangedSubview(button(for: cancelRow, isCancel: true))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
stack.addArrangedSubview(spacer(height: 8))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private func header(_ text: String, style: UIFont.TextStyle, weight: UIFont.Weight) -> UIView {
|
|
132
|
+
let label = UILabel()
|
|
133
|
+
label.text = text
|
|
134
|
+
label.numberOfLines = 0
|
|
135
|
+
label.textAlignment = .center
|
|
136
|
+
label.textColor = .secondaryLabel
|
|
137
|
+
label.adjustsFontForContentSizeCategory = true
|
|
138
|
+
label.font = UIFontMetrics(forTextStyle: style).scaledFont(
|
|
139
|
+
for: .systemFont(ofSize: UIFont.preferredFont(forTextStyle: style).pointSize, weight: weight)
|
|
140
|
+
)
|
|
141
|
+
if weight == .semibold {
|
|
142
|
+
label.accessibilityTraits.insert(.header)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let container = UIView()
|
|
146
|
+
label.translatesAutoresizingMaskIntoConstraints = false
|
|
147
|
+
container.addSubview(label)
|
|
148
|
+
NSLayoutConstraint.activate([
|
|
149
|
+
label.topAnchor.constraint(equalTo: container.topAnchor, constant: 4),
|
|
150
|
+
label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -4),
|
|
151
|
+
label.leadingAnchor.constraint(equalTo: container.layoutMarginsGuide.leadingAnchor),
|
|
152
|
+
label.trailingAnchor.constraint(equalTo: container.layoutMarginsGuide.trailingAnchor),
|
|
153
|
+
])
|
|
154
|
+
container.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)
|
|
155
|
+
|
|
156
|
+
return container
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private func button(for row: Row, isCancel: Bool) -> UIButton {
|
|
160
|
+
// Precedence matches the alert: destructive > cancel tint > tint > default.
|
|
161
|
+
let color: UIColor =
|
|
162
|
+
row.isDestructive
|
|
163
|
+
? (destructiveColor ?? .systemRed)
|
|
164
|
+
: (isCancel ? (cancelButtonTintColor ?? tintColor ?? .label) : (tintColor ?? .label))
|
|
165
|
+
|
|
166
|
+
var config = UIButton.Configuration.plain()
|
|
167
|
+
config.title = row.label
|
|
168
|
+
config.baseForegroundColor = color
|
|
169
|
+
config.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20)
|
|
170
|
+
config.titleAlignment = .leading
|
|
171
|
+
config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
|
172
|
+
var attributes = attributes
|
|
173
|
+
attributes.font = UIFont.preferredFont(forTextStyle: .body).withWeight(isCancel || row.isPreferred ? .semibold : .regular)
|
|
174
|
+
|
|
175
|
+
return attributes
|
|
176
|
+
}
|
|
177
|
+
config.background.cornerRadius = 0
|
|
178
|
+
|
|
179
|
+
let button = UIButton(configuration: config)
|
|
180
|
+
button.contentHorizontalAlignment = .leading
|
|
181
|
+
button.isEnabled = row.isEnabled
|
|
182
|
+
// A full-width row highlight, as in a list, rather than a dimmed title.
|
|
183
|
+
button.configurationUpdateHandler = { button in
|
|
184
|
+
var updated = button.configuration
|
|
185
|
+
updated?.background.backgroundColor = button.isHighlighted ? .systemFill : .clear
|
|
186
|
+
updated?.baseForegroundColor = button.isEnabled ? color : .tertiaryLabel
|
|
187
|
+
button.configuration = updated
|
|
188
|
+
}
|
|
189
|
+
button.addAction(UIAction { [weak self] _ in self?.onSelect?(row.index) }, for: .touchUpInside)
|
|
190
|
+
|
|
191
|
+
return button
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private func spacer(height: CGFloat) -> UIView {
|
|
195
|
+
let view = UIView()
|
|
196
|
+
view.heightAnchor.constraint(equalToConstant: height).isActive = true
|
|
197
|
+
|
|
198
|
+
return view
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private extension UIFont {
|
|
203
|
+
func withWeight(_ weight: UIFont.Weight) -> UIFont {
|
|
204
|
+
let descriptor = fontDescriptor.addingAttributes([
|
|
205
|
+
.traits: [UIFontDescriptor.TraitKey.weight: weight],
|
|
206
|
+
])
|
|
207
|
+
|
|
208
|
+
return UIFont(descriptor: descriptor, size: pointSize)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -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
|
|
@@ -53,6 +68,15 @@ RCT_EXPORT_METHOD(showActionSheetWithOptions
|
|
|
53
68
|
payload[@"destructiveColor"] = options.destructiveColor();
|
|
54
69
|
payload[@"presentationStyle"] = options.presentationStyle();
|
|
55
70
|
|
|
71
|
+
if (options.preferredButtonIndex()) {
|
|
72
|
+
payload[@"preferredButtonIndex"] = @(*options.preferredButtonIndex());
|
|
73
|
+
}
|
|
74
|
+
if (options.detents()) {
|
|
75
|
+
payload[@"detents"] = RCTConvertVecToArray(*options.detents(), ^id(NSString *element) {
|
|
76
|
+
return element;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
56
80
|
// The anchor arrives already measured from the ref on the JS side, so this
|
|
57
81
|
// module never resolves a view and needs no React Native view API.
|
|
58
82
|
auto anchorRect = options.anchorRect();
|
|
@@ -98,13 +122,21 @@ RCT_EXPORT_METHOD(showPromptWithOptions
|
|
|
98
122
|
return @(element);
|
|
99
123
|
});
|
|
100
124
|
}
|
|
101
|
-
if (options.
|
|
102
|
-
payload[@"
|
|
125
|
+
if (options.preferredButtonIndex()) {
|
|
126
|
+
payload[@"preferredButtonIndex"] = @(*options.preferredButtonIndex());
|
|
127
|
+
}
|
|
128
|
+
if (options.textRequiredButtonIndices()) {
|
|
129
|
+
payload[@"textRequiredButtonIndices"] =
|
|
130
|
+
RCTConvertVecToArray(*options.textRequiredButtonIndices(), ^id(double element) {
|
|
131
|
+
return @(element);
|
|
132
|
+
});
|
|
103
133
|
}
|
|
104
134
|
|
|
105
135
|
payload[@"title"] = options.title();
|
|
106
136
|
payload[@"message"] = options.message();
|
|
137
|
+
payload[@"type"] = options.type();
|
|
107
138
|
payload[@"placeholder"] = options.placeholder();
|
|
139
|
+
payload[@"passwordPlaceholder"] = options.passwordPlaceholder();
|
|
108
140
|
payload[@"defaultValue"] = options.defaultValue();
|
|
109
141
|
payload[@"keyboardType"] = options.keyboardType();
|
|
110
142
|
payload[@"tintColor"] = options.tintColor();
|
|
@@ -114,12 +146,26 @@ RCT_EXPORT_METHOD(showPromptWithOptions
|
|
|
114
146
|
|
|
115
147
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
116
148
|
[UnifiedActionSheetImpl.shared showPromptWithOptions:payload
|
|
117
|
-
completion:^(NSInteger buttonIndex, NSString *text) {
|
|
118
|
-
resolve(@{
|
|
149
|
+
completion:^(NSInteger buttonIndex, NSString *text, NSString *password) {
|
|
150
|
+
resolve(@{
|
|
151
|
+
@"buttonIndex" : @(buttonIndex),
|
|
152
|
+
@"text" : text,
|
|
153
|
+
@"password" : password,
|
|
154
|
+
});
|
|
119
155
|
}];
|
|
120
156
|
});
|
|
121
157
|
}
|
|
122
158
|
|
|
159
|
+
/// A reload (or any teardown of the JS runtime) replaces the app's root view,
|
|
160
|
+
/// but the sheets are presented over it and would otherwise stay on screen.
|
|
161
|
+
/// Android closes its dialogs in its own invalidate().
|
|
162
|
+
- (void)invalidate
|
|
163
|
+
{
|
|
164
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
165
|
+
[UnifiedActionSheetImpl.shared invalidate];
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
123
169
|
RCT_EXPORT_METHOD(dismissActionSheet)
|
|
124
170
|
{
|
|
125
171
|
dispatch_async(dispatch_get_main_queue(), ^{
|