Verified Commit d8f747b1 authored by Lauri Timmanee's avatar Lauri Timmanee
Browse files

Issue #3359494 by bnjmnm, Spokje, lauriii, hooroomoo: Focus is lost on dialog...

Issue #3359494 by bnjmnm, Spokje, lauriii, hooroomoo: Focus is lost on dialog close if the opener is inside a collapsible element
parent 96e2fd9d
Loading
Loading
Loading
Loading
+26 −2
Original line number Diff line number Diff line
@@ -3,7 +3,7 @@
 * Extends the Drupal AJAX functionality to integrate the dialog API.
 */

(function ($, Drupal) {
(function ($, Drupal, { focusable }) {
  /**
   * Initialize dialogs for Ajax purposes.
   *
@@ -46,6 +46,30 @@
      // Overwrite the close method to remove the dialog on closing.
      settings.dialog.close = function (event, ...args) {
        originalClose.apply(settings.dialog, [event, ...args]);
        // Check if the opener element is inside an AJAX container.
        const $element = $(event.target);
        const ajaxContainer = $element.data('uiDialog')
          ? $element
              .data('uiDialog')
              .opener.closest('[data-drupal-ajax-container]')
          : [];

        // If the opener element was in an ajax container, and focus is on the
        // body element, we can assume focus was lost. To recover, focus is
        // moved to the first focusable element in the container.
        if (
          ajaxContainer.length &&
          (document.activeElement === document.body ||
            $(document.activeElement).not(':visible'))
        ) {
          const focusableChildren = focusable(ajaxContainer[0]);
          if (focusableChildren.length > 0) {
            setTimeout(() => {
              focusableChildren[0].focus();
            }, 0);
          }
        }

        $(event.target).remove();
      };
    },
@@ -246,4 +270,4 @@
  $(window).on('dialog:beforeclose', (e, dialog, $element) => {
    $element.off('.dialog');
  });
})(jQuery, Drupal);
})(jQuery, Drupal, window.tabbable);
+2 −2
Original line number Diff line number Diff line
@@ -393,8 +393,8 @@ public function testBlockContextualLinks() {
    $cached_id_token = Crypt::hmacBase64($cached_id, Settings::getHashSalt() . $this->container->get('private_key')->get());
    // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
    // Check existence of the contextual link placeholders.
    $this->assertSession()->responseContains('<div' . new Attribute(['data-contextual-id' => $id, 'data-contextual-token' => $id_token]) . '></div>');
    $this->assertSession()->responseContains('<div' . new Attribute(['data-contextual-id' => $cached_id, 'data-contextual-token' => $cached_id_token]) . '></div>');
    $this->assertSession()->responseContains('<div' . new Attribute(['data-contextual-id' => $id, 'data-contextual-token' => $id_token, 'data-drupal-ajax-container' => '']) . '></div>');
    $this->assertSession()->responseContains('<div' . new Attribute(['data-contextual-id' => $cached_id, 'data-contextual-token' => $cached_id_token, 'data-drupal-ajax-container' => '']) . '></div>');

    // Get server-rendered contextual links.
    // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks()
+1 −0
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ public static function preRenderPlaceholder(array $element) {
    $attribute = new Attribute([
      'data-contextual-id' => $element['#id'],
      'data-contextual-token' => $token,
      'data-drupal-ajax-container' => '',
    ]);
    $element['#markup'] = new FormattableMarkup('<div@attributes></div>', ['@attributes' => $attribute]);

+19 −1
Original line number Diff line number Diff line
@@ -85,9 +85,27 @@ public function testContextualLinksClick() {
    $this->assertSession()->assertWaitOnAjaxRequest();
    $current_page_string = 'NOT_RELOADED_IF_ON_PAGE';
    $this->getSession()->executeScript('document.body.appendChild(document.createTextNode("' . $current_page_string . '"));');
    $this->clickContextualLink('#block-branding', 'Test Link with Ajax');

    // Move the pointer over the branding block so the contextual link appears
    // as it would with a real user interaction. Otherwise clickContextualLink()
    // does not open the dialog in a manner that is opener-aware, and it isn't
    // possible to reliably test focus management.
    $driver_session = $this->getSession()->getDriver()->getWebDriverSession();
    $element = $driver_session->element('css selector', '#block-branding');
    $driver_session->moveto(['element' => $element->getID()]);
    $this->clickContextualLink('#block-branding', 'Test Link with Ajax', FALSE);
    $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#drupal-modal'));
    $this->assertSession()->elementContains('css', '#drupal-modal', 'Everything is contextual!');
    $this->getSession()->executeScript('document.querySelector("#block-branding .trigger").addEventListener("focus", (e) => e.target.classList.add("i-am-focused"))');
    $this->getSession()->getPage()->pressButton('Close');
    $this->assertSession()->assertNoElementAfterWait('css', 'ui.dialog');

    // When the dialog is closed, the opening contextual link is now inside a
    // collapsed container, so focus should be routed to the contextual link
    // toggle button.
    $this->assertNotNull($this->assertSession()->waitForElement('css', '.trigger.i-am-focused'), $this->getSession()->getPage()->find('css', '#block-branding')->getOuterHtml());
    $this->assertJsCondition('document.activeElement === document.querySelector("#block-branding button.trigger")', 10000, 'Focus should be on the contextual trigger, but instead is at ' . $this->getSession()->evaluateScript('document.activeElement.outerHTML'));

    // Check to make sure that page was not reloaded.
    $this->assertSession()->pageTextContains($current_page_string);

+8 −0
Original line number Diff line number Diff line
@@ -29,3 +29,11 @@ dialog_renderer_test.modal_content_input:
    _title: 'Thing 3'
  requirements:
    _access: 'TRUE'

dialog_renderer_test.collapsed_opener:
  path: '/dialog_renderer-collapsed-opener'
  defaults:
    _controller: '\Drupal\dialog_renderer_test\Controller\TestController::collapsedOpener'
    _title: 'Collapsed Openers'
  requirements:
    _access: 'TRUE'
Loading