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.
Files changed (51) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +44 -112
  3. package/android/build.gradle +28 -1
  4. package/android/src/main/java/com/unifiedactionsheet/ActionSheetOptions.kt +41 -2
  5. package/android/src/main/java/com/unifiedactionsheet/PromptOptions.kt +27 -5
  6. package/android/src/main/java/com/unifiedactionsheet/SheetContentBuilder.kt +75 -9
  7. package/android/src/main/java/com/unifiedactionsheet/SheetPresenters.kt +61 -10
  8. package/android/src/main/java/com/unifiedactionsheet/UnifiedActionSheetModule.kt +14 -7
  9. package/android/src/material/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
  10. package/android/src/material/java/com/unifiedactionsheet/MaterialBottomSheetPresenter.kt +153 -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 +210 -0
  15. package/ios/UnifiedActionSheet.mm +51 -5
  16. package/ios/UnifiedActionSheetImpl.swift +259 -22
  17. package/jest/index.d.ts +12 -9
  18. package/jest/index.js +37 -18
  19. package/lib/commonjs/NativeUnifiedActionSheet.js.map +1 -1
  20. package/lib/commonjs/index.js +102 -18
  21. package/lib/commonjs/index.js.map +1 -1
  22. package/lib/module/NativeUnifiedActionSheet.js.map +1 -1
  23. package/lib/module/index.js +102 -18
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts +10 -1
  26. package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts +5 -2
  28. package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/src/common-options.interface.d.ts +9 -0
  30. package/lib/typescript/commonjs/src/common-options.interface.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/src/index.d.ts +6 -6
  32. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/src/prompt-options.interface.d.ts +10 -6
  34. package/lib/typescript/commonjs/src/prompt-options.interface.d.ts.map +1 -1
  35. package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts +10 -1
  36. package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts.map +1 -1
  37. package/lib/typescript/module/src/action-sheet-options.interface.d.ts +5 -2
  38. package/lib/typescript/module/src/action-sheet-options.interface.d.ts.map +1 -1
  39. package/lib/typescript/module/src/common-options.interface.d.ts +9 -0
  40. package/lib/typescript/module/src/common-options.interface.d.ts.map +1 -1
  41. package/lib/typescript/module/src/index.d.ts +6 -6
  42. package/lib/typescript/module/src/index.d.ts.map +1 -1
  43. package/lib/typescript/module/src/prompt-options.interface.d.ts +10 -6
  44. package/lib/typescript/module/src/prompt-options.interface.d.ts.map +1 -1
  45. package/package.json +12 -2
  46. package/plugin/withUnifiedActionSheet.js +58 -0
  47. package/src/NativeUnifiedActionSheet.ts +12 -2
  48. package/src/action-sheet-options.interface.ts +12 -1
  49. package/src/common-options.interface.ts +18 -0
  50. package/src/index.tsx +147 -42
  51. 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.secureTextEntry()) {
102
- payload[@"secureTextEntry"] = @(*options.secureTextEntry());
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(@{@"buttonIndex" : @(buttonIndex), @"text" : text});
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(), ^{