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
@@ -2,7 +2,9 @@ import Foundation
2
2
  import UIKit
3
3
 
4
4
  @objc(UnifiedActionSheetImpl)
5
- public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDelegate {
5
+ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDelegate,
6
+ UISheetPresentationControllerDelegate
7
+ {
6
8
  @objc public static let shared = UnifiedActionSheetImpl()
7
9
 
8
10
  private static let dismissedByApi = -2
@@ -24,6 +26,7 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
24
26
  let tintColor = Self.color(options["tintColor"])
25
27
  let cancelButtonTintColor = Self.color(options["cancelButtonTintColor"])
26
28
  let destructiveColor = Self.color(options["destructiveColor"])
29
+ let preferredIndex = (options["preferredButtonIndex"] as? NSNumber)?.intValue
27
30
 
28
31
  guard let parent = Self.presentedViewController() else {
29
32
  completion(cancelButtonIndex)
@@ -31,6 +34,29 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
31
34
  return
32
35
  }
33
36
 
37
+ // 'bottom' is a real sheet on iPhone. iPad keeps the action sheet below,
38
+ // which presents as a popover there: a sheet on iPad is a centered form
39
+ // sheet, not attached to the bottom at all.
40
+ if options["presentationStyle"] as? String == "bottom",
41
+ UIDevice.current.userInterfaceIdiom != .pad
42
+ {
43
+ presentBottomSheet(
44
+ on: parent,
45
+ options: options,
46
+ labels: labels,
47
+ cancelButtonIndex: cancelButtonIndex,
48
+ destructiveIndices: destructiveIndices,
49
+ disabledIndices: disabledIndices,
50
+ preferredIndex: preferredIndex,
51
+ tintColor: tintColor,
52
+ cancelButtonTintColor: cancelButtonTintColor,
53
+ destructiveColor: destructiveColor,
54
+ completion: completion
55
+ )
56
+
57
+ return
58
+ }
59
+
34
60
  // 'centered' maps to UIKit's centered alert style; anything else (including
35
61
  // absent, and 'anchored') stays .actionSheet, the iOS default — presented
36
62
  // from the bottom on iPhone and as a popover on iPad.
@@ -45,7 +71,7 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
45
71
  let presentation = Presentation(
46
72
  controller: alert,
47
73
  cancelButtonIndex: cancelButtonIndex,
48
- completion: { index, _ in completion(index) }
74
+ completion: { index, _, _ in completion(index) }
49
75
  )
50
76
 
51
77
  for (index, label) in labels.enumerated() {
@@ -53,7 +79,10 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
53
79
  let style: UIAlertAction.Style =
54
80
  destructiveIndices.contains(index) ? .destructive : (isCancel ? .cancel : .default)
55
81
 
56
- 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
+
57
86
  self?.finish(presentation, index: index)
58
87
  }
59
88
 
@@ -74,6 +103,12 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
74
103
  alert.addAction(action)
75
104
  }
76
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
+
77
112
  alert.view.tintColor = tintColor
78
113
 
79
114
  switch options["userInterfaceStyle"] as? String {
@@ -116,7 +151,7 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
116
151
  /// sheet, and a prompt is inherently a centered, modal question.
117
152
  @objc public func showPrompt(
118
153
  options: NSDictionary,
119
- completion: @escaping (Int, String) -> Void
154
+ completion: @escaping (Int, String, String) -> Void
120
155
  ) {
121
156
  let labels = options["options"] as? [String] ?? []
122
157
  let cancelButtonIndex = (options["cancelButtonIndex"] as? NSNumber)?.intValue ?? -1
@@ -129,9 +164,14 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
129
164
  let tintColor = Self.color(options["tintColor"])
130
165
  let cancelButtonTintColor = Self.color(options["cancelButtonTintColor"])
131
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"
132
172
 
133
173
  guard let parent = Self.presentedViewController() else {
134
- completion(cancelButtonIndex, "")
174
+ completion(cancelButtonIndex, "", "")
135
175
 
136
176
  return
137
177
  }
@@ -145,15 +185,35 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
145
185
  alert.addTextField { field in
146
186
  field.placeholder = Self.text(options["placeholder"])
147
187
  field.text = Self.text(options["defaultValue"])
148
- field.isSecureTextEntry = (options["secureTextEntry"] as? NSNumber)?.boolValue ?? false
188
+ field.isSecureTextEntry = type == "secure-text"
149
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
+ }
150
203
  }
151
204
 
152
205
  let presentation = Presentation(
153
206
  controller: alert,
154
207
  cancelButtonIndex: cancelButtonIndex,
155
208
  // Weak, or the presentation would retain the controller it is stored on.
156
- 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
+ },
157
217
  completion: completion
158
218
  )
159
219
 
@@ -162,7 +222,10 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
162
222
  let style: UIAlertAction.Style =
163
223
  destructiveIndices.contains(index) ? .destructive : (isCancel ? .cancel : .default)
164
224
 
165
- 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
+
166
229
  self?.finish(presentation, index: index)
167
230
  }
