From 66c3914d39bf24d828ec8516389d7cadad52294e Mon Sep 17 00:00:00 2001
From: nod_ <nod_@598310.no-reply.drupal.org>
Date: Wed, 17 Apr 2024 17:12:06 +0200
Subject: [PATCH] Issue #3296098 by catch, finnsky, smustgrave: Removal
 :tabbable usage in dialog.js

---
 .../Drupal/Core/Ajax/OpenDialogCommand.php    |  6 ++
 .../Core/Ajax/OpenOffCanvasDialogCommand.php  | 19 ++++-
 core/misc/dialog/dialog.jquery-ui.js          | 76 +++++++++++++++++++
 core/misc/dialog/dialog.js                    |  2 -
 .../modules/ckeditor5/ckeditor5.ckeditor5.yml |  3 +-
 core/modules/editor/js/editor.js              |  4 +-
 .../src/MediaLibraryUiBuilder.php             |  4 +-
 .../src/Controller/TestController.php         |  4 +-
 .../Functional/Ajax/OffCanvasDialogTest.php   |  6 +-
 .../src/Controller/TestController.php         |  4 +-
 .../views_ui/src/Form/Ajax/ViewsFormBase.php  |  4 +-
 .../Ajax/OpenOffCanvasDialogCommandTest.php   |  6 +-
 12 files changed, 123 insertions(+), 15 deletions(-)

