Issue #2531700 by dmsmidt, alexrayu, cboyden, finne, tameeshb, kattekrab, andrewmacpherson, hass, droplet, drpal, dsnopek, nod_: Fragment links to children elements in closed grouping elements don't work
parent
4f1d0f7f41
commit
bbc8743178
|
@ -137,6 +137,28 @@
|
|||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Open parent details elements of a targeted page fragment.
|
||||
*
|
||||
* Opens all (nested) details element on a hash change or fragment link click
|
||||
* when the target is a child element, in order to make sure the targeted
|
||||
* element is visible. Aria attributes on the summary
|
||||
* are set by triggering the click event listener in details-aria.js.
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
* @param {jQuery} $target
|
||||
* The targeted node as a jQuery object.
|
||||
*/
|
||||
const handleFragmentLinkClickOrHashChange = (e, $target) => {
|
||||
$target.parents('details').not('[open]').find('> summary').trigger('click');
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds a listener to handle fragment link clicks and URL hash changes.
|
||||
*/
|
||||
$('body').on('formFragmentLinkClickOrHashChange.details', handleFragmentLinkClickOrHashChange);
|
||||
|
||||
// Expose constructor in the public space.
|
||||
Drupal.CollapsibleDetails = CollapsibleDetails;
|
||||
}(jQuery, Modernizr, Drupal));
|
||||
|
|
|
@ -77,5 +77,11 @@
|
|||
}
|
||||
};
|
||||
|
||||
var handleFragmentLinkClickOrHashChange = function handleFragmentLinkClickOrHashChange(e, $target) {
|
||||
$target.parents('details').not('[open]').find('> summary').trigger('click');
|
||||
};
|
||||
|
||||
$('body').on('formFragmentLinkClickOrHashChange.details', handleFragmentLinkClickOrHashChange);
|
||||
|
||||
Drupal.CollapsibleDetails = CollapsibleDetails;
|
||||
})(jQuery, Modernizr, Drupal);
|
|
@ -12,6 +12,16 @@
|
|||
* @event formUpdated
|
||||
*/
|
||||
|
||||
/**
|
||||
* Triggers when a click on a page fragment link or hash change is detected.
|
||||
*
|
||||
* The event triggers when the fragment in the URL changes (a hash change) and
|
||||
* when a link containing a fragment identifier is clicked. In case the hash
|
||||
* changes due to a click this event will only be triggered once.
|
||||
*
|
||||
* @event formFragmentLinkClickOrHashChange
|
||||
*/
|
||||
|
||||
(function ($, Drupal, debounce) {
|
||||
/**
|
||||
* Retrieves the summary for the first element.
|
||||
|
@ -245,4 +255,45 @@
|
|||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a fragment interaction event on a hash change or fragment link click.
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
*
|
||||
* @fires event:formFragmentLinkClickOrHashChange
|
||||
*/
|
||||
const handleFragmentLinkClickOrHashChange = (e) => {
|
||||
let $target;
|
||||
|
||||
if (e.type === 'click') {
|
||||
$target = e.currentTarget.location ? $(e.currentTarget.location.hash) : $(e.currentTarget.hash);
|
||||
}
|
||||
else {
|
||||
$target = $(`#${location.hash.substr(1)}`);
|
||||
}
|
||||
|
||||
$('body').trigger('formFragmentLinkClickOrHashChange', [$target]);
|
||||
|
||||
/**
|
||||
* Clicking a fragment link or a hash change should focus the target
|
||||
* element, but event timing issues in multiple browsers require a timeout.
|
||||
*/
|
||||
setTimeout(() => {
|
||||
$target.focus();
|
||||
}, 300, $target);
|
||||
};
|
||||
|
||||
// Binds a listener to handle URL fragment changes.
|
||||
$(window).on('hashchange.form-fragment', debounce(handleFragmentLinkClickOrHashChange, 300, true));
|
||||
|
||||
/**
|
||||
* Binds a listener to handle clicks on fragment links and absolute URL links
|
||||
* containing a fragment, this is needed next to the hash change listener
|
||||
* because clicking such links doesn't trigger a hash change when the fragment
|
||||
* is already in the URL.
|
||||
*/
|
||||
$(document).on('click.form-fragment', 'a[href*="#"]', debounce(handleFragmentLinkClickOrHashChange, 300, true));
|
||||
|
||||
}(jQuery, Drupal, Drupal.debounce));
|
||||
|
|
|
@ -124,4 +124,24 @@
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
var handleFragmentLinkClickOrHashChange = function handleFragmentLinkClickOrHashChange(e) {
|
||||
var $target = void 0;
|
||||
|
||||
if (e.type === 'click') {
|
||||
$target = e.currentTarget.location ? $(e.currentTarget.location.hash) : $(e.currentTarget.hash);
|
||||
} else {
|
||||
$target = $('#' + location.hash.substr(1));
|
||||
}
|
||||
|
||||
$('body').trigger('formFragmentLinkClickOrHashChange', [$target]);
|
||||
|
||||
setTimeout(function () {
|
||||
$target.focus();
|
||||
}, 300, $target);
|
||||
};
|
||||
|
||||
$(window).on('hashchange.form-fragment', debounce(handleFragmentLinkClickOrHashChange, 300, true));
|
||||
|
||||
$(document).on('click.form-fragment', 'a[href*="#"]', debounce(handleFragmentLinkClickOrHashChange, 300, true));
|
||||
})(jQuery, Drupal, Drupal.debounce);
|
|
@ -13,6 +13,23 @@
|
|||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
/**
|
||||
* Show the parent vertical tab pane of a targeted page fragment.
|
||||
*
|
||||
* In order to make sure a targeted element inside a vertical tab pane is
|
||||
* visible on a hash change or fragment link click, show all parent panes.
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
* @param {jQuery} $target
|
||||
* The targeted node as a jQuery object.
|
||||
*/
|
||||
const handleFragmentLinkClickOrHashChange = (e, $target) => {
|
||||
$target.parents('.vertical-tabs__pane').each((index, pane) => {
|
||||
$(pane).data('verticalTab').focus();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This script transforms a set of details into a stack of vertical tabs.
|
||||
*
|
||||
|
@ -36,6 +53,11 @@
|
|||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a listener to handle fragment link clicks and URL hash changes.
|
||||
*/
|
||||
$('body').once('vertical-tabs-fragments').on('formFragmentLinkClickOrHashChange.verticalTabs', handleFragmentLinkClickOrHashChange);
|
||||
|
||||
$(context).find('[data-vertical-tabs-panes]').once('vertical-tabs').each(function () {
|
||||
const $this = $(this).addClass('vertical-tabs__panes');
|
||||
const focusID = $this.find(':hidden.vertical-tabs__active-tab').val();
|
||||
|
|
|
@ -6,6 +6,12 @@
|
|||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
var handleFragmentLinkClickOrHashChange = function handleFragmentLinkClickOrHashChange(e, $target) {
|
||||
$target.parents('.vertical-tabs__pane').each(function (index, pane) {
|
||||
$(pane).data('verticalTab').focus();
|
||||
});
|
||||
};
|
||||
|
||||
Drupal.behaviors.verticalTabs = {
|
||||
attach: function attach(context) {
|
||||
var width = drupalSettings.widthBreakpoint || 640;
|
||||
|
@ -15,6 +21,8 @@
|
|||
return;
|
||||
}
|
||||
|
||||
$('body').once('vertical-tabs-fragments').on('formFragmentLinkClickOrHashChange.verticalTabs', handleFragmentLinkClickOrHashChange);
|
||||
|
||||
$(context).find('[data-vertical-tabs-panes]').once('vertical-tabs').each(function () {
|
||||
var $this = $(this).addClass('vertical-tabs__panes');
|
||||
var focusID = $this.find(':hidden.vertical-tabs__active-tab').val();
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\FunctionalJavascriptTests\Core\Form;
|
||||
|
||||
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
|
||||
|
||||
/**
|
||||
* Tests for form grouping elements.
|
||||
*
|
||||
* @group form
|
||||
*/
|
||||
class FormGroupingElementsTest extends JavascriptTestBase {
|
||||
|
||||
/**
|
||||
* Required modules.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['form_test'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$account = $this->drupalCreateUser();
|
||||
$this->drupalLogin($account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that vertical tab children become visible.
|
||||
*
|
||||
* Makes sure that a child element of a vertical tab that is not visible,
|
||||
* becomes visible when the tab is clicked, a fragment link to the child is
|
||||
* clicked or when the URI fragment pointing to that child changes.
|
||||
*/
|
||||
public function testVerticalTabChildVisibility() {
|
||||
$session = $this->getSession();
|
||||
$web_assert = $this->assertSession();
|
||||
|
||||
// Request the group vertical tabs testing page with a fragment identifier
|
||||
// to the second element.
|
||||
$this->drupalGet('form-test/group-vertical-tabs', ['fragment' => 'edit-element-2']);
|
||||
|
||||
$page = $session->getPage();
|
||||
|
||||
$tab_link_1 = $page->find('css', '.vertical-tabs__menu-item > a');
|
||||
|
||||
$child_1_selector = '#edit-element';
|
||||
$child_1 = $page->find('css', $child_1_selector);
|
||||
|
||||
$child_2_selector = '#edit-element-2';
|
||||
$child_2 = $page->find('css', $child_2_selector);
|
||||
|
||||
// Assert that the child in the second vertical tab becomes visible.
|
||||
// It should be visible after initial load due to the fragment in the URI.
|
||||
$this->assertTrue($child_2->isVisible(), 'Child 2 is visible due to a URI fragment');
|
||||
|
||||
// Click on a fragment link pointing to an invisible child inside an
|
||||
// inactive vertical tab.
|
||||
$session->executeScript("jQuery('<a href=\"$child_1_selector\"></a>').insertAfter('h1')[0].click()");
|
||||
|
||||
// Assert that the child in the first vertical tab becomes visible.
|
||||
$web_assert->waitForElementVisible('css', $child_1_selector, 50);
|
||||
|
||||
// Trigger a URI fragment change (hashchange) to show the second vertical
|
||||
// tab again.
|
||||
$session->executeScript("location.replace('$child_2_selector')");
|
||||
|
||||
// Assert that the child in the second vertical tab becomes visible again.
|
||||
$web_assert->waitForElementVisible('css', $child_2_selector, 50);
|
||||
|
||||
$tab_link_1->click();
|
||||
|
||||
// Assert that the child in the first vertical tab is visible again after
|
||||
// a click on the first tab.
|
||||
$this->assertTrue($child_1->isVisible(), 'Child 1 is visible after clicking the parent tab');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that details element children become visible.
|
||||
*
|
||||
* Makes sure that a child element of a details element that is not visible,
|
||||
* becomes visible when a fragment link to the child is clicked or when the
|
||||
* URI fragment pointing to that child changes.
|
||||
*/
|
||||
public function testDetailsChildVisibility() {
|
||||
$session = $this->getSession();
|
||||
$web_assert = $this->assertSession();
|
||||
|
||||
// Store reusable JavaScript code to remove the current URI fragment and
|
||||
// close all details.
|
||||
$reset_js = "location.replace('#'); jQuery('details').removeAttr('open')";
|
||||
|
||||
// Request the group details testing page.
|
||||
$this->drupalGet('form-test/group-details');
|
||||
|
||||
$page = $session->getPage();
|
||||
|
||||
$session->executeScript($reset_js);
|
||||
|
||||
$child_selector = '#edit-element';
|
||||
$child = $page->find('css', $child_selector);
|
||||
|
||||
// Assert that the child is not visible.
|
||||
$this->assertFalse($child->isVisible(), 'Child is not visible');
|
||||
|
||||
// Trigger a URI fragment change (hashchange) to open all parent details
|
||||
// elements of the child.
|
||||
$session->executeScript("location.replace('$child_selector')");
|
||||
|
||||
// Assert that the child becomes visible again after a hash change.
|
||||
$web_assert->waitForElementVisible('css', $child_selector, 50);
|
||||
|
||||
$session->executeScript($reset_js);
|
||||
|
||||
// Click on a fragment link pointing to an invisible child inside a closed
|
||||
// details element.
|
||||
$session->executeScript("jQuery('<a href=\"$child_selector\"></a>').insertAfter('h1')[0].click()");
|
||||
|
||||
// Assert that the child is visible again after a fragment link click.
|
||||
$web_assert->waitForElementVisible('css', $child_selector, 50);
|
||||
|
||||
// Find the summary belonging to the closest details element.
|
||||
$summary = $page->find('css', '#edit-meta > summary');
|
||||
|
||||
// Assert that both aria-expanded and aria-pressed are true.
|
||||
$this->assertTrue($summary->getAttribute('aria-expanded'));
|
||||
$this->assertTrue($summary->getAttribute('aria-pressed'));
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue