diff --git a/action-sheet/README.md b/action-sheet/README.md
index 8f8656ce3..ea657acd4 100644
--- a/action-sheet/README.md
+++ b/action-sheet/README.md
@@ -84,18 +84,20 @@ to select.
#### ShowActionsResult
-| Prop | Type | Description | Since |
-| ----------- | ------------------- | -------------------------------------------- | ----- |
-| **`index`** | number | The index of the clicked option (Zero-based) | 1.0.0 |
+| Prop | Type | Description | Since |
+| -------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
+| **`index`** | number | The index of the clicked option (Zero-based), or -1 if the sheet was canceled. On iOS, if there is a button with ActionSheetButtonStyle.Cancel, and user clicks outside the sheet, the index of the cancel option is returned | 1.0.0 |
+| **`canceled`** | boolean | 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 |
#### ShowActionsOptions
-| Prop | Type | Description | Since |
-| ------------- | -------------------------------- | ------------------------------------------------------------------------ | ----- |
-| **`title`** | string | The title of the Action Sheet. | 1.0.0 |
-| **`message`** | string | A message to show under the title. This option is only supported on iOS. | 1.0.0 |
-| **`options`** | ActionSheetButton[] | Options the user can choose from. | 1.0.0 |
+| Prop | Type | Description | Since |
+| ---------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
+| **`title`** | string | The title of the Action Sheet. | 1.0.0 |
+| **`message`** | string | A message to show under the title. This option is only supported on iOS. | 1.0.0 |
+| **`options`** | ActionSheetButton[] | Options the user can choose from. | 1.0.0 |
+| **`cancelable`** | boolean | 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 |
#### ActionSheetButton
diff --git a/action-sheet/android/src/main/java/com/capacitorjs/plugins/actionsheet/ActionSheet.java b/action-sheet/android/src/main/java/com/capacitorjs/plugins/actionsheet/ActionSheet.java
index e83b3414e..4e5da7f52 100644
--- a/action-sheet/android/src/main/java/com/capacitorjs/plugins/actionsheet/ActionSheet.java
+++ b/action-sheet/android/src/main/java/com/capacitorjs/plugins/actionsheet/ActionSheet.java
@@ -26,7 +26,9 @@ public interface OnCancelListener {
@Override
public void onCancel(DialogInterface dialog) {
super.onCancel(dialog);
- this.cancelListener.onCancel();
+ if (this.cancelListener != null) {
+ this.cancelListener.onCancel();
+ }
}
private String title;
diff --git a/action-sheet/android/src/main/java/com/capacitorjs/plugins/actionsheet/ActionSheetPlugin.java b/action-sheet/android/src/main/java/com/capacitorjs/plugins/actionsheet/ActionSheetPlugin.java
index 10c58e819..808f0af6d 100644
--- a/action-sheet/android/src/main/java/com/capacitorjs/plugins/actionsheet/ActionSheetPlugin.java
+++ b/action-sheet/android/src/main/java/com/capacitorjs/plugins/actionsheet/ActionSheetPlugin.java
@@ -19,6 +19,7 @@ public class ActionSheetPlugin extends Plugin {
@PluginMethod
public void showActions(final PluginCall call) {
String title = call.getString("title");
+ boolean cancelable = Boolean.TRUE.equals(call.getBoolean("cancelable", false));
JSArray options = call.getArray("options");
if (options == null) {
call.reject("Must supply options");
@@ -39,7 +40,10 @@ public void showActions(final PluginCall call) {
}
implementation.setTitle(title);
implementation.setOptions(actionOptions);
- implementation.setCancelable(false);
+ implementation.setCancelable(cancelable);
+ if (cancelable) {
+ implementation.setOnCancelListener(() -> resolve(call, -1));
+ }
implementation.setOnSelectedListener((index) -> {
JSObject ret = new JSObject();
ret.put("index", index);
@@ -52,4 +56,12 @@ public void showActions(final PluginCall call) {
call.reject("JSON error processing an option for showActions", ex);
}
}
+
+ private void resolve(final PluginCall call, int selectedIndex) {
+ JSObject ret = new JSObject();
+ ret.put("index", selectedIndex);
+ ret.put("canceled", selectedIndex < 0);
+ call.resolve(ret);
+ implementation.dismiss();
+ }
}
diff --git a/action-sheet/ios/Sources/ActionSheetPlugin/ActionSheetPlugin.swift b/action-sheet/ios/Sources/ActionSheetPlugin/ActionSheetPlugin.swift
index 7a4d84ce4..b4a2b83ff 100644
--- a/action-sheet/ios/Sources/ActionSheetPlugin/ActionSheetPlugin.swift
+++ b/action-sheet/ios/Sources/ActionSheetPlugin/ActionSheetPlugin.swift
@@ -6,13 +6,14 @@ import Capacitor
* here: https://capacitorjs.com/docs/plugins/ios
*/
@objc(ActionSheetPlugin)
-public class ActionSheetPlugin: CAPPlugin, CAPBridgedPlugin {
+public class ActionSheetPlugin: CAPPlugin, CAPBridgedPlugin, UIAdaptivePresentationControllerDelegate {
public let identifier = "ActionSheetPlugin"
public let jsName = "ActionSheet"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "showActions", returnType: CAPPluginReturnPromise)
]
private let implementation = ActionSheet()
+ private var currentCall: CAPPluginCall?
@objc func showActions(_ call: CAPPluginCall) {
let title = call.options["title"] as? String
@@ -20,6 +21,7 @@ public class ActionSheetPlugin: CAPPlugin, CAPBridgedPlugin {
let options = call.getArray("options", JSObject.self) ?? []
var alertActions = [UIAlertAction]()
+ var hasCancellableButton = false
for (index, option) in options.enumerated() {
let style = option["style"] as? String ?? "DEFAULT"
let title = option["title"] as? String ?? ""
@@ -27,12 +29,19 @@ public class ActionSheetPlugin: CAPPlugin, CAPBridgedPlugin {
if style == "DESTRUCTIVE" {
buttonStyle = .destructive
} else if style == "CANCEL" {
+ hasCancellableButton = true
buttonStyle = .cancel
}
- let action = UIAlertAction(title: title, style: buttonStyle, handler: { (_) in
- call.resolve([
- "index": index
- ])
+ let action = UIAlertAction(title: title, style: buttonStyle, handler: { [weak self] (_) in
+ if buttonStyle == .cancel {
+ call.actionSheetCanceled()
+ } else {
+ call.resolve([
+ "index": index,
+ "canceled": false
+ ])
+ }
+ self?.currentCall = nil
})
alertActions.append(action)
}
@@ -40,9 +49,60 @@ public class ActionSheetPlugin: CAPPlugin, CAPBridgedPlugin {
DispatchQueue.main.async { [weak self] in
if let alertController = self?.implementation.buildActionSheet(title: title, message: message, actions: alertActions) {
self?.setCenteredPopover(alertController)
- self?.bridge?.viewController?.present(alertController, animated: true, completion: nil)
+ self?.bridge?.viewController?.present(alertController, animated: true) {
+ if !hasCancellableButton {
+ self?.setupCancelationListerners(alertController, call)
+ }
+ }
}
}
}
+ private func setupCancelationListerners(_ alertController: UIAlertController, _ call: CAPPluginCall) {
+ if #available(iOS 26, *) {
+ self.currentCall = call
+ alertController.presentationController?.delegate = self
+ } else {
+ // For iOS versions below 26, setting the presentation controller delegate would result in a crash
+ // "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"
+ // Hence, the alternative by adding a gesture recognizer (which only works for iOS versions below 26)
+ let gestureRecognizer = TapGestureRecognizerWithClosure {
+ alertController.dismiss(animated: true, completion: nil)
+ call.actionSheetCanceled()
+ }
+ let backroundView = alertController.view.superview?.subviews[0]
+ backroundView?.addGestureRecognizer(gestureRecognizer)
+ }
+ }
+
+ // MARK: - UIAdaptivePresentationControllerDelegate
+
+ public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
+ self.currentCall?.actionSheetCanceled()
+ self.currentCall = nil
+ }
+}
+
+// MARK: - TapGestureRecognizerWithClosure
+private final class TapGestureRecognizerWithClosure: UITapGestureRecognizer {
+ private let onTap: () -> Void
+
+ init(onTap: @escaping () -> Void) {
+ self.onTap = onTap
+ super.init(target: nil, action: nil)
+ self.addTarget(self, action: #selector(action))
+ }
+
+ @objc private func action() {
+ onTap()
+ }
+}
+
+private extension CAPPluginCall {
+ func actionSheetCanceled() {
+ resolve([
+ "index": -1,
+ "canceled": true
+ ])
+ }
}
diff --git a/action-sheet/src/definitions.ts b/action-sheet/src/definitions.ts
index 84dbfbaae..09b0470e2 100644
--- a/action-sheet/src/definitions.ts
+++ b/action-sheet/src/definitions.ts
@@ -21,6 +21,17 @@ export interface ShowActionsOptions {
* @since 1.0.0
*/
options: ActionSheetButton[];
+
+ /**
+ * 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.
+ *
+ * @since 8.1.0
+ */
+ cancelable?: boolean;
}
export enum ActionSheetButtonStyle {
@@ -76,11 +87,21 @@ export interface ActionSheetButton {
export interface ShowActionsResult {
/**
- * The index of the clicked option (Zero-based)
+ * The index of the clicked option (Zero-based), or -1 if the sheet was canceled.
+ *
+ * On iOS, if there is a button with ActionSheetButtonStyle.Cancel, and user clicks outside the sheet, the index of the cancel option is returned
*
* @since 1.0.0
*/
index: number;
+ /**
+ * True if sheet was canceled by user; False otherwise
+ *
+ * On Web, requires having @ionic/pwa-elements version 3.4.0 or higher.
+ *
+ * @since 8.1.0
+ */
+ canceled: boolean;
}
export interface ActionSheetPlugin {
diff --git a/action-sheet/src/web.ts b/action-sheet/src/web.ts
index 4859264c8..fbb85264e 100644
--- a/action-sheet/src/web.ts
+++ b/action-sheet/src/web.ts
@@ -11,14 +11,23 @@ export class ActionSheetWeb extends WebPlugin implements ActionSheetPlugin {
document.body.appendChild(actionSheet);
}
actionSheet.header = options.title;
- actionSheet.cancelable = false;
+ actionSheet.cancelable = options.cancelable;
actionSheet.options = options.options;
actionSheet.addEventListener('onSelection', async (e: any) => {
const selection = e.detail;
resolve({
index: selection,
+ canceled: false,
});
});
+ if (options.cancelable) {
+ actionSheet.addEventListener('onCanceled', async () => {
+ resolve({
+ index: -1,
+ canceled: true,
+ });
+ });
+ }
});
}
}