168
231
 
@@ -179,6 +242,29 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
179
242
  alert.addAction(action)
180
243
  }
181
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
+
182
268
  alert.view.tintColor = tintColor
183
269
 
184
270
  switch options["userInterfaceStyle"] as? String {
@@ -191,6 +277,116 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
191
277
  parent.present(alert, animated: true)
192
278
  }
193
279
 
280
+ private func presentBottomSheet(
281
+ on parent: UIViewController,
282
+ options: NSDictionary,
283
+ labels: [String],
284
+ cancelButtonIndex: Int,
285
+ destructiveIndices: Set<Int>,
286
+ disabledIndices: Set<Int>,
287
+ preferredIndex: Int?,
288
+ tintColor: UIColor?,
289
+ cancelButtonTintColor: UIColor?,
290
+ destructiveColor: UIColor?,
291
+ completion: @escaping (Int) -> Void
292
+ ) {
293
+ let rows = labels.enumerated().map { index, label in
294
+ BottomSheetViewController.Row(
295
+ index: index,
296
+ label: label,
297
+ isDestructive: destructiveIndices.contains(index),
298
+ isEnabled: !disabledIndices.contains(index),
299
+ isPreferred: index == preferredIndex
300
+ )
301
+ }
302
+
303
+ let sheet = BottomSheetViewController(
304
+ title: Self.text(options["title"]),
305
+ message: Self.text(options["message"]),
306
+ rows: rows.filter { $0.index != cancelButtonIndex },
307
+ cancelRow: rows.first { $0.index == cancelButtonIndex },
308
+ tintColor: tintColor,
309
+ cancelButtonTintColor: cancelButtonTintColor,
310
+ destructiveColor: destructiveColor
311
+ )
312
+
313
+ let presentation = Presentation(
314
+ controller: sheet,
315
+ cancelButtonIndex: cancelButtonIndex,
316
+ completion: { index, _, _ in completion(index) }
317
+ )
318
+
319
+ // Weak: the presentation holds the sheet, which holds this closure.
320
+ sheet.onSelect = { [weak self, weak presentation] index in
321
+ guard let presentation else { return }
322
+
323
+ // Unlike an alert action, a row does not dismiss the sheet itself.
324
+ // Resolve once it is gone, as an alert does.
325
+ presentation.controller.dismiss(animated: true) {
326
+ self?.finish(presentation, index: index)
327
+ }
328
+ }
329
+
330
+ switch options["userInterfaceStyle"] as? String {
331
+ case "dark": sheet.overrideUserInterfaceStyle = .dark
332
+ case "light": sheet.overrideUserInterfaceStyle = .light
333
+ default: sheet.overrideUserInterfaceStyle = .unspecified
334
+ }
335
+ sheet.view.tintColor = tintColor
336
+
337
+ if let controller = sheet.sheetPresentationController {
338
+ controller.prefersGrabberVisible = true
339
+ controller.prefersEdgeAttachedInCompactHeight = true
340
+ // Swipe down resolves through presentationControllerDidDismiss.
341
+ controller.delegate = self
342
+
343
+ let bounds = parent.view.window?.bounds ?? parent.view.bounds
344
+ let fitting = sheet.fittingHeight(width: bounds.width)
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]
366
+ } else {
367
+ controller.detents = [.medium(), .large()]
368
+ }
369
+ }
370
+
371
+ presentations.append(presentation)
372
+ parent.present(sheet, animated: true)
373
+ }
374
+
375
+ /// Interactive dismissal of a sheet: a swipe down or a tap on the dimmed
376
+ /// area. It resolves the cancel index, or -1 without one, like Android. A
377
+ /// programmatic dismiss() does not call this.
378
+ public func presentationControllerDidDismiss(
379
+ _ presentationController: UIPresentationController
380
+ ) {
381
+ guard
382
+ let presentation = presentations.first(where: {
383
+ $0.controller === presentationController.presentedViewController
384
+ })
385
+ else { return }
386
+
387
+ finish(presentation, index: presentation.cancelButtonIndex)
388
+ }
389
+
194
390
  @objc public func dismissAll() {
195
391
  guard let bottom = presentations.first else { return }
196
392
 
@@ -211,6 +407,21 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
211
407
  }
