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.
Files changed (45) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +24 -4
  3. package/android/src/main/java/com/unifiedactionsheet/ActionSheetOptions.kt +35 -0
  4. package/android/src/main/java/com/unifiedactionsheet/PromptOptions.kt +27 -5
  5. package/android/src/main/java/com/unifiedactionsheet/SheetContentBuilder.kt +75 -9
  6. package/android/src/main/java/com/unifiedactionsheet/SheetPresenters.kt +61 -10
  7. package/android/src/main/java/com/unifiedactionsheet/UnifiedActionSheetModule.kt +8 -7
  8. package/android/src/material/java/com/unifiedactionsheet/MaterialBottomSheetPresenter.kt +65 -2
  9. package/ios/BottomSheetViewController.swift +3 -1
  10. package/ios/UnifiedActionSheet.mm +25 -4
  11. package/ios/UnifiedActionSheetImpl.swift +124 -27
  12. package/jest/index.d.ts +12 -9
  13. package/jest/index.js +37 -18
  14. package/lib/commonjs/NativeUnifiedActionSheet.js.map +1 -1
  15. package/lib/commonjs/index.js +84 -17
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/module/NativeUnifiedActionSheet.js.map +1 -1
  18. package/lib/module/index.js +84 -17
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts +7 -1
  21. package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts.map +1 -1
  22. package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts +4 -1
  23. package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/src/common-options.interface.d.ts +9 -0
  25. package/lib/typescript/commonjs/src/common-options.interface.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/src/index.d.ts +6 -6
  27. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/src/prompt-options.interface.d.ts +10 -6
  29. package/lib/typescript/commonjs/src/prompt-options.interface.d.ts.map +1 -1
  30. package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts +7 -1
  31. package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts.map +1 -1
  32. package/lib/typescript/module/src/action-sheet-options.interface.d.ts +4 -1
  33. package/lib/typescript/module/src/action-sheet-options.interface.d.ts.map +1 -1
  34. package/lib/typescript/module/src/common-options.interface.d.ts +9 -0
  35. package/lib/typescript/module/src/common-options.interface.d.ts.map +1 -1
  36. package/lib/typescript/module/src/index.d.ts +6 -6
  37. package/lib/typescript/module/src/index.d.ts.map +1 -1
  38. package/lib/typescript/module/src/prompt-options.interface.d.ts +10 -6
  39. package/lib/typescript/module/src/prompt-options.interface.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/NativeUnifiedActionSheet.ts +7 -2
  42. package/src/action-sheet-options.interface.ts +11 -0
  43. package/src/common-options.interface.ts +18 -0
  44. package/src/index.tsx +112 -39
  45. 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.secureTextEntry()) {
117
- 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
+ });
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(@{@"buttonIndex" : @(buttonIndex), @"text" : text});
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
- let action = UIAlertAction(title: label, style: style) { [weak self] _ in
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 = (options["secureTextEntry"] as? NSNumber)?.boolValue ?? false
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
- currentText: { [weak alert] in alert?.textFields?.first?.text ?? "" },
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
- let action = UIAlertAction(title: label, style: style) { [weak self] _ in
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
- // A short list opens at its own height; a long one opens at half height
283
- // and drags up to expand, like the Android sheet. Fitting the content
284
- // needs a custom detent, iOS 16+; iOS 15 opens at half height.
285
- if #available(iOS 16.0, *), fitting <= bounds.height / 2 {
286
- controller.detents = [
287
- .custom(identifier: .init("unifiedActionSheet.content")) { context in
288
- min(fitting, context.maximumDetentValue)
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 value that matters is
382
- /// whatever is in the field when the prompt closes. Sheets pass a constant.
383
- private let currentText: () -> String
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
- currentText: @escaping () -> String = { "" },
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.currentText = currentText
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
- completion(index, currentText())
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. Pass nothing to go back to
8
- /// resolving with no selection.
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<number | undefined>,
13
+ Promise<ActionSheetResultInterface>,
13
14
  [ActionSheetOptionsInterface]
14
15
  >;
15
- /// Queues what the next prompt resolves with. Pass nothing to go back to
16
- /// resolving with no selection.
17
- export declare function setNextPromptResult(
18
- result?: PromptResultInterface
19
- ): void;
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 | undefined>,
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 with no selection. To simulate a tap, queue
11
- * the index the next sheet should resolve with; the pressed button's onPress
12
- * runs, exactly as it would in the real module:
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 the text the field should resolve 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 buttonIndex = nextButtonIndex;
49
+ const buttons = buttonsOf(options);
50
+ const result = closeResult(buttons, nextButtonIndex);
33
51
  nextButtonIndex = undefined;
34
52
 
35
- if (buttonIndex != null) {
36
- const button = options && options.options && options.options[buttonIndex];
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(buttonIndex);
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 result = nextPromptResult;
66
+ const buttons = buttonsOf(options);
67
+ const queued = nextPromptResult;
51
68
  nextPromptResult = undefined;
52
69
 
53
- if (result) {
54
- const button =
55
- options && options.options && options.options[result.buttonIndex];
56
- if (button && typeof button.onPress === 'function') {
57
- button.onPress(result.text);
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,GAkDtDC,gCAAmB,CAACC,YAAY,CAAO,oBAAoB,CAAC","ignoreList":[]}
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":[]}
@@ -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 programmatic dismiss resolves with no index rather than a selection.
57
- .then(buttonIndex => buttonIndex === DISMISSED_BY_API ? undefined : buttonIndex).catch(() => options.cancelButtonIndex ?? -1);
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 undefined index nothing, so optional chaining covers both.
96
- const press = buttonIndex => {
97
- if (buttonIndex != null) options.options[buttonIndex]?.onPress?.();
98
- return buttonIndex;
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(undefined);
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 with no
118
- /// index, exactly as dismissActionSheet() does.
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(undefined);
184
+ return Promise.resolve({
185
+ ...DISMISSED,
186
+ text: ''
187
+ });
142
188
  }
143
- return nativeModule().showPromptWithOptions(wire).then(result =>
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 text goes to the handler, so a caller using onPress alone never
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) buttons[result.buttonIndex]?.onPress?.(result.text);
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
  };