diff --git a/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php
index a15eaa3211f8..c9ba3e7bb1e4 100644
--- a/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php
+++ b/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php
@@ -76,6 +76,12 @@ public function __construct($selector, string|\Stringable|null $title, $content,
     $title = PlainTextOutput::renderFromHtml($title);
 
     $dialog_options += ['title' => $title];
+    if (isset($dialog_options['dialogClass'])) {
+      @trigger_error('Passing $dialog_options[\'dialogClass\'] to OpenDialogCommand::__construct() is deprecated in drupal:10.3.0 and will be removed in drupal:12.0.0. Use $dialog_options[\'classes\'] instead. See https://www.drupal.org/node/3440844', E_USER_DEPRECATED);
+      $dialog_options['classes']['ui-dialog'] = $dialog_options['dialogClass'];
+      unset($dialog_options['dialogClass']);
+    }
+
     $this->selector = $selector;
     $this->content = $content;
     $this->dialogOptions = $dialog_options;
diff --git a/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php
index 1144a189d774..a7aefb00664b 100644
--- a/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php
+++ b/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php
@@ -38,6 +38,21 @@ class OpenOffCanvasDialogCommand extends OpenDialogCommand {
    *   (optional) The position to render the off-canvas dialog.
    */
   public function __construct(string|\Stringable|null $title, $content, array $dialog_options = [], $settings = NULL, $position = 'side') {
+    $dialog_class = FALSE;
+    if (isset($dialog_options['classes']['ui-dialog'])) {
+      $dialog_class = $dialog_options['classes']['ui-dialog'];
+    }
+    elseif (isset($dialog_options['dialogClass'])) {
+      @trigger_error('Passing $dialog_options[\'dialogClass\'] to OpenOffCanvasDialogCommand::__construct() is deprecated in drupal:10.3.0 and will be removed in drupal:12.0.0. Use $dialog_options[\'classes\'] instead. See https://www.drupal.org/node/3440844', E_USER_DEPRECATED);
+      $dialog_class = $dialog_options['dialogClass'];
+      unset($dialog_options['dialogClass']);
+    }
+    if ($dialog_class) {
+      $dialog_options['classes']['ui-dialog'] = $dialog_class . ' ' . "ui-dialog-off-canvas ui-dialog-position-$position";
+    }
+    else {
+      $dialog_options['classes']['ui-dialog'] = "ui-dialog-off-canvas ui-dialog-position-$position";
+    }
     parent::__construct('#drupal-off-canvas', $title, $content, $dialog_options, $settings);
     $this->dialogOptions['modal'] = FALSE;
     $this->dialogOptions['autoResize'] = FALSE;
@@ -45,9 +60,7 @@ public function __construct(string|\Stringable|null $title, $content, array $dia
     $this->dialogOptions['draggable'] = FALSE;
     $this->dialogOptions['drupalAutoButtons'] = FALSE;
     $this->dialogOptions['drupalOffCanvasPosition'] = $position;
-    if (empty($dialog_options['dialogClass'])) {
-      $this->dialogOptions['dialogClass'] = "ui-dialog-off-canvas ui-dialog-position-$position";
-    }
+
     // Add CSS class to #drupal-off-canvas element. This enables developers to
     // select previous versions of off-canvas styles by using custom selector:
     // #drupal-off-canvas:not(.drupal-off-canvas-reset).
diff --git a/core/misc/dialog/dialog.jquery-ui.js b/core/misc/dialog/dialog.jquery-ui.js
index 783797d1a148..32367977d8c8 100644
--- a/core/misc/dialog/dialog.jquery-ui.js
+++ b/core/misc/dialog/dialog.jquery-ui.js
@@ -30,6 +30,82 @@
         $buttons.eq(index).addClass(opts.buttonPrimaryClass);
       }
     },
+    _createWrapper() {
+      this.uiDialog = $('<div>')
+        .hide()
+        .attr({
+          // Setting tabIndex makes the div focusable
+          tabIndex: -1,
+          role: 'dialog',
+        })
+        .appendTo(this._appendTo());
+
+      this._addClass(
+        this.uiDialog,
+        'ui-dialog',
+        'ui-widget ui-widget-content ui-front',
+      );
+      this._on(this.uiDialog, {
+        keydown(event) {
+          if (
+            this.options.closeOnEscape &&
+            !event.isDefaultPrevented() &&
+            event.keyCode &&
+            event.keyCode === $.ui.keyCode.ESCAPE
+          ) {
+            event.preventDefault();
+            this.close(event);
+            return;
+          }
+
+          // Prevent tabbing out of dialogs
+          if (
+            event.keyCode !== $.ui.keyCode.TAB ||
+            event.isDefaultPrevented()
+          ) {
+            return;
+          }
+
+          const tabbableElements = tabbable(this.uiDialog[0]);
+          if (tabbableElements.length) {
+            const first = tabbableElements[0];
+            const last = tabbableElements[tabbableElements.length - 1];
+
+            if (
+              (event.target === last || event.target === this.uiDialog[0]) &&
+              !event.shiftKey
+            ) {
+              this._delay(function () {
+                $(first).trigger('focus');
+              });
+              event.preventDefault();
+            } else if (
+              (event.target === first || event.target === this.uiDialog[0]) &&
+              event.shiftKey
+            ) {
+              this._delay(function () {
+                $(last).trigger('focus');
+              });
+              event.preventDefault();
+            }
+          }
+        },
+        mousedown(event) {
+          if (this._moveToTop(event)) {
+            this._focusTabbable();
+          }
+        },
+      });
+
+      // We assume that any existing aria-describedby attribute means
+      // that the dialog content is marked up properly
+      // otherwise we brute force the content as the description
+      if (!this.element.find('[aria-describedby]').length) {
+        this.uiDialog.attr({
+          'aria-describedby': this.element.uniqueId().attr('id'),
+        });
+      }
+    },
     // Override jQuery UI's `_focusTabbable()` so finding tabbable elements uses
     // the core/tabbable library instead of jQuery UI's `:tabbable` selector.
     _focusTabbable() {
diff --git a/core/misc/dialog/dialog.js b/core/misc/dialog/dialog.js
index 3ec4218282b2..597ffde1ddf8 100644
--- a/core/misc/dialog/dialog.js
+++ b/core/misc/dialog/dialog.js
@@ -12,14 +12,12 @@
    * @type {object}
    *
    * @prop {boolean} [autoOpen=true]
-   * @prop {string} [dialogClass='']
    * @prop {string} [buttonClass='button']
    * @prop {string} [buttonPrimaryClass='button--primary']
    * @prop {function} close
    */
   drupalSettings.dialog = {
     autoOpen: true,
-    dialogClass: '',
     // Drupal-specific extensions: see dialog.jquery-ui.js.
     buttonClass: 'button',
     buttonPrimaryClass: 'button--primary',
diff --git a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
index 2163e5e5915a..2a2c3fd16587 100644
--- a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
+++ b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
@@ -820,8 +820,9 @@ media_library_mediaLibrary:
             name: Drupal.ckeditor5.openDialog
             invoke: false
         dialogSettings:
+          classes:
+            ui-dialog: media-library-widget-modal
           height: 75%
-          dialogClass: media-library-widget-modal
   drupal:
     label: Media Library
     elements: false
diff --git a/core/modules/editor/js/editor.js b/core/modules/editor/js/editor.js
index cc2d1b9fff6a..b0900e99b614 100644
--- a/core/modules/editor/js/editor.js
+++ b/core/modules/editor/js/editor.js
@@ -137,7 +137,9 @@
       );
       const confirmationDialog = Drupal.dialog(`<div>${message}</div>`, {
         title: Drupal.t('Change text format?'),
-        dialogClass: 'editor-change-text-format-modal',
+        classes: {
+          'ui-dialog': 'editor-change-text-format-modal',
+        },
         resizable: false,
         buttons: [
           {
diff --git a/core/modules/media_library/src/MediaLibraryUiBuilder.php b/core/modules/media_library/src/MediaLibraryUiBuilder.php
index 47143fcc251a..f7319244eda5 100644
--- a/core/modules/media_library/src/MediaLibraryUiBuilder.php
+++ b/core/modules/media_library/src/MediaLibraryUiBuilder.php
@@ -89,7 +89,9 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Req
    */
   public static function dialogOptions() {
     return [
-      'dialogClass' => 'media-library-widget-modal',
+      'classes' => [
+        'ui-dialog' => 'media-library-widget-modal',
+      ],
       'title' => t('Add or select media'),
       'height' => '75%',
       'width' => '75%',
diff --git a/core/modules/system/tests/modules/dialog_renderer_test/src/Controller/TestController.php b/core/modules/system/tests/modules/dialog_renderer_test/src/Controller/TestController.php
index 00ff48ce4ec4..ac2729c5e915 100644
--- a/core/modules/system/tests/modules/dialog_renderer_test/src/Controller/TestController.php
+++ b/core/modules/system/tests/modules/dialog_renderer_test/src/Controller/TestController.php
@@ -126,7 +126,9 @@ public function linksDisplay() {
           'class' => ['use-ajax'],
           'data-dialog-type' => 'modal',
           'data-dialog-options' => Json::encode([
-            'dialogClass' => 'no-close',
+            'classes' => [
+              'ui-dialog' => 'no-close',
+            ],
           ]),
         ],
         '#attached' => [
diff --git a/core/modules/system/tests/src/Functional/Ajax/OffCanvasDialogTest.php b/core/modules/system/tests/src/Functional/Ajax/OffCanvasDialogTest.php
index 7002e8066371..5b20009d2c42 100644
--- a/core/modules/system/tests/src/Functional/Ajax/OffCanvasDialogTest.php
+++ b/core/modules/system/tests/src/Functional/Ajax/OffCanvasDialogTest.php
@@ -47,6 +47,10 @@ public function testDialog($position) {
       'data' => (string) $dialog_contents,
       'dialogOptions' =>
         [
+          'classes' => [
+            'ui-dialog' => 'ui-dialog-off-canvas ui-dialog-position-' . ($position ?: 'side'),
+            'ui-dialog-content' => 'drupal-off-canvas-reset',
+          ],
           'title' => 'AJAX Dialog & contents',
           'modal' => FALSE,
           'autoResize' => FALSE,
@@ -54,8 +58,6 @@ public function testDialog($position) {
           'draggable' => FALSE,
           'drupalAutoButtons' => FALSE,
           'drupalOffCanvasPosition' => $position ?: 'side',
-          'dialogClass' => 'ui-dialog-off-canvas ui-dialog-position-' . ($position ?: 'side'),
-          'classes' => ['ui-dialog-content' => 'drupal-off-canvas-reset'],
           'width' => 300,
         ],
       'effect' => 'fade',
diff --git a/core/modules/views/tests/modules/views_test_modal/src/Controller/TestController.php b/core/modules/views/tests/modules/views_test_modal/src/Controller/TestController.php
index b89cf4081d73..84c0360b746d 100644
--- a/core/modules/views/tests/modules/views_test_modal/src/Controller/TestController.php
+++ b/core/modules/views/tests/modules/views_test_modal/src/Controller/TestController.php
@@ -22,7 +22,9 @@ public function modal() {
         'class' => ['use-ajax'],
         'data-dialog-type' => 'modal',
         'data-dialog-options' => Json::encode([
-          'dialogClass' => 'views-test-modal',
+          'classes' => [
+            'ui-dialog' => 'views-test-modal',
+          ],
           'height' => '50%',
           'width' => '50%',
           'title' => $this->t('Administer content'),
diff --git a/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php b/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
index 78efad376097..806a96d236dd 100644
--- a/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
+++ b/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
@@ -244,7 +244,9 @@ protected function ajaxFormWrapper($form_class, FormStateInterface &$form_state)
       $display .= $output;
 
       $options = [
-        'dialogClass' => 'views-ui-dialog js-views-ui-dialog',
+        'classes' => [
+          'ui-dialog' => 'views-ui-dialog js-views-ui-dialog',
+        ],
         'width' => '75%',
       ];
 
diff --git a/core/tests/Drupal/Tests/Core/Ajax/OpenOffCanvasDialogCommandTest.php b/core/tests/Drupal/Tests/Core/Ajax/OpenOffCanvasDialogCommandTest.php
index 0da679579bbc..1ad22f8905f4 100644
--- a/core/tests/Drupal/Tests/Core/Ajax/OpenOffCanvasDialogCommandTest.php
+++ b/core/tests/Drupal/Tests/Core/Ajax/OpenOffCanvasDialogCommandTest.php
@@ -34,8 +34,10 @@ public function testRender($position) {
         'resizable' => 'w',
         'draggable' => FALSE,
         'drupalAutoButtons' => FALSE,
-        'dialogClass' => 'ui-dialog-off-canvas ui-dialog-position-' . $position,
-        'classes' => ['ui-dialog-content' => 'drupal-off-canvas-reset'],
+        'classes' => [
+          'ui-dialog' => 'ui-dialog-off-canvas ui-dialog-position-' . $position,
+          'ui-dialog-content' => 'drupal-off-canvas-reset',
+        ],
         'width' => 300,
         'drupalOffCanvasPosition' => $position,
       ],
-- 
GitLab