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, + }); + }); + } }); } }