Skip to content

Commit 251c62c

Browse files
feat(action-sheet): Add cancelable input option and and canceled output parameter (#2285)
1 parent 2788f81 commit 251c62c

File tree

6 files changed

+124
-18
lines changed

6 files changed

+124
-18
lines changed

action-sheet/README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,20 @@ to select.
8484

8585
#### ShowActionsResult
8686

87-
| Prop | Type | Description | Since |
88-
| ----------- | ------------------- | -------------------------------------------- | ----- |
89-
| **`index`** | <code>number</code> | The index of the clicked option (Zero-based) | 1.0.0 |
87+
| Prop | Type | Description | Since |
88+
| -------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
89+
| **`index`** | <code>number</code> | The index of the clicked option (Zero-based), or -1 if the sheet was canceled. On iOS, if there is a button with <a href="#actionsheetbuttonstyle">ActionSheetButtonStyle.Cancel</a>, and user clicks outside the sheet, the index of the cancel option is returned | 1.0.0 |
90+
| **`canceled`** | <code>boolean</code> | True if sheet was canceled by user; False otherwise On Web, requires having @ionic/pwa-elements version 3.4.0 or higher. | 8.1.0 |
9091

9192

9293
#### ShowActionsOptions
9394

94-
| Prop | Type | Description | Since |
95-
| ------------- | -------------------------------- | ------------------------------------------------------------------------ | ----- |
96-
| **`title`** | <code>string</code> | The title of the Action Sheet. | 1.0.0 |
97-
| **`message`** | <code>string</code> | A message to show under the title. This option is only supported on iOS. | 1.0.0 |
98-
| **`options`** | <code>ActionSheetButton[]</code> | Options the user can choose from. | 1.0.0 |
95+
| Prop | Type | Description | Since |
96+
| ---------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
97+
| **`title`** | <code>string</code> | The title of the Action Sheet. | 1.0.0 |
98+
| **`message`** | <code>string</code> | A message to show under the title. This option is only supported on iOS. | 1.0.0 |
99+
| **`options`** | <code>ActionSheetButton[]</code> | Options the user can choose from. | 1.0.0 |
100+
| **`cancelable`** | <code>boolean</code> | If true, sheet is canceled when clicked outside; If false, it is not. By default, false. Not available on iOS, sheet is always cancelable by clicking outside of it. On Web, requires having @ionic/pwa-elements version 3.4.0 or higher. | 8.1.0 |
99101

100102

101103
#### ActionSheetButton

action-sheet/android/src/main/java/com/capacitorjs/plugins/actionsheet/ActionSheet.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ public interface OnCancelListener {
2626
@Override
2727
public void onCancel(DialogInterface dialog) {
2828
super.onCancel(dialog);
29-
this.cancelListener.onCancel();
29+
if (this.cancelListener != null) {
30+
this.cancelListener.onCancel();
31+
}
3032
}
3133

3234
private String title;

action-sheet/android/src/main/java/com/capacitorjs/plugins/actionsheet/ActionSheetPlugin.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class ActionSheetPlugin extends Plugin {
1919
@PluginMethod
2020
public void showActions(final PluginCall call) {
2121
String title = call.getString("title");
22+
boolean cancelable = Boolean.TRUE.equals(call.getBoolean("cancelable", false));
2223
JSArray options = call.getArray("options");
2324
if (options == null) {
2425
call.reject("Must supply options");
@@ -39,7 +40,10 @@ public void showActions(final PluginCall call) {
3940
}
4041
implementation.setTitle(title);
4142
implementation.setOptions(actionOptions);
42-
implementation.setCancelable(false);
43+
implementation.setCancelable(cancelable);
44+
if (cancelable) {
45+
implementation.setOnCancelListener(() -> resolve(call, -1));
46+
}
4347
implementation.setOnSelectedListener((index) -> {
4448
JSObject ret = new JSObject();
4549
ret.put("index", index);
@@ -52,4 +56,12 @@ public void showActions(final PluginCall call) {
5256
call.reject("JSON error processing an option for showActions", ex);
5357
}
5458
}
59+
60+
private void resolve(final PluginCall call, int selectedIndex) {
61+
JSObject ret = new JSObject();
62+
ret.put("index", selectedIndex);
63+
ret.put("canceled", selectedIndex < 0);
64+
call.resolve(ret);
65+
implementation.dismiss();
66+
}
5567
}

action-sheet/ios/Sources/ActionSheetPlugin/ActionSheetPlugin.swift

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,103 @@ import Capacitor
66
* here: https://capacitorjs.com/docs/plugins/ios
77
*/
88
@objc(ActionSheetPlugin)
9-
public class ActionSheetPlugin: CAPPlugin, CAPBridgedPlugin {
9+
public class ActionSheetPlugin: CAPPlugin, CAPBridgedPlugin, UIAdaptivePresentationControllerDelegate {
1010
public let identifier = "ActionSheetPlugin"
1111
public let jsName = "ActionSheet"
1212
public let pluginMethods: [CAPPluginMethod] = [
1313
CAPPluginMethod(name: "showActions", returnType: CAPPluginReturnPromise)
1414
]
1515
private let implementation = ActionSheet()
16+
private var currentCall: CAPPluginCall?
1617

1718
@objc func showActions(_ call: CAPPluginCall) {
1819
let title = call.options["title"] as? String
1920
let message = call.options["message"] as? String
2021

2122
let options = call.getArray("options", JSObject.self) ?? []
2223
var alertActions = [UIAlertAction]()
24+
var hasCancellableButton = false
2325
for (index, option) in options.enumerated() {
2426
let style = option["style"] as? String ?? "DEFAULT"
2527
let title = option["title"] as? String ?? ""
2628
var buttonStyle: UIAlertAction.Style = .default
2729
if style == "DESTRUCTIVE" {
2830
buttonStyle = .destructive
2931
} else if style == "CANCEL" {
32+
hasCancellableButton = true
3033
buttonStyle = .cancel
3134
}
32-
let action = UIAlertAction(title: title, style: buttonStyle, handler: { (_) in
33-
call.resolve([
34-
"index": index
35-
])
35+
let action = UIAlertAction(title: title, style: buttonStyle, handler: { [weak self] (_) in
36+
if buttonStyle == .cancel {
37+
call.actionSheetCanceled()
38+
} else {
39+
call.resolve([
40+
"index": index,
41+
"canceled": false
42+
])
43+
}
44+
self?.currentCall = nil
3645
})
3746
alertActions.append(action)
3847
}
3948

4049
DispatchQueue.main.async { [weak self] in
4150
if let alertController = self?.implementation.buildActionSheet(title: title, message: message, actions: alertActions) {
4251
self?.setCenteredPopover(alertController)
43-
self?.bridge?.viewController?.present(alertController, animated: true, completion: nil)
52+
self?.bridge?.viewController?.present(alertController, animated: true) {
53+
if !hasCancellableButton {
54+
self?.setupCancelationListerners(alertController, call)
55+
}
56+
}
4457
}
4558
}
4659
}
4760

61+
private func setupCancelationListerners(_ alertController: UIAlertController, _ call: CAPPluginCall) {
62+
if #available(iOS 26, *) {
63+
self.currentCall = call
64+
alertController.presentationController?.delegate = self
65+
} else {
66+
// For iOS versions below 26, setting the presentation controller delegate would result in a crash
67+
// "Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The presentation controller of an alert controller presenting as an alert must not have its delegate modified"
68+
// Hence, the alternative by adding a gesture recognizer (which only works for iOS versions below 26)
69+
let gestureRecognizer = TapGestureRecognizerWithClosure {
70+
alertController.dismiss(animated: true, completion: nil)
71+
call.actionSheetCanceled()
72+
}
73+
let backroundView = alertController.view.superview?.subviews[0]
74+
backroundView?.addGestureRecognizer(gestureRecognizer)
75+
}
76+
}
77+
78+
// MARK: - UIAdaptivePresentationControllerDelegate
79+
80+
public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
81+
self.currentCall?.actionSheetCanceled()
82+
self.currentCall = nil
83+
}
84+
}
85+
86+
// MARK: - TapGestureRecognizerWithClosure
87+
private final class TapGestureRecognizerWithClosure: UITapGestureRecognizer {
88+
private let onTap: () -> Void
89+
90+
init(onTap: @escaping () -> Void) {
91+
self.onTap = onTap
92+
super.init(target: nil, action: nil)
93+
self.addTarget(self, action: #selector(action))
94+
}
95+
96+
@objc private func action() {
97+
onTap()
98+
}
99+
}
100+
101+
private extension CAPPluginCall {
102+
func actionSheetCanceled() {
103+
resolve([
104+
"index": -1,
105+
"canceled": true
106+
])
107+
}
48108
}

action-sheet/src/definitions.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ export interface ShowActionsOptions {
2121
* @since 1.0.0
2222
*/
2323
options: ActionSheetButton[];
24+
25+
/**
26+
* If true, sheet is canceled when clicked outside; If false, it is not. By default, false.
27+
*
28+
* Not available on iOS, sheet is always cancelable by clicking outside of it.
29+
*
30+
* On Web, requires having @ionic/pwa-elements version 3.4.0 or higher.
31+
*
32+
* @since 8.1.0
33+
*/
34+
cancelable?: boolean;
2435
}
2536

2637
export enum ActionSheetButtonStyle {
@@ -76,11 +87,21 @@ export interface ActionSheetButton {
7687

7788
export interface ShowActionsResult {
7889
/**
79-
* The index of the clicked option (Zero-based)
90+
* The index of the clicked option (Zero-based), or -1 if the sheet was canceled.
91+
*
92+
* On iOS, if there is a button with ActionSheetButtonStyle.Cancel, and user clicks outside the sheet, the index of the cancel option is returned
8093
*
8194
* @since 1.0.0
8295
*/
8396
index: number;
97+
/**
98+
* True if sheet was canceled by user; False otherwise
99+
*
100+
* On Web, requires having @ionic/pwa-elements version 3.4.0 or higher.
101+
*
102+
* @since 8.1.0
103+
*/
104+
canceled: boolean;
84105
}
85106

86107
export interface ActionSheetPlugin {

action-sheet/src/web.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,23 @@ export class ActionSheetWeb extends WebPlugin implements ActionSheetPlugin {
1111
document.body.appendChild(actionSheet);
1212
}
1313
actionSheet.header = options.title;
14-
actionSheet.cancelable = false;
14+
actionSheet.cancelable = options.cancelable;
1515
actionSheet.options = options.options;
1616
actionSheet.addEventListener('onSelection', async (e: any) => {
1717
const selection = e.detail;
1818
resolve({
1919
index: selection,
20+
canceled: false,
2021
});
2122
});
23+
if (options.cancelable) {
24+
actionSheet.addEventListener('onCanceled', async () => {
25+
resolve({
26+
index: -1,
27+
canceled: true,
28+
});
29+
});
30+
}
2231
});
2332
}
2433
}

0 commit comments

Comments
 (0)