react-native-unified-action-sheet 0.3.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 +55 -1
- package/README.md +24 -4
- package/android/src/main/java/com/unifiedactionsheet/ActionSheetOptions.kt +35 -0
- 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 +8 -7
- package/android/src/material/java/com/unifiedactionsheet/MaterialBottomSheetPresenter.kt +65 -2
- package/ios/BottomSheetViewController.swift +3 -1
- package/ios/UnifiedActionSheet.mm +25 -4
- package/ios/UnifiedActionSheetImpl.swift +124 -27
- 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 +84 -17
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/NativeUnifiedActionSheet.js.map +1 -1
- package/lib/module/index.js +84 -17
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts +7 -1
- package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts +4 -1
- 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 +7 -1
- package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts.map +1 -1
- package/lib/typescript/module/src/action-sheet-options.interface.d.ts +4 -1
- 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 +1 -1
- package/src/NativeUnifiedActionSheet.ts +7 -2
- package/src/action-sheet-options.interface.ts +11 -0
- package/src/common-options.interface.ts +18 -0
- package/src/index.tsx +112 -39
- package/src/prompt-options.interface.ts +21 -5
|
@@ -10,6 +10,8 @@ final class BottomSheetViewController: UIViewController {
|
|
|
10
10
|
let label: String
|
|
11
11
|
let isDestructive: Bool
|
|
12
12
|
let isEnabled: Bool
|
|
13
|
+
/// Semibold, like the preferred action of an alert.
|
|
14
|
+
let isPreferred: Bool
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
private let sheetTitle: String?
|
|
@@ -168,7 +170,7 @@ final class BottomSheetViewController: UIViewController {
|
|
|
168
170
|
config.titleAlignment = .leading
|
|
169
171
|
config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
|
170
172
|
var attributes = attributes
|
|
171
|
-
attributes.font = UIFont.preferredFont(forTextStyle: .body).withWeight(isCancel ? .semibold : .regular)
|
|
173
|
+
attributes.font = UIFont.preferredFont(forTextStyle: .body).withWeight(isCancel || row.isPreferred ? .semibold : .regular)
|
|
172
174
|
|
|
173
175
|
return attributes
|
|
174
176
|
}
|
|
@@ -68,6 +68,15 @@ RCT_EXPORT_METHOD(showActionSheetWithOptions
|
|
|
68
68
|
payload[@"destructiveColor"] = options.destructiveColor();
|
|
69
69
|
payload[@"presentationStyle"] = options.presentationStyle();
|
|
70
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
|
+
|
|
71
80
|
// The anchor arrives already measured from the ref on the JS side, so this
|
|
72
81
|
// module never resolves a view and needs no React Native view API.
|
|
73
82
|
auto anchorRect = options.anchorRect();
|
|
@@ -113,13 +122,21 @@ RCT_EXPORT_METHOD(showPromptWithOptions
|
|
|
113
122
|
return @(element);
|
|
114
123
|
});
|
|
115
124
|
}
|
|
116
|
-
if (options.
|
|
117
|
-
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
|
+
});
|
|
118
133
|
}
|
|
119
134
|
|
|
120
135
|
payload[@"title"] = options.title();
|
|
121
136
|
payload[@"message"] = options.message();
|
|
137
|
+
payload[@"type"] = options.type();
|
|
122
138
|
payload[@"placeholder"] = options.placeholder();
|
|
139
|
+
payload[@"passwordPlaceholder"] = options.passwordPlaceholder();
|
|
123
140
|
payload[@"defaultValue"] = options.defaultValue();
|
|
124
141
|
payload[@"keyboardType"] = options.keyboardType();
|
|
125
142
|
payload[@"tintColor"] = options.tintColor();
|
|
@@ -129,8 +146,12 @@ RCT_EXPORT_METHOD(showPromptWithOptions
|
|
|
129
146
|
|
|
130
147
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
131
148
|
[UnifiedActionSheetImpl.shared showPromptWithOptions:payload
|
|
132
|
-
completion:^(NSInteger buttonIndex, NSString *text) {
|
|
133
|
-
resolve(@{
|
|
149
|
+
completion:^(NSInteger buttonIndex, NSString *text, NSString *password) {
|
|
150
|
+
resolve(@{
|
|
151
|
+
@"buttonIndex" : @(buttonIndex),
|
|
152
|
+
@"text" : text,
|
|
153
|
+
@"password" : password,
|
|
154
|
+
});
|
|
134
155
|
}];
|
|
135
156
|
});
|
|
136
157
|
}
|
|
@@ -26,6 +26,7 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
26
26
|
let tintColor = Self.color(options["tintColor"])
|
|
27
27
|
let cancelButtonTintColor = Self.color(options["cancelButtonTintColor"])
|
|
28
28
|
let destructiveColor = Self.color(options["destructiveColor"])
|
|
29
|
+
let preferredIndex = (options["preferredButtonIndex"] as? NSNumber)?.intValue
|
|
29
30
|
|
|
30
31
|
guard let parent = Self.presentedViewController() else {
|
|
31
32
|
completion(cancelButtonIndex)
|
|
@@ -46,6 +47,7 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
46
47
|
cancelButtonIndex: cancelButtonIndex,
|
|
47
48
|
destructiveIndices: destructiveIndices,
|
|
48
49
|
disabledIndices: disabledIndices,
|
|
50
|
+
preferredIndex: preferredIndex,
|
|
49
51
|
tintColor: tintColor,
|
|
50
52
|
cancelButtonTintColor: cancelButtonTintColor,
|
|
51
53
|
destructiveColor: destructiveColor,
|
|
@@ -69,7 +71,7 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
69
71
|
let presentation = Presentation(
|
|
70
72
|
controller: alert,
|
|
71
73
|
cancelButtonIndex: cancelButtonIndex,
|
|
72
|
-
completion: { index, _ in completion(index) }
|
|
74
|
+
completion: { index, _, _ in completion(index) }
|
|
73
75
|
)
|
|
74
76
|
|
|
75
77
|
for (index, label) in labels.enumerated() {
|
|
@@ -77,7 +79,10 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
77
79
|
let style: UIAlertAction.Style =
|
|
78
80
|
destructiveIndices.contains(index) ? .destructive : (isCancel ? .cancel : .default)
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
// Weak: the presentation holds the alert, which holds this handler.
|
|
83
|
+
let action = UIAlertAction(title: label, style: style) { [weak self, weak presentation] _ in
|
|
84
|
+
guard let presentation else { return }
|
|
85
|
+
|
|
81
86
|
self?.finish(presentation, index: index)
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -98,6 +103,12 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
98
103
|
alert.addAction(action)
|
|
99
104
|
}
|
|
100
105
|
|
|
106
|
+
// UIKit only honors a preferred action in the alert style; the action
|
|
107
|
+
// sheet already bolds its cancel button.
|
|
108
|
+
if isCentered, let preferredIndex, alert.actions.indices.contains(preferredIndex) {
|
|
109
|
+
alert.preferredAction = alert.actions[preferredIndex]
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
alert.view.tintColor = tintColor
|
|
102
113
|
|
|
103
114
|
switch options["userInterfaceStyle"] as? String {
|
|
@@ -140,7 +151,7 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
140
151
|
/// sheet, and a prompt is inherently a centered, modal question.
|
|
141
152
|
@objc public func showPrompt(
|
|
142
153
|
options: NSDictionary,
|
|
143
|
-
completion: @escaping (Int, String) -> Void
|
|
154
|
+
completion: @escaping (Int, String, String) -> Void
|
|
144
155
|
) {
|
|
145
156
|
let labels = options["options"] as? [String] ?? []
|
|
146
157
|
let cancelButtonIndex = (options["cancelButtonIndex"] as? NSNumber)?.intValue ?? -1
|
|
@@ -153,9 +164,14 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
153
164
|
let tintColor = Self.color(options["tintColor"])
|
|
154
165
|
let cancelButtonTintColor = Self.color(options["cancelButtonTintColor"])
|
|
155
166
|
let destructiveColor = Self.color(options["destructiveColor"])
|
|
167
|
+
let preferredIndex = (options["preferredButtonIndex"] as? NSNumber)?.intValue
|
|
168
|
+
let textRequiredIndices = Set(
|
|
169
|
+
(options["textRequiredButtonIndices"] as? [NSNumber])?.map { $0.intValue } ?? []
|
|
170
|
+
)
|
|
171
|
+
let type = options["type"] as? String ?? "plain-text"
|
|
156
172
|
|
|
157
173
|
guard let parent = Self.presentedViewController() else {
|
|
158
|
-
completion(cancelButtonIndex, "")
|
|
174
|
+
completion(cancelButtonIndex, "", "")
|
|
159
175
|
|
|
160
176
|
return
|
|
161
177
|
}
|
|
@@ -169,15 +185,35 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
169
185
|
alert.addTextField { field in
|
|
170
186
|
field.placeholder = Self.text(options["placeholder"])
|
|
171
187
|
field.text = Self.text(options["defaultValue"])
|
|
172
|
-
field.isSecureTextEntry =
|
|
188
|
+
field.isSecureTextEntry = type == "secure-text"
|
|
173
189
|
field.keyboardType = Self.keyboardType(options["keyboardType"])
|
|
190
|
+
if type == "login-password" {
|
|
191
|
+
field.textContentType = .username
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// As in React Native's Alert.prompt: a secure field below the login one.
|
|
196
|
+
// The content types let Password AutoFill offer saved credentials.
|
|
197
|
+
if type == "login-password" {
|
|
198
|
+
alert.addTextField { field in
|
|
199
|
+
field.placeholder = Self.text(options["passwordPlaceholder"])
|
|
200
|
+
field.isSecureTextEntry = true
|
|
201
|
+
field.textContentType = .password
|
|
202
|
+
}
|
|
174
203
|
}
|
|
175
204
|
|
|
176
205
|
let presentation = Presentation(
|
|
177
206
|
controller: alert,
|
|
178
207
|
cancelButtonIndex: cancelButtonIndex,
|
|
179
208
|
// Weak, or the presentation would retain the controller it is stored on.
|
|
180
|
-
|
|
209
|
+
currentValues: { [weak alert] in
|
|
210
|
+
let fields = alert?.textFields ?? []
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
fields.first?.text ?? "",
|
|
214
|
+
fields.count > 1 ? (fields[1].text ?? "") : ""
|
|
215
|
+
)
|
|
216
|
+
},
|
|
181
217
|
completion: completion
|
|
182
218
|
)
|
|
183
219
|
|
|
@@ -186,7 +222,10 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
186
222
|
let style: UIAlertAction.Style =
|
|
187
223
|
destructiveIndices.contains(index) ? .destructive : (isCancel ? .cancel : .default)
|
|
188
224
|
|
|
189
|
-
|
|
225
|
+
// Weak: the presentation holds the alert, which holds this handler.
|
|
226
|
+
let action = UIAlertAction(title: label, style: style) { [weak self, weak presentation] _ in
|
|
227
|
+
guard let presentation else { return }
|
|
228
|
+
|
|
190
229
|
self?.finish(presentation, index: index)
|
|
191
230
|
}
|
|
192
231
|
|
|
@@ -203,6 +242,29 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
203
242
|
alert.addAction(action)
|
|
204
243
|
}
|
|
205
244
|
|
|
245
|
+
// Bold, and triggered by the keyboard's return key.
|
|
246
|
+
if let preferredIndex, alert.actions.indices.contains(preferredIndex) {
|
|
247
|
+
alert.preferredAction = alert.actions[preferredIndex]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// requiresText: those buttons stay disabled while any field is empty.
|
|
251
|
+
if !textRequiredIndices.isEmpty {
|
|
252
|
+
let update: () -> Void = { [weak alert] in
|
|
253
|
+
guard let alert else { return }
|
|
254
|
+
|
|
255
|
+
let filled = (alert.textFields ?? []).allSatisfy { !($0.text ?? "").isEmpty }
|
|
256
|
+
for index in textRequiredIndices where alert.actions.indices.contains(index) {
|
|
257
|
+
alert.actions[index].isEnabled = filled && !disabledIndices.contains(index)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
alert.textFields?.forEach { field in
|
|
262
|
+
field.addAction(UIAction { _ in update() }, for: .editingChanged)
|
|
263
|
+
}
|
|
264
|
+
// A defaultValue may already satisfy it.
|
|
265
|
+
update()
|
|
266
|
+
}
|
|
267
|
+
|
|
206
268
|
alert.view.tintColor = tintColor
|
|
207
269
|
|
|
208
270
|
switch options["userInterfaceStyle"] as? String {
|
|
@@ -222,6 +284,7 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
222
284
|
cancelButtonIndex: Int,
|
|
223
285
|
destructiveIndices: Set<Int>,
|
|
224
286
|
disabledIndices: Set<Int>,
|
|
287
|
+
preferredIndex: Int?,
|
|
225
288
|
tintColor: UIColor?,
|
|
226
289
|
cancelButtonTintColor: UIColor?,
|
|
227
290
|
destructiveColor: UIColor?,
|
|
@@ -232,7 +295,8 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
232
295
|
index: index,
|
|
233
296
|
label: label,
|
|
234
297
|
isDestructive: destructiveIndices.contains(index),
|
|
235
|
-
isEnabled: !disabledIndices.contains(index)
|
|
298
|
+
isEnabled: !disabledIndices.contains(index),
|
|
299
|
+
isPreferred: index == preferredIndex
|
|
236
300
|
)
|
|
237
301
|
}
|
|
238
302
|
|
|
@@ -249,7 +313,7 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
249
313
|
let presentation = Presentation(
|
|
250
314
|
controller: sheet,
|
|
251
315
|
cancelButtonIndex: cancelButtonIndex,
|
|
252
|
-
completion: { index, _ in completion(index) }
|
|
316
|
+
completion: { index, _, _ in completion(index) }
|
|
253
317
|
)
|
|
254
318
|
|
|
255
319
|
// Weak: the presentation holds the sheet, which holds this closure.
|
|
@@ -278,16 +342,27 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
278
342
|
|
|
279
343
|
let bounds = parent.view.window?.bounds ?? parent.view.bounds
|
|
280
344
|
let fitting = sheet.fittingHeight(width: bounds.width)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
345
|
+
let requested = options["detents"] as? [String] ?? []
|
|
346
|
+
|
|
347
|
+
if !requested.isEmpty {
|
|
348
|
+
// The caller's heights; it opens at the first. Duplicates collapse,
|
|
349
|
+
// e.g. 'auto' and 'medium' on iOS 15, where 'auto' is half height.
|
|
350
|
+
var detents: [UISheetPresentationController.Detent] = []
|
|
351
|
+
var identifiers: [UISheetPresentationController.Detent.Identifier] = []
|
|
352
|
+
for name in requested {
|
|
353
|
+
let (detent, identifier) = Self.detent(named: name, fitting: fitting)
|
|
354
|
+
guard !identifiers.contains(identifier) else { continue }
|
|
355
|
+
|
|
356
|
+
detents.append(detent)
|
|
357
|
+
identifiers.append(identifier)
|
|
358
|
+
}
|
|
359
|
+
controller.detents = detents
|
|
360
|
+
controller.selectedDetentIdentifier = identifiers.first
|
|
361
|
+
} else if #available(iOS 16.0, *), fitting <= bounds.height / 2 {
|
|
362
|
+
// A short list opens at its own height; a long one opens at half
|
|
363
|
+
// height and drags up to expand, like the Android sheet. Fitting the
|
|
364
|
+
// content needs a custom detent, iOS 16+; iOS 15 opens at half height.
|
|
365
|
+
controller.detents = [Self.detent(named: "auto", fitting: fitting).0]
|
|
291
366
|
} else {
|
|
292
367
|
controller.detents = [.medium(), .large()]
|
|
293
368
|
}
|
|
@@ -374,24 +449,45 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
374
449
|
presentation.resolve(index)
|
|
375
450
|
}
|
|
376
451
|
|
|
452
|
+
/// 'auto' fits the content (iOS 16+; half height on iOS 15), 'medium' is
|
|
453
|
+
/// half height, 'large' is full height.
|
|
454
|
+
private static func detent(
|
|
455
|
+
named name: String,
|
|
456
|
+
fitting: CGFloat
|
|
457
|
+
) -> (UISheetPresentationController.Detent, UISheetPresentationController.Detent.Identifier) {
|
|
458
|
+
switch name {
|
|
459
|
+
case "large":
|
|
460
|
+
return (.large(), .large)
|
|
461
|
+
case "auto":
|
|
462
|
+
if #available(iOS 16.0, *) {
|
|
463
|
+
let identifier = UISheetPresentationController.Detent.Identifier("unifiedActionSheet.content")
|
|
464
|
+
|
|
465
|
+
return (.custom(identifier: identifier) { min(fitting, $0.maximumDetentValue) }, identifier)
|
|
466
|
+
}
|
|
467
|
+
return (.medium(), .medium)
|
|
468
|
+
default:
|
|
469
|
+
return (.medium(), .medium)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
377
473
|
private final class Presentation {
|
|
378
474
|
/// A UIAlertController, or the bottom sheet on iPhone.
|
|
379
475
|
let controller: UIViewController
|
|
380
476
|
let cancelButtonIndex: Int
|
|
381
|
-
/// Read at resolve time, not at creation: the
|
|
382
|
-
/// whatever is in the
|
|
383
|
-
private let
|
|
384
|
-
private var completion: ((Int, String) -> Void)?
|
|
477
|
+
/// Read at resolve time, not at creation: the values that matter are
|
|
478
|
+
/// whatever is in the fields when the prompt closes. Sheets pass constants.
|
|
479
|
+
private let currentValues: () -> (String, String)
|
|
480
|
+
private var completion: ((Int, String, String) -> Void)?
|
|
385
481
|
|
|
386
482
|
init(
|
|
387
483
|
controller: UIViewController,
|
|
388
484
|
cancelButtonIndex: Int,
|
|
389
|
-
|
|
390
|
-
completion: @escaping (Int, String) -> Void
|
|
485
|
+
currentValues: @escaping () -> (String, String) = { ("", "") },
|
|
486
|
+
completion: @escaping (Int, String, String) -> Void
|
|
391
487
|
) {
|
|
392
488
|
self.controller = controller
|
|
393
489
|
self.cancelButtonIndex = cancelButtonIndex
|
|
394
|
-
self.
|
|
490
|
+
self.currentValues = currentValues
|
|
395
491
|
self.completion = completion
|
|
396
492
|
}
|
|
397
493
|
|
|
@@ -404,7 +500,8 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
|
|
|
404
500
|
guard let completion else { return }
|
|
405
501
|
|
|
406
502
|
self.completion = nil
|
|
407
|
-
|
|
503
|
+
let (text, password) = currentValues()
|
|
504
|
+
completion(index, text, password)
|
|
408
505
|
}
|
|
409
506
|
}
|
|
410
507
|
|
package/jest/index.d.ts
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ActionSheetOptionsInterface,
|
|
3
|
+
ActionSheetResultInterface,
|
|
3
4
|
PromptOptionsInterface,
|
|
4
5
|
PromptResultInterface,
|
|
5
6
|
} from 'react-native-unified-action-sheet';
|
|
6
7
|
|
|
7
|
-
/// Queues the index the next sheet resolves with
|
|
8
|
-
/// resolving
|
|
8
|
+
/// Queues the index the next sheet resolves with; the reason is derived as the
|
|
9
|
+
/// real module would. Pass nothing to go back to resolving as dismissed.
|
|
9
10
|
export declare function setNextButtonIndex(index?: number): void;
|
|
10
11
|
|
|
11
12
|
export declare const showActionSheetWithOptions: jest.Mock<
|
|
12
|
-
Promise<
|
|
13
|
+
Promise<ActionSheetResultInterface>,
|
|
13
14
|
[ActionSheetOptionsInterface]
|
|
14
15
|
>;
|
|
15
|
-
/// Queues what the next prompt resolves with
|
|
16
|
-
/// resolving
|
|
17
|
-
export declare function setNextPromptResult(
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/// Queues what the next prompt resolves with; the reason is derived as the
|
|
17
|
+
/// real module would. Pass nothing to go back to resolving as dismissed.
|
|
18
|
+
export declare function setNextPromptResult(result?: {
|
|
19
|
+
buttonIndex: number;
|
|
20
|
+
text: string;
|
|
21
|
+
password?: string;
|
|
22
|
+
}): void;
|
|
20
23
|
|
|
21
24
|
export declare const showPromptWithOptions: jest.Mock<
|
|
22
|
-
Promise<PromptResultInterface
|
|
25
|
+
Promise<PromptResultInterface>,
|
|
23
26
|
[PromptOptionsInterface]
|
|
24
27
|
>;
|
|
25
28
|
export declare const dismissActionSheet: jest.Mock<void, []>;
|
package/jest/index.js
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* require('react-native-unified-action-sheet/jest')
|
|
8
8
|
* );
|
|
9
9
|
*
|
|
10
|
-
* By default every sheet resolves
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* By default every sheet resolves as dismissed: { reason: 'dismissed',
|
|
11
|
+
* buttonIndex: undefined }. To simulate a tap, queue the index the next sheet
|
|
12
|
+
* should resolve with; it resolves { reason, buttonIndex } with the reason the
|
|
13
|
+
* real module would give ('cancelled' for the cancel button or -1, otherwise
|
|
14
|
+
* 'selected'), and the pressed button's onPress runs as it would for real:
|
|
13
15
|
*
|
|
14
16
|
* setNextButtonIndex(0);
|
|
15
17
|
* await openTheSheet();
|
|
@@ -17,11 +19,26 @@
|
|
|
17
19
|
* Prefer that over mockResolvedValueOnce, which replaces the implementation and
|
|
18
20
|
* so skips onPress.
|
|
19
21
|
*
|
|
20
|
-
* A prompt is queued the same way, with
|
|
22
|
+
* A prompt is queued the same way, with what the field(s) should hold (password
|
|
23
|
+
* only for a 'login-password' prompt); onPress receives { text, password }:
|
|
21
24
|
*
|
|
22
25
|
* setNextPromptResult({ buttonIndex: 0, text: 'typed' });
|
|
23
26
|
* await openThePrompt();
|
|
24
27
|
*/
|
|
28
|
+
const buttonsOf = (options) => (options && options.options) || [];
|
|
29
|
+
|
|
30
|
+
/// The same rule as the real module: the first button styled 'cancel', or -1,
|
|
31
|
+
/// is a cancellation; any other index is a selection.
|
|
32
|
+
const closeResult = (buttons, buttonIndex) => {
|
|
33
|
+
if (buttonIndex == null)
|
|
34
|
+
return { reason: 'dismissed', buttonIndex: undefined };
|
|
35
|
+
|
|
36
|
+
const cancelIndex = buttons.findIndex((button) => button.style === 'cancel');
|
|
37
|
+
const cancelled = buttonIndex < 0 || buttonIndex === cancelIndex;
|
|
38
|
+
|
|
39
|
+
return { reason: cancelled ? 'cancelled' : 'selected', buttonIndex };
|
|
40
|
+
};
|
|
41
|
+
|
|
25
42
|
let nextButtonIndex;
|
|
26
43
|
|
|
27
44
|
const setNextButtonIndex = (index) => {
|
|
@@ -29,15 +46,14 @@ const setNextButtonIndex = (index) => {
|
|
|
29
46
|
};
|
|
30
47
|
|
|
31
48
|
const showActionSheetWithOptions = jest.fn((options) => {
|
|
32
|
-
const
|
|
49
|
+
const buttons = buttonsOf(options);
|
|
50
|
+
const result = closeResult(buttons, nextButtonIndex);
|
|
33
51
|
nextButtonIndex = undefined;
|
|
34
52
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (button && typeof button.onPress === 'function') button.onPress();
|
|
38
|
-
}
|
|
53
|
+
const button = result.buttonIndex != null && buttons[result.buttonIndex];
|
|
54
|
+
if (button && typeof button.onPress === 'function') button.onPress();
|
|
39
55
|
|
|
40
|
-
return Promise.resolve(
|
|
56
|
+
return Promise.resolve(result);
|
|
41
57
|
});
|
|
42
58
|
|
|
43
59
|
let nextPromptResult;
|
|
@@ -47,16 +63,19 @@ const setNextPromptResult = (result) => {
|
|
|
47
63
|
};
|
|
48
64
|
|
|
49
65
|
const showPromptWithOptions = jest.fn((options) => {
|
|
50
|
-
const
|
|
66
|
+
const buttons = buttonsOf(options);
|
|
67
|
+
const queued = nextPromptResult;
|
|
51
68
|
nextPromptResult = undefined;
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
const values = { text: (queued && queued.text) || '' };
|
|
71
|
+
if (queued && queued.password != null) values.password = queued.password;
|
|
72
|
+
const result = {
|
|
73
|
+
...closeResult(buttons, queued ? queued.buttonIndex : undefined),
|
|
74
|
+
...values,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const button = result.buttonIndex != null && buttons[result.buttonIndex];
|
|
78
|
+
if (button && typeof button.onPress === 'function') button.onPress(values);
|
|
60
79
|
|
|
61
80
|
return Promise.resolve(result);
|
|
62
81
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_reactNative","require","_default","exports","default","TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeUnifiedActionSheet.ts"],"mappings":";;;;;;AAAA,IAAAA,YAAA,GAAAC,OAAA;AAAqE,IAAAC,QAAA,GAAAC,OAAA,CAAAC,OAAA,
|
|
1
|
+
{"version":3,"names":["_reactNative","require","_default","exports","default","TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeUnifiedActionSheet.ts"],"mappings":";;;;;;AAAA,IAAAA,YAAA,GAAAC,OAAA;AAAqE,IAAAC,QAAA,GAAAC,OAAA,CAAAC,OAAA,GAuDtDC,gCAAmB,CAACC,YAAY,CAAO,oBAAoB,CAAC","ignoreList":[]}
|
package/lib/commonjs/index.js
CHANGED
|
@@ -31,6 +31,7 @@ const toWireButtons = buttons => {
|
|
|
31
31
|
const destructive = [];
|
|
32
32
|
const disabled = [];
|
|
33
33
|
let cancelButtonIndex;
|
|
34
|
+
let preferredButtonIndex;
|
|
34
35
|
buttons.forEach((button, index) => {
|
|
35
36
|
labels.push(button.label);
|
|
36
37
|
if (button.style === 'destructive') destructive.push(index);
|
|
@@ -38,6 +39,9 @@ const toWireButtons = buttons => {
|
|
|
38
39
|
cancelButtonIndex = index;
|
|
39
40
|
}
|
|
40
41
|
if (button.disabled) disabled.push(index);
|
|
42
|
+
if (button.preferred && preferredButtonIndex == null) {
|
|
43
|
+
preferredButtonIndex = index;
|
|
44
|
+
}
|
|
41
45
|
});
|
|
42
46
|
return {
|
|
43
47
|
options: labels,
|
|
@@ -49,12 +53,40 @@ const toWireButtons = buttons => {
|
|
|
49
53
|
}),
|
|
50
54
|
...(disabled.length === 0 ? {} : {
|
|
51
55
|
disabledButtonIndices: disabled
|
|
56
|
+
}),
|
|
57
|
+
...(preferredButtonIndex == null ? {} : {
|
|
58
|
+
preferredButtonIndex
|
|
52
59
|
})
|
|
53
60
|
};
|
|
54
61
|
};
|
|
62
|
+
|
|
63
|
+
/// Native sides only report an index: DISMISSED_BY_API, the cancel button's
|
|
64
|
+
/// index, -1 (a cancel gesture with no cancel button), or another button.
|
|
65
|
+
const toCloseResult = (buttonIndex, cancelButtonIndex) => {
|
|
66
|
+
if (buttonIndex === DISMISSED_BY_API) {
|
|
67
|
+
return {
|
|
68
|
+
reason: 'dismissed',
|
|
69
|
+
buttonIndex: undefined
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (buttonIndex < 0 || buttonIndex === cancelButtonIndex) {
|
|
73
|
+
return {
|
|
74
|
+
reason: 'cancelled',
|
|
75
|
+
buttonIndex
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
reason: 'selected',
|
|
80
|
+
buttonIndex
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const DISMISSED = {
|
|
84
|
+
reason: 'dismissed',
|
|
85
|
+
buttonIndex: undefined
|
|
86
|
+
};
|
|
55
87
|
const showWithNativeModule = options => nativeModule().showActionSheetWithOptions(options)
|
|
56
|
-
// A
|
|
57
|
-
.
|
|
88
|
+
// A native failure reads as a cancellation rather than a rejection.
|
|
89
|
+
.catch(() => options.cancelButtonIndex ?? -1).then(buttonIndex => toCloseResult(buttonIndex, options.cancelButtonIndex));
|
|
58
90
|
let warnedMaterialDisabled = false;
|
|
59
91
|
|
|
60
92
|
/// 'bottom' needs Material on Android, which the app opts into at build time.
|
|
@@ -92,13 +124,15 @@ const withAnchorRect = (wire, anchor) => {
|
|
|
92
124
|
const showActionSheetWithOptions = options => {
|
|
93
125
|
const wire = toWireOptions(options);
|
|
94
126
|
|
|
95
|
-
// -1 and
|
|
96
|
-
const press =
|
|
97
|
-
if (buttonIndex != null)
|
|
98
|
-
|
|
127
|
+
// -1 and a dismissal index nothing, so optional chaining covers both.
|
|
128
|
+
const press = result => {
|
|
129
|
+
if (result.buttonIndex != null) {
|
|
130
|
+
options.options[result.buttonIndex]?.onPress?.();
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
99
133
|
};
|
|
100
134
|
if (_reactNative.Platform.OS !== 'ios' && _reactNative.Platform.OS !== 'android') {
|
|
101
|
-
return Promise.resolve(
|
|
135
|
+
return Promise.resolve(DISMISSED);
|
|
102
136
|
}
|
|
103
137
|
warnIfBottomUnavailable(options.presentationStyle);
|
|
104
138
|
|
|
@@ -114,8 +148,8 @@ const dismissActionSheet = () => {
|
|
|
114
148
|
}
|
|
115
149
|
};
|
|
116
150
|
|
|
117
|
-
/// Closes every open sheet, not just the top-most one. Each resolves
|
|
118
|
-
///
|
|
151
|
+
/// Closes every open sheet, not just the top-most one. Each resolves as
|
|
152
|
+
/// 'dismissed', exactly as with dismissActionSheet().
|
|
119
153
|
exports.dismissActionSheet = dismissActionSheet;
|
|
120
154
|
const dismissAllActionSheets = () => {
|
|
121
155
|
if (_reactNative.Platform.OS === 'ios' || _reactNative.Platform.OS === 'android') {
|
|
@@ -131,24 +165,57 @@ exports.dismissAllActionSheets = dismissAllActionSheets;
|
|
|
131
165
|
const showPromptWithOptions = options => {
|
|
132
166
|
const {
|
|
133
167
|
options: buttons,
|
|
168
|
+
secureTextEntry,
|
|
169
|
+
type,
|
|
134
170
|
...rest
|
|
135
171
|
} = options;
|
|
172
|
+
const textRequired = buttons.flatMap((button, index) => button.requiresText ? [index] : []);
|
|
136
173
|
const wire = {
|
|
137
174
|
...rest,
|
|
138
|
-
...toWireButtons(buttons)
|
|
175
|
+
...toWireButtons(buttons),
|
|
176
|
+
// One field of truth for the native sides: the old boolean maps onto type.
|
|
177
|
+
type: type ?? (secureTextEntry ? 'secure-text' : 'plain-text'),
|
|
178
|
+
...(textRequired.length === 0 ? {} : {
|
|
179
|
+
textRequiredButtonIndices: textRequired
|
|
180
|
+
})
|
|
139
181
|
};
|
|
182
|
+
const hasPassword = wire.type === 'login-password';
|
|
140
183
|
if (_reactNative.Platform.OS !== 'ios' && _reactNative.Platform.OS !== 'android') {
|
|
141
|
-
return Promise.resolve(
|
|
184
|
+
return Promise.resolve({
|
|
185
|
+
...DISMISSED,
|
|
186
|
+
text: ''
|
|
187
|
+
});
|
|
142
188
|
}
|
|
143
|
-
return nativeModule().showPromptWithOptions(wire).
|
|
144
|
-
// A programmatic dismiss resolves with no selection, matching the sheet.
|
|
145
|
-
result.buttonIndex === DISMISSED_BY_API ? undefined : result).catch(() => ({
|
|
189
|
+
return nativeModule().showPromptWithOptions(wire).catch(() => ({
|
|
146
190
|
buttonIndex: wire.cancelButtonIndex ?? -1,
|
|
147
|
-
text: ''
|
|
191
|
+
text: '',
|
|
192
|
+
password: ''
|
|
193
|
+
})).then(({
|
|
194
|
+
buttonIndex,
|
|
195
|
+
text,
|
|
196
|
+
password
|
|
197
|
+
}) => ({
|
|
198
|
+
...toCloseResult(buttonIndex, wire.cancelButtonIndex),
|
|
199
|
+
text,
|
|
200
|
+
// Even a dismissal carries the fields, so a draft is recoverable.
|
|
201
|
+
...(hasPassword ? {
|
|
202
|
+
password
|
|
203
|
+
} : {})
|
|
148
204
|
})).then(result => {
|
|
149
|
-
// The
|
|
205
|
+
// The values go to the handler, so a caller using onPress alone never
|
|
150
206
|
// has to read the resolved value.
|
|
151
|
-
if (result
|
|
207
|
+
if (result.buttonIndex != null) {
|
|
208
|
+
const {
|
|
209
|
+
text,
|
|
210
|
+
password
|
|
211
|
+
} = result;
|
|
212
|
+
buttons[result.buttonIndex]?.onPress?.(hasPassword ? {
|
|
213
|
+
text,
|
|
214
|
+
password
|
|
215
|
+
} : {
|
|
216
|
+
text
|
|
217
|
+
});
|
|
218
|
+
}
|
|
152
219
|
return result;
|
|
153
220
|
});
|
|
154
221
|
};
|