212
408
  }
213
409
 
410
+ /// The JS runtime is going away: close every sheet, prompt and alert, but
411
+ /// resolve none of them. Their promises belong to the runtime being torn
412
+ /// down, and the list is shared, so stale entries must not outlive it.
413
+ @objc public func invalidate() {
414
+ let all = presentations
415
+ presentations.removeAll()
416
+ all.forEach { $0.discard() }
417
+
418
+ // As in dismissAll(): the lowest sheet's presenter dismisses it and
419
+ // everything stacked above it.
420
+ all.first { $0.controller.presentingViewController != nil }?
421
+ .controller.presentingViewController?
422
+ .dismiss(animated: false)
423
+ }
424
+
214
425
  @objc public func dismiss() {
215
426
  guard let presentation = presentations.last else { return }
216
427
 
@@ -224,10 +435,8 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
224
435
  public func popoverPresentationControllerDidDismissPopover(
225
436
  _ popoverPresentationController: UIPopoverPresentationController
226
437
  ) {
227
- guard
228
- let alert = popoverPresentationController.presentedViewController
229
- as? UIAlertController,
230
- let presentation = presentations.first(where: { $0.controller === alert })
438
+ let alert = popoverPresentationController.presentedViewController
439
+ guard let presentation = presentations.first(where: { $0.controller === alert })
231
440
  else { return }
232
441
 
233
442
  finish(presentation, index: presentation.cancelButtonIndex)
@@ -240,31 +449,59 @@ public class UnifiedActionSheetImpl: NSObject, UIPopoverPresentationControllerDe
240
449
  presentation.resolve(index)
241
450
  }
242
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
+
243
473
  private final class Presentation {
244
- let controller: UIAlertController
474
+ /// A UIAlertController, or the bottom sheet on iPhone.
475
+ let controller: UIViewController
245
476
  let cancelButtonIndex: Int
246
- /// Read at resolve time, not at creation: the value that matters is
247
- /// whatever is in the field when the prompt closes. Sheets pass a constant.
248
- private let currentText: () -> String
249
- 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)?
250
481
 
251
482
  init(
252
- controller: UIAlertController,
483
+ controller: UIViewController,
253
484
  cancelButtonIndex: Int,
254
- currentText: @escaping () -> String = { "" },
255
- completion: @escaping (Int, String) -> Void
485
+ currentValues: @escaping () -> (String, String) = { ("", "") },
486
+ completion: @escaping (Int, String, String) -> Void
256
487
  ) {
257
488
  self.controller = controller
258
489
  self.cancelButtonIndex = cancelButtonIndex
259
- self.currentText = currentText
490
+ self.currentValues = currentValues
260
491
  self.completion = completion
261
492
  }
262
493
 
494
+ /// Drops the completion without calling it.
495
+ func discard() {
496
+ completion = nil
497
+ }
498
+
263
499
  func resolve(_ index: Int) {
264
500
  guard let completion else { return }
265
501
 
266
502
  self.completion = nil
267
- completion(index, currentText())
503
+ let (text, password) = currentValues()
504
+ completion(index, text, password)
268
505
  }
269
506
  }
270
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,GA6CtDC,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":[]}