diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 50abc2e501ce8d6b18cdf5f677b399a77700017a..cd99207c0fabfad526cae7a808dd5e23aaf49b72 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -575,15 +575,11 @@ drupal.displace: drupal.dropbutton: version: VERSION js: - misc/dropbutton/dropbutton.js: {} + # Needed to make sure the element is loaded before connectedCallback is executed. + misc/dropbutton/dropbutton.js: { attributes: { defer: true } } css: component: misc/dropbutton/dropbutton.css: {} - dependencies: - - core/jquery - - core/drupal - - core/drupalSettings - - core/once drupal.entity-form: version: VERSION diff --git a/core/misc/dropbutton/dropbutton.js b/core/misc/dropbutton/dropbutton.js index 1f14dc12817d0eee5c4b72629919e44a99dd70a6..e2ce7bc37b51c5953bcd491a46c18e57062b0e0f 100644 --- a/core/misc/dropbutton/dropbutton.js +++ b/core/misc/dropbutton/dropbutton.js @@ -3,234 +3,117 @@ * Dropbutton feature. */ -(function ($, Drupal) { - /** - * A DropButton presents an HTML list as a button with a primary action. - * - * All secondary actions beyond the first in the list are presented in a - * dropdown list accessible through a toggle arrow associated with the button. - * - * @constructor Drupal.DropButton - * - * @param {HTMLElement} dropbutton - * A DOM element. - * @param {object} settings - * A list of options including: - * @param {string} settings.title - * The text inside the toggle link element. This text is hidden - * from visual UAs. - */ - function DropButton(dropbutton, settings) { - // Merge defaults with settings. - const options = $.extend( - { title: Drupal.t('List additional actions') }, - settings, - ); - const $dropbutton = $(dropbutton); +customElements.define( + 'drupal-dropbutton', + class DrupalDropbutton extends HTMLElement { + connectedCallback() { + const settings = drupalSettings?.dropbutton; + // Merge defaults with settings. + const options = { + title: Drupal.t('List additional actions'), + ...settings, + }; + + const actions = this.querySelectorAll('.dropbutton li'); + + // Add the special dropdown only if there are hidden actions. + if (actions.length > 1) { + // Identify the first element of the collection. + const primary = actions[0]; + + this.classList.add('dropbutton-multiple'); + actions.forEach((li) => + li.classList.add('dropbutton-action', 'secondary-action'), + ); + primary.classList.remove('secondary-action'); + // Add toggle link. + primary.insertAdjacentHTML( + 'afterend', + DrupalDropbutton.dropbuttonToggle(options), + ); + + this.addEventListener('click', this); + this.addEventListener('mouseleave', this); + this.addEventListener('mouseenter', this); + this.addEventListener('focusout', this); + this.addEventListener('focusin', this); + } else { + this.classList.add('dropbutton-single'); + } + } + + disconnectedCallback() { + this.removeEventListener('click', this); + this.removeEventListener('mouseleave', this); + this.removeEventListener('mouseenter', this); + this.removeEventListener('focusout', this); + this.removeEventListener('focusin', this); + } + + handleEvent(event) { + if ( + event.type === 'click' && + event.target.matches('[data-drupal-dropbutton-toggle]') + ) { + event.preventDefault(); + this.toggle(); + } else if (['mouseleave', 'focusout'].includes(event.type)) { + this.hoverOut(); + } else if (['mouseenter', 'focusin'].includes(event.type)) { + this.hoverIn(); + } + } /** - * @type {jQuery} + * Toggle the dropbutton open and closed. + * + * @param {boolean} [show] + * Force the dropbutton to open by passing true or to close by + * passing false. */ - this.$dropbutton = $dropbutton; + toggle(show) { + const isBool = typeof show === 'boolean'; + show = isBool ? show : !this.classList.contains('open'); + this.classList.toggle('open', show); + } /** - * @type {jQuery} + * @method */ - this.$list = $dropbutton.find('.dropbutton'); + hoverIn() { + // Clear any previous timer we were using. + if (this.timerID) { + window.clearTimeout(this.timerID); + } + } /** - * Find actions and mark them. - * - * @type {jQuery} + * @method */ - this.$actions = this.$list.find('li').addClass('dropbutton-action'); - - // Add the special dropdown only if there are hidden actions. - if (this.$actions.length > 1) { - // Identify the first element of the collection. - const $primary = this.$actions.slice(0, 1); - // Identify the secondary actions. - const $secondary = this.$actions.slice(1); - $secondary.addClass('secondary-action'); - // Add toggle link. - $primary.after(Drupal.theme('dropbuttonToggle', options)); - // Bind mouse events. - this.$dropbutton.addClass('dropbutton-multiple').on({ - /** - * Adds a timeout to close the dropdown on mouseleave. - * - * @ignore - */ - 'mouseleave.dropbutton': this.hoverOut.bind(this), - - /** - * Clears timeout when mouseout of the dropdown. - * - * @ignore - */ - 'mouseenter.dropbutton': this.hoverIn.bind(this), - - /** - * Similar to mouseleave/mouseenter, but for keyboard navigation. - * - * @ignore - */ - 'focusout.dropbutton': this.focusOut.bind(this), + hoverOut() { + // Wait half a second before closing. + this.timerID = window.setTimeout(() => this.close(), 500); + } - /** - * @ignore - */ - 'focusin.dropbutton': this.focusIn.bind(this), - }); - } else { - this.$dropbutton.addClass('dropbutton-single'); + /** + * @method + */ + open() { + this.toggle(true); } - } - /** - * Delegated callback for opening and closing dropbutton secondary actions. - * - * @function Drupal.DropButton~dropbuttonClickHandler - * - * @param {jQuery.Event} e - * The event triggered. - */ - function dropbuttonClickHandler(e) { - e.preventDefault(); - $(e.target).closest('.dropbutton-wrapper').toggleClass('open'); - } + /** + * @method + */ + close() { + this.toggle(false); + } - /** - * Process elements with the .dropbutton class on page load. - * - * @type {Drupal~behavior} - * - * @prop {Drupal~behaviorAttach} attach - * Attaches dropButton behaviors. - */ - Drupal.behaviors.dropButton = { - attach(context, settings) { - const dropbuttons = once('dropbutton', '.dropbutton-wrapper', context); - if (dropbuttons.length) { - // Adds the delegated handler that will toggle dropdowns on click. - const body = once('dropbutton-click', 'body'); - if (body.length) { - $(body).on('click', '.dropbutton-toggle', dropbuttonClickHandler); - } - // Initialize all buttons. - dropbuttons.forEach((dropbutton) => { - DropButton.dropbuttons.push( - new DropButton(dropbutton, settings.dropbutton), - ); - }); + static dropbuttonToggle(options) { + if (Drupal?.theme?.dropbuttonToggle) { + return Drupal.theme.dropbuttonToggle(options); } - }, - }; - - /** - * Extend the DropButton constructor. - */ - $.extend( - DropButton, - /** @lends Drupal.DropButton */ { - /** - * Store all processed DropButtons. - * - * @type {Array.<Drupal.DropButton>} - */ - dropbuttons: [], - }, - ); - - /** - * Extend the DropButton prototype. - */ - $.extend( - DropButton.prototype, - /** @lends Drupal.DropButton# */ { - /** - * Toggle the dropbutton open and closed. - * - * @param {boolean} [show] - * Force the dropbutton to open by passing true or to close by - * passing false. - */ - toggle(show) { - const isBool = typeof show === 'boolean'; - show = isBool ? show : !this.$dropbutton.hasClass('open'); - this.$dropbutton.toggleClass('open', show); - }, - - /** - * @method - */ - hoverIn() { - // Clear any previous timer we were using. - if (this.timerID) { - window.clearTimeout(this.timerID); - } - }, - - /** - * @method - */ - hoverOut() { - // Wait half a second before closing. - this.timerID = window.setTimeout(this.close.bind(this), 500); - }, - - /** - * @method - */ - open() { - this.toggle(true); - }, - - /** - * @method - */ - close() { - this.toggle(false); - }, - - /** - * @param {jQuery.Event} e - * The event triggered. - */ - focusOut(e) { - this.hoverOut.call(this, e); - }, - - /** - * @param {jQuery.Event} e - * The event triggered. - */ - focusIn(e) { - this.hoverIn.call(this, e); - }, - }, - ); - - $.extend( - Drupal.theme, - /** @lends Drupal.theme */ { - /** - * A toggle is an interactive element often bound to a click handler. - * - * @param {object} options - * Options object. - * @param {string} [options.title] - * The button text. - * - * @return {string} - * A string representing a DOM fragment. - */ - dropbuttonToggle(options) { - return `<li class="dropbutton-toggle"><button type="button"><span class="dropbutton-arrow"><span class="visually-hidden">${options.title}</span></span></button></li>`; - }, - }, - ); - - // Expose constructor in the public space. - Drupal.DropButton = DropButton; -})(jQuery, Drupal); + return `<li class="dropbutton-toggle" data-drupal-dropbutton-toggle><button type="button"><span class="dropbutton-arrow"><span class="visually-hidden">${options.title}</span></span></button></li>`; + } + }, +); diff --git a/core/modules/system/templates/dropbutton-wrapper.html.twig b/core/modules/system/templates/dropbutton-wrapper.html.twig index 9c87a49f6b8ea59d890c24b96d4c3063298e744f..554b88fc9d35e7ce09f75c1b5f6a6325e4d4aee3 100644 --- a/core/modules/system/templates/dropbutton-wrapper.html.twig +++ b/core/modules/system/templates/dropbutton-wrapper.html.twig @@ -13,10 +13,10 @@ #} {% if children %} {% apply spaceless %} - <div class="dropbutton-wrapper" data-drupal-ajax-container> + <drupal-dropbutton class="dropbutton-wrapper" data-drupal-ajax-container> <div class="dropbutton-widget"> {{ children }} </div> - </div> + </drupal-dropbutton> {% endapply %} {% endif %} diff --git a/core/modules/views/tests/src/Kernel/Plugin/RowRenderCacheTest.php b/core/modules/views/tests/src/Kernel/Plugin/RowRenderCacheTest.php index 1629bbdad047bb61a88127b8bdd90a4bc27c8c38..5990fc230d5b2a7c984cffc3643c788be49be46a 100644 --- a/core/modules/views/tests/src/Kernel/Plugin/RowRenderCacheTest.php +++ b/core/modules/views/tests/src/Kernel/Plugin/RowRenderCacheTest.php @@ -185,10 +185,10 @@ protected function doTestRenderedOutput(AccountInterface $account, $check_cache $expected = $access ? "<a href=\"$node_url/delete?destination=/\" hreflang=\"en\">delete</a>" : ""; $output = $view->style_plugin->getField($index, 'delete_node'); $this->assertSame($expected, (string) $output); - $expected = $access ? ' <div class="dropbutton-wrapper" data-drupal-ajax-container><div class="dropbutton-widget"><ul class="dropbutton">' . + $expected = $access ? ' <drupal-dropbutton class="dropbutton-wrapper" data-drupal-ajax-container><div class="dropbutton-widget"><ul class="dropbutton">' . '<li><a href="' . $node_url . '/edit?destination=/" aria-label="Edit ' . $node->label() . '" hreflang="en">Edit</a></li>' . '<li><a href="' . $node_url . '/delete?destination=/" aria-label="Delete ' . $node->label() . '" class="use-ajax" data-dialog-type="modal" data-dialog-options="' . Html::escape(Json::encode(['width' => 880])) . '" hreflang="en">Delete</a></li>' . - '</ul></div></div>' : ''; + '</ul></div></drupal-dropbutton>' : ''; $output = $view->style_plugin->getField($index, 'operations'); $this->assertSame($expected, (string) $output); diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ThrobberTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ThrobberTest.php index 0372ae1cd996e606dc80886bd4a64383bec57945..665afbb457b1fb80fed41bee4e64143c7d59c70f 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ThrobberTest.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ThrobberTest.php @@ -95,7 +95,7 @@ public function testThemingThrobberElement(): void { $this->assertNotEmpty($web_assert->waitForElementVisible('css', '#drupal-modal')); hold_test_response(TRUE); $this->clickLink('Place block'); - $this->assertNotNull($web_assert->waitForElement('xpath', '//div[contains(@class, "dropbutton-wrapper")]/following-sibling::div[contains(@class, "ajax-progress-throbber")]')); + $this->assertNotNull($web_assert->waitForElement('xpath', '//drupal-dropbutton[contains(@class, "dropbutton-wrapper")]/following-sibling::div[contains(@class, "ajax-progress-throbber")]')); hold_test_response(FALSE); $web_assert->assertNoElementAfterWait('css', '.ajax-progress-throbber'); } diff --git a/core/themes/claro/js/dropbutton.js b/core/themes/claro/js/dropbutton.js index 7fb87d60c25c89067993df6e5068c83ab4258c97..106e1b58c2160162a1c24ca0a5c3b891b387d7a9 100644 --- a/core/themes/claro/js/dropbutton.js +++ b/core/themes/claro/js/dropbutton.js @@ -19,5 +19,5 @@ * A string representing a DOM fragment. */ Drupal.theme.dropbuttonToggle = (options) => - `<li class="dropbutton-toggle"><button type="button" class="dropbutton__toggle"><span class="visually-hidden">${options.title}</span></button></li>`; + `<li class="dropbutton-toggle"><button type="button" data-drupal-dropbutton-toggle class="dropbutton__toggle"><span class="visually-hidden">${options.title}</span></button></li>`; })(Drupal);