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

8.4.x
Lee Rowlands 2017-07-26 10:21:52 +10:00
parent 4f1d0f7f41
commit bbc8743178
No known key found for this signature in database
GPG Key ID: 2B829A3DF9204DC4
7 changed files with 262 additions and 0 deletions

View File

@ -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));

View File

@ -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);

View File

@ -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));

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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'));
}
}