Skip to content
Snippets Groups Projects
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
No related branches found
No related tags found
50 merge requests!54479.5.x SF update,!5014Issue #3071143: Table Render Array Example Is Incorrect,!4868Issue #1428520: Improve menu parent link selection,!4686Issue #3292350: file_validate_image_resolution does not update file size after resizing,!4594Applying patch for Views Global Text area field to allow extra HTML tags. As video, source and iframe tag is not rendering. Due to which Media embedded video and remote-video not rendering in Views Global Text area field.,!4289Issue #1344552 by marcingy, Niklas Fiekas, Ravi.J, aleevas, Eduardo Morales...,!4114Issue #2707291: Disable body-level scrolling when a dialog is open as a modal,!4022Update String overrides example in default.settings.php,!3878Removed unused condition head title for views,!38582585169-10.1.x,!3825Issue #2972573: randomMachineName() should conform to processMachineName() pattern,!3818Issue #2140179: $entity->original gets stale between updates,!3770Issue #3318112: Move "Block layout" from Structure to Appearance,!3742Issue #3328429: Create item list field formatter for displaying ordered and unordered lists,!3731Claro: role=button on status report items,!3668Resolve #3347842 "Deprecate the trusted",!3651Issue #3347736: Create new SDC component for Olivero (header-search),!3546refactored dialog.pcss file,!3531Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!3502Issue #3335308: Confusing behavior with FormState::setFormState and FormState::setMethod,!3452Issue #3332701: Refactor Claro's tablesort-indicator stylesheet,!3451Issue #2410579: Allows setting the current language programmatically.,!3355Issue #3209129: Scrolling problems when adding a block via layout builder,!3228Issue #2920678: Add config validation for the allowed characters of machine names,!3226Issue #2987537: Custom menu link entity type should not declare "bundle" entity key,!3154Fixes #2987987 - CSRF token validation broken on routes with optional parameters.,!3147Issue #3328457: Replace most substr($a, $i) where $i is negative with str_ends_with(),!3146Issue #3328456: Replace substr($a, 0, $i) with str_starts_with(),!3133core/modules/system/css/components/hidden.module.css,!31312878513-10.1.x,!3009Issue #3323252: Add @method PhpDoc for EntityStorageInterface descendants,!2812Issue #3312049: [Followup] Fix Drupal.Commenting.FunctionComment.MissingReturnType returns for NULL,!2614Issue #2981326: Replace non-test usages of \Drupal::logger() with IoC injection,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2334Issue #3228209: Add hasRole() method to AccountInterface,!2062Issue #3246454: Add weekly granularity to views date sort,!1591Issue #3199697: Add JSON:API Translation experimental module,!1255Issue #3238922: Refactor (if feasible) uses of the jQuery serialize function to use vanillaJS,!1105Issue #3025039: New non translatable field on translatable content throws error,!1073issue #3191727: Focus states on mobile second level navigation items fixed,!877Issue #2708101: Default value for link text is not saved,!844Resolve #3036010 "Updaters",!673Issue #3214208: FinishResponseSubscriber could create duplicate headers,!617Issue #3043725: Provide a Entity Handler for user cancelation,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493,!485Sets the autocomplete attribute for username/password input field on login form.,!30Issue #3182188: Updates composer usage to point at ./vendor/bin/composer,!23Issue #2879087: Use comment access handler instead of hardcoding permissions
......@@ -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);
......@@ -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]);
......
......@@ -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);
......
......@@ -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