From 3e6e4f2ee35c00d79814f57f3d3a3d68d7ae268c Mon Sep 17 00:00:00 2001
From: Sage Abdullah <sage.abdullah@torchbox.com>
Date: Mon, 1 Jul 2024 14:13:56 +0100
Subject: [PATCH] Add confirm() method to DialogController

Like hide(), but dispatches an event to indicate that the dialog was 'confirmed'
---
 .../src/controllers/DialogController.test.js  | 64 +++++++++++++++++++
 client/src/controllers/DialogController.ts    | 19 ++++--
 2 files changed, 79 insertions(+), 4 deletions(-)

diff --git a/client/src/controllers/DialogController.test.js b/client/src/controllers/DialogController.test.js
index 1e41bfc848..6b71a08ba3 100644
--- a/client/src/controllers/DialogController.test.js
+++ b/client/src/controllers/DialogController.test.js
@@ -103,6 +103,70 @@ describe('DialogController', () => {
       expect(document.documentElement.style.overflowY).toBe('');
     });
 
+    it('should support the ability to confirm the dialog with an event to indicate the confirmation', async () => {
+      const hiddenListener = jest.fn();
+      document.addEventListener('w-dialog:hidden', hiddenListener);
+
+      const confirmedListener = jest.fn();
+      document.addEventListener('w-dialog:confirmed', confirmedListener);
+
+      // Add a confirm button to the dialog
+      const dialogBody = document.getElementById('dialog-body');
+      const confirmButton = document.createElement('button');
+      confirmButton.type = 'button';
+      confirmButton.setAttribute('data-action', 'w-dialog#confirm');
+      dialogBody.appendChild(confirmButton);
+
+      application.start();
+
+      await Promise.resolve();
+
+      expect(hiddenListener).not.toHaveBeenCalled();
+      expect(confirmedListener).not.toHaveBeenCalled();
+
+      const dialog = document.getElementById('dialog-container');
+
+      // closed by default
+      expect(dialog.getAttribute('aria-hidden')).toEqual('true');
+      expect(document.documentElement.style.overflowY).toBe('');
+
+      // show the dialog manually
+      dialog.dispatchEvent(new CustomEvent('w-dialog:show'));
+
+      expect(dialog.getAttribute('aria-hidden')).toEqual(null);
+      expect(hiddenListener).not.toHaveBeenCalled();
+      expect(confirmedListener).not.toHaveBeenCalled();
+      // add style to root element on shown by default
+      expect(document.documentElement.style.overflowY).toBe('hidden');
+
+      // hide the dialog using the confirm button
+      confirmButton.click();
+
+      expect(dialog.getAttribute('aria-hidden')).toEqual('true');
+
+      // w-dialog:hide event should still be dispatched
+      expect(hiddenListener).toHaveBeenCalledWith(
+        expect.objectContaining({
+          detail: expect.objectContaining({
+            body: dialogBody,
+            dialog: expect.any(Object),
+          }),
+        }),
+      );
+      // reset style on root element when hidden by default
+      expect(document.documentElement.style.overflowY).toBe('');
+
+      // w-dialog:confirmed event should be dispatched
+      expect(confirmedListener).toHaveBeenCalledWith(
+        expect.objectContaining({
+          detail: expect.objectContaining({
+            body: dialogBody,
+            dialog: expect.any(Object),
+          }),
+        }),
+      );
+    });
+
     it('should support the ability use a theme to avoid document style change', async () => {
       const dialog = document.getElementById('dialog-container');
 
diff --git a/client/src/controllers/DialogController.ts b/client/src/controllers/DialogController.ts
index 51dcba7b4b..a60d10369f 100644
--- a/client/src/controllers/DialogController.ts
+++ b/client/src/controllers/DialogController.ts
@@ -29,14 +29,17 @@ export class DialogController extends Controller<HTMLElement> {
   /** Optional targets that will be dispatched events for key dialog events. */
   declare readonly notifyTargets: HTMLElement[];
 
+  get eventDetail() {
+    return { body: this.bodyTarget, dialog: this.dialog };
+  }
+
   connect() {
     this.dialog = new A11yDialog(this.element);
-    const detail = { body: this.bodyTarget, dialog: this.dialog };
     const isFloating = this.themeValue === FLOATING;
     this.dialog
       .on('show', () => {
         if (!isFloating) document.documentElement.style.overflowY = 'hidden';
-        this.dispatch('shown', { detail, cancelable: false });
+        this.dispatch('shown', { detail: this.eventDetail, cancelable: false });
         this.notifyTargets.forEach((target) => {
           this.dispatch('shown', {
             target,
@@ -47,7 +50,10 @@ export class DialogController extends Controller<HTMLElement> {
       })
       .on('hide', () => {
         if (!isFloating) document.documentElement.style.overflowY = '';
-        this.dispatch('hidden', { detail, cancelable: false });
+        this.dispatch('hidden', {
+          detail: this.eventDetail,
+          cancelable: false,
+        });
         this.notifyTargets.forEach((target) => {
           this.dispatch('hidden', {
             target,
@@ -56,7 +62,7 @@ export class DialogController extends Controller<HTMLElement> {
           });
         });
       });
-    this.dispatch('ready', { detail });
+    this.dispatch('ready', { detail: this.eventDetail });
     if (this.notifyTargets && Array.isArray(this.notifyTargets)) {
       this.notifyTargets.forEach((target) => {
         this.dispatch('ready', { target, bubbles: false, cancelable: false });
@@ -72,4 +78,9 @@ export class DialogController extends Controller<HTMLElement> {
   show() {
     this.dialog.show();
   }
+
+  confirm() {
+    this.hide();
+    this.dispatch('confirmed', { detail: this.eventDetail, cancelable: false });
+  }
 }