Skip to content
Snippets Groups Projects
Verified Commit 489ee3a0 authored by Lauri Timmanee's avatar Lauri Timmanee
Browse files

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

Issue #3359494 by bnjmnm, lauriii, hooroomoo: Focus is lost on dialog close if the opener is inside a collapsible element

(cherry picked from commit d6bb0067)
parent 2ee2a87b
No related branches found
No related tags found
31 merge requests!7564Revert "Issue #3364773 by roshnichordiya, Chris Matthews, thakurnishant_06,...,!5752Issue #3275828 by joachim, quietone, bradjones1, Berdir: document the reason...,!5688Issue #3087950 by Utkarsh_33, swatichouhan012, komalk, Sivaji_Ganesh_Jojodae,...,!5627Issue #3261805: Field not saved when change of 0 on string start,!5427Issue #3338518: send credentials in ajax if configured in CORS settings.,!5395Issue #3387916 by fjgarlin, Spokje: Each GitLab job exposes user email,!5217Issue #3386607 by alexpott: Improve spell checking in commit-code-check.sh,!5064Issue #3379522 by finnsky, Gauravvvv, kostyashupenko, smustgrave, Chi: Revert...,!5040SDC ComponentElement: Transform slots scalar values to #plain_text instead of throwing an exception,!4958Issue #3392147: Whitelist IP for a Ban module.,!4942Issue #3365945: Errors: The following table(s) do not have a primary key: forum_index,!4894Issue #3280279: Add API to allow sites to opt in to upload SVG images in CKEditor 5,!4857Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!4856Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!4788Issue #3272985: RSS Feed header reverts to text/html when cached,!4716Issue #3362929: Improve 400 responses for broken/invalid image style routes,!4553Draft: Issue #2980951: Permission to see own unpublished comments in comment thread,!4273Add UUID to sections,!4192Issue #3367204: [CKEditor5] Missing dependency on drupal.ajax,!4100Issue #3249600: Add support for PHP 8.1 Enums as allowed values for list_* data types,!4090Draft: Issue #3362924 by shwetaDevkate, Gauravvvv, frank8199,!3679Issue #115801: Allow password on registration without disabling e-mail verification,!3676Issue #3347497: Introduce a FetchModeTrait to allow emulating PDO fetch modes,!3629Issue #3347343: Continuation Add Views EntityReference filter to be available for all entity reference fields,!3106Issue #3017548: "Filtered HTML" text format does not support manual teaser break (<!--break-->),!3066Issue #3325175: Deprecate calling \Drupal\menu_link_content\Form\MenuLinkContentForm::_construct() with the $language_manager argument,!3004Issue #2463967: Use .user.ini file for PHP settings,!2851Issue #2264739: Allow multiple field widgets to not use tabledrag,!1484Exposed filters get values from URL when Ajax is on,!925Issue #2339235: Remove taxonomy hard dependency on node module,!872Draft: Issue #3221319: Race condition when creating menu links and editing content deletes menu links
......@@ -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,27 @@
// 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) {
focusableChildren[0].focus();
}
}
$(event.target).remove();
};
},
......@@ -246,4 +267,4 @@
$(window).on('dialog:beforeclose', (e, dialog, $element) => {
$element.off('.dialog');
});
})(jQuery, Drupal);
})(jQuery, Drupal, window.tabbable);
......@@ -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()
......
......@@ -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]);
......
......@@ -88,6 +88,14 @@ public function testContextualLinksClick() {
$this->clickContextualLink('#block-branding', 'Test Link with Ajax');
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#drupal-modal'));
$this->assertSession()->elementContains('css', '#drupal-modal', 'Everything is contextual!');
$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->assertJsCondition('document.activeElement === document.querySelector("#block-branding button.trigger")');
// Check to make sure that page was not reloaded.
$this->assertSession()->pageTextContains($current_page_string);
......
......@@ -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'
......@@ -189,4 +189,39 @@ public function linksDisplay() {
];
}
/**
* Displays a dropbutton with a link that opens in a modal dialog.
*
* @return array
* Render array with links.
*/
public function collapsedOpener() {
return [
'#markup' => '<h2>Honk</h2>',
'dropbutton' => [
'#type' => 'dropbutton',
'#dropbutton_type' => 'small',
'#links' => [
'front' => [
'title' => 'front!',
'url' => Url::fromRoute('<front>'),
],
'in a dropbutton' => [
'title' => 'inside a dropbutton',
'url' => Url::fromRoute('dialog_renderer_test.modal_content'),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
],
],
],
],
'#attached' => [
'library' => [
'core/drupal.ajax',
],
],
];
}
}
......@@ -76,4 +76,31 @@ public function testModalRenderer() {
$this->assertJsCondition('document.activeElement === document.querySelector(".ui-dialog .form-text")');
}
/**
* Confirm focus management of a dialog openers in a dropbutton.
*/
public function testOpenerInDropbutton() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet('dialog_renderer-collapsed-opener');
// Open a modal using a link inside a dropbutton.
$page->find('css', '.dropbutton-toggle button')->click();
$modal_link = $assert_session->waitForElementVisible('css', '.secondary-action a');
$modal_link->click();
$assert_session->waitForElementVisible('css', '.ui-dialog');
$assert_session->assertVisibleInViewport('css', '.ui-dialog .ui-dialog-content');
$page->pressButton('Close');
// When the dialog "closes" it is still present, so wait on it switching to
// `display: none;`.
$assert_session->waitForElement('css', '.ui-dialog[style*="display: none;"]');
// Confirm that when the modal closes, focus is moved to the first visible
// and focusable item in the contextual link container, because the original
// opener is not available.
$this->assertJsCondition('document.activeElement === document.querySelector(".dropbutton-action a")');
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment