diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 0437a2a8a9..fda9fcc207 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1175,6 +1175,16 @@ function template_preprocess_item_list(&$variables) { } } +/** + * Sends the splitbutton item list override through the item list preprocessor. + * + * @param array $variables + * The same structure as the array sent to template_preprocess_item_list(). + */ +function template_preprocess_item_list__splitbutton(array &$variables) { + template_preprocess_item_list($variables); +} + /** * Prepares variables for splitbutton templates. * diff --git a/core/lib/Drupal/Core/Render/Element/Splitbutton.php b/core/lib/Drupal/Core/Render/Element/Splitbutton.php index d402289d00..7d927aa960 100644 --- a/core/lib/Drupal/Core/Render/Element/Splitbutton.php +++ b/core/lib/Drupal/Core/Render/Element/Splitbutton.php @@ -8,6 +8,9 @@ /** * Provides a form element for a toggleable menu button. * + * Splitbutton's implementation is based on the W3C accessible menu button + * design pattern: https://www.w3.org/TR/wai-aria-practices/#menubutton. + * * Properties: * - #splitbutton_items: Items that will be themed as a splitbutton_item_list. * By default, the items can be of the following types: submit, link and @@ -26,6 +29,8 @@ * should be confirmed that there is still an element with the * `data-drupal-splitbutton-trigger` attribute, as it is necessary for * splitbutton's JavaScript. + * - #hover: If this is TRUE and #title is not empty or false, the splitbutton + * can be opened on hover. * * Deprecated Properties: * - #links: An array of links to actions. See template_preprocess_links() for @@ -99,6 +104,9 @@ public static function preRenderSplitbutton(array $element) { static::buildItemList($element, $items); $element['#splitbutton_multiple'] = TRUE; $element['#attributes']['data-drupal-splitbutton-multiple'] = ''; + if (!empty($element['#hover']) && !empty($element['#title'])) { + $element['#attributes']['data-drupal-splitbutton-hover'] = ''; + } } else { $element['#splitbutton_multiple'] = FALSE; diff --git a/core/misc/splitbutton/splitbutton-init.es6.js b/core/misc/splitbutton/splitbutton-init.es6.js index 60eec711f7..6304738f1a 100644 --- a/core/misc/splitbutton/splitbutton-init.es6.js +++ b/core/misc/splitbutton/splitbutton-init.es6.js @@ -5,14 +5,14 @@ (($, Drupal) => { /** - * Process elements with the .splitbutton class on page load. + * Process elements with the [data-drupal-splitbutton-multiple] attribute. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Attaches splitButton behaviors. */ - Drupal.behaviors.splitButton = { + Drupal.behaviors.SplitButton = { attach(context) { const $splitbuttons = $(context) .find('[data-drupal-splitbutton-multiple]') diff --git a/core/misc/splitbutton/splitbutton-init.js b/core/misc/splitbutton/splitbutton-init.js index 9bbcaff993..b09c1963f1 100644 --- a/core/misc/splitbutton/splitbutton-init.js +++ b/core/misc/splitbutton/splitbutton-init.js @@ -6,7 +6,7 @@ **/ (function ($, Drupal) { - Drupal.behaviors.splitButton = { + Drupal.behaviors.SplitButton = { attach: function attach(context) { var $splitbuttons = $(context).find('[data-drupal-splitbutton-multiple]').once('splitbutton'); diff --git a/core/misc/splitbutton/splitbutton.css b/core/misc/splitbutton/splitbutton.css index 5b8070d14f..bd97df677a 100644 --- a/core/misc/splitbutton/splitbutton.css +++ b/core/misc/splitbutton/splitbutton.css @@ -38,6 +38,14 @@ transform: translate(50%, -50%) rotate(180deg); } +[data-drupal-splitbutton-toggle-with-title] { + padding-right: 18px; /* LTR */ +} +[dir="rtl"] [data-drupal-splitbutton-toggle-with-title] { + padding-right: 6px; + padding-left: 18px; /* LTR */ +} + html:not(.js) [data-drupal-splitbutton-trigger] { display: none; } diff --git a/core/misc/splitbutton/splitbutton.es6.js b/core/misc/splitbutton/splitbutton.es6.js index 6467278b50..b1bdaa3357 100644 --- a/core/misc/splitbutton/splitbutton.es6.js +++ b/core/misc/splitbutton/splitbutton.es6.js @@ -4,6 +4,24 @@ */ ((Drupal, Popper) => { + /** + * Constructs a splitbutton UI element, based on the W3C menu button pattern. + * + * This software includes material derived from : + * - https://www.w3.org/TR/2016/WD-wai-aria-practices-1.1-20161214/examples/menu-button/menu-button-1/js/Menubutton.js + * - https://www.w3.org/TR/2016/WD-wai-aria-practices-1.1-20161214/examples/menu-button/menu-button-1/js/MenuItemAction.js + * - https://www.w3.org/TR/2016/WD-wai-aria-practices-1.1-20161214/examples/menu-button/menu-button-1/js/PopupMenuAction.js + * Copyright © 2020 W3C® (MIT, ERCIM, Keio, Beihang). + * The code was adapted for use in Drupal, to use modern ES6 syntax, and to + * meet Drupal's JavaScript code standards. + * + * @param {HTMLElement} splitbutton + * Markup that includes menu items such as links or submit inputs, and a + * button that toggles their visibility. + * + * @return {Drupal.SplitButton} + * Class representing a splitbutton UI element. + */ Drupal.SplitButton = class { constructor(splitbutton) { splitbutton.setAttribute('data-drupal-splitbutton-enabled', ''); @@ -40,13 +58,8 @@ this.splitbutton.addEventListener('focusout', () => this.focusOut()); this.splitbutton.addEventListener('keydown', e => this.keydown(e)); this.trigger.addEventListener('click', e => this.clickToggle(e)); - - // The splitbutton items use the item-list template, but should not be - // styled as an item list. Removing the class here addresses this for any - // theme displaying a splitbutton. - const itemList = splitbutton.querySelector('.item-list'); - if (itemList) { - itemList.classList.remove('item-list'); + if (this.splitbutton.hasAttribute('data-drupal-splitbutton-hover')) { + this.splitbutton.addEventListener('mouseenter', () => this.open()); } } @@ -57,7 +70,7 @@ // If this.menuItems is empty, the initialization hasn't occurred yet. if (this.menuItems.length === 0) { const itemTags = - this.menu.getAttribute('data-drupal-item-tags') || 'a, input, button'; + this.menu.getAttribute('data-drupal-splitbutton-item-tags') || 'a, input, button'; Array.prototype.slice .call(this.menu.querySelectorAll(itemTags)) .forEach((item, index) => { @@ -179,7 +192,9 @@ * Event listener for hover and focus out. */ hoverOut() { - // Wait half a second before closing. + // Wait half a second before closing, to prevent unwanted closings due to + // the pointer briefly straying from the target while moving to a new item + // within the splitbutton. this.timerID = window.setTimeout(() => this.toggle(false), 500); } diff --git a/core/misc/splitbutton/splitbutton.js b/core/misc/splitbutton/splitbutton.js index 56d651c2f0..e8b37a30a3 100644 --- a/core/misc/splitbutton/splitbutton.js +++ b/core/misc/splitbutton/splitbutton.js @@ -57,10 +57,11 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d this.trigger.addEventListener('click', function (e) { return _this.clickToggle(e); }); - var itemList = splitbutton.querySelector('.item-list'); - if (itemList) { - itemList.classList.remove('item-list'); + if (this.splitbutton.hasAttribute('data-drupal-splitbutton-hover')) { + this.splitbutton.addEventListener('mouseenter', function () { + return _this.open(); + }); } } @@ -70,7 +71,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d var _this2 = this; if (this.menuItems.length === 0) { - var itemTags = this.menu.getAttribute('data-drupal-item-tags') || 'a, input, button'; + var itemTags = this.menu.getAttribute('data-drupal-splitbutton-item-tags') || 'a, input, button'; Array.prototype.slice.call(this.menu.querySelectorAll(itemTags)).forEach(function (item, index) { item.setAttribute('data-drupal-splitbutton-item', index); item.classList.add('splitbutton__operation-list-item'); diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 7552c32210..cdebd32a8d 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -242,6 +242,17 @@ function system_theme() { 'off_canvas_page_wrapper' => [ 'variables' => ['children' => NULL], ], + 'item_list__splitbutton' => [ + 'variables' => [ + 'items' => [], + 'title' => '', + 'list_type' => 'ul', + 'wrapper_attributes' => [], + 'attributes' => [], + 'empty' => NULL, + 'context' => [], + ], + ], ]); } diff --git a/core/modules/system/templates/item-list--splitbutton.html.twig b/core/modules/system/templates/item-list--splitbutton.html.twig new file mode 100644 index 0000000000..9820125200 --- /dev/null +++ b/core/modules/system/templates/item-list--splitbutton.html.twig @@ -0,0 +1,22 @@ +{# +/** + * @file + * Default theme implementation for an item list used by splitbuttons. + * + * Available variables: + * - items: A list of items. Each item contains: + * - attributes: HTML attributes to be applied to each list item. + * - value: The content of the list element. + * - list_type: The tag for list element ("ul" or "ol"). + * - attributes: HTML attributes to be applied to the list. + * + * @ingroup themeable + */ +#} +{% if items %} + <{{ list_type }} {{ attributes }}> + {%- for item in items -%} + {{ item.value }} + {%- endfor -%} + +{%- endif %} diff --git a/core/modules/system/templates/splitbutton.html.twig b/core/modules/system/templates/splitbutton.html.twig index b074c137ab..2d116aa629 100644 --- a/core/modules/system/templates/splitbutton.html.twig +++ b/core/modules/system/templates/splitbutton.html.twig @@ -24,15 +24,16 @@ * @ingroup themeable */ #} +{% set toggle_with_title = main_element is empty and title is not empty %}
- {{ main_element }} - {% if splitbutton_multiple and exclude_toggle == FALSE %} - - {% endif %} + {{ main_element }} + {% if splitbutton_multiple and exclude_toggle == FALSE %} + + {% endif %}
{{ splitbutton_item_list }}
diff --git a/core/modules/system/tests/modules/splitbutton_test/src/Element/SplitTextField.php b/core/modules/system/tests/modules/splitbutton_test/src/Element/SplitTextField.php index ece6f9e153..bfa75a0068 100644 --- a/core/modules/system/tests/modules/splitbutton_test/src/Element/SplitTextField.php +++ b/core/modules/system/tests/modules/splitbutton_test/src/Element/SplitTextField.php @@ -90,7 +90,7 @@ public static function buildItemList(&$element, $items) { '#theme' => 'item_list', '#attributes' => [ 'data-drupal-splitbutton-target' => $trigger_id, - 'data-drupal-item-tags' => 'li', + 'data-drupal-splitbutton-item-tags' => 'li', 'aria-labelledby' => "$trigger_id-trigger", 'role' => 'listbox', 'id' => "$trigger_id-menu", diff --git a/core/modules/system/tests/modules/splitbutton_test/src/Form/SplitButtonTestForm.php b/core/modules/system/tests/modules/splitbutton_test/src/Form/SplitButtonTestForm.php index 3b03d651d1..1bb1210b88 100644 --- a/core/modules/system/tests/modules/splitbutton_test/src/Form/SplitButtonTestForm.php +++ b/core/modules/system/tests/modules/splitbutton_test/src/Form/SplitButtonTestForm.php @@ -274,6 +274,13 @@ public function buildForm(array $form, FormStateInterface $form_state, $disabled '#required' => TRUE, ]; + $form['hover_splitbutton'] = [ + '#type' => 'splitbutton', + '#splitbutton_items' => $links_plus_button, + '#title' => $this->t('Splitbutton that opens on hover'), + '#hover' => TRUE, + ]; + return $form; } diff --git a/core/profiles/demo_umami/themes/umami/templates/components/form/splitbutton.html.twig b/core/profiles/demo_umami/themes/umami/templates/components/form/splitbutton.html.twig index c4c39e0239..bddee00fb9 100644 --- a/core/profiles/demo_umami/themes/umami/templates/components/form/splitbutton.html.twig +++ b/core/profiles/demo_umami/themes/umami/templates/components/form/splitbutton.html.twig @@ -61,14 +61,16 @@ })}) %} {% endif %} +{% set toggle_with_title = main_element is empty and title is not empty %} +
- {{ main_element }} - {% if splitbutton_multiple and exclude_toggle == FALSE %} - - {% endif %} + {{ main_element }} + {% if splitbutton_multiple and exclude_toggle == FALSE %} + + {% endif %}
{{ splitbutton_item_list }} diff --git a/core/themes/bartik/templates/splitbutton.html.twig b/core/themes/bartik/templates/splitbutton.html.twig index c4c39e0239..bddee00fb9 100644 --- a/core/themes/bartik/templates/splitbutton.html.twig +++ b/core/themes/bartik/templates/splitbutton.html.twig @@ -61,14 +61,16 @@ })}) %} {% endif %} +{% set toggle_with_title = main_element is empty and title is not empty %} +
- {{ main_element }} - {% if splitbutton_multiple and exclude_toggle == FALSE %} - - {% endif %} + {{ main_element }} + {% if splitbutton_multiple and exclude_toggle == FALSE %} + + {% endif %}
{{ splitbutton_item_list }} diff --git a/core/themes/claro/css/components/splitbutton.css b/core/themes/claro/css/components/splitbutton.css index a7dbd12fb5..12a82719bf 100644 --- a/core/themes/claro/css/components/splitbutton.css +++ b/core/themes/claro/css/components/splitbutton.css @@ -25,7 +25,7 @@ */ /* * Inputs. - */ /* Absolute zero with opacity. */ /* Davy's grey with 0.6 opacity. */ /* Light gray with 0.3 opacity on white bg. */ /* Old silver with 0.5 opacity on white bg. */ /* (1/8)em ~ 2px */ /* (1/16)em ~ 1px */ /* Font size is too big to use 1rem for extrasmall line-height */ /* 7px inside the form element label. */ /* 8px with the checkbox width of 19px */ + */ /* Absolute zero with opacity. */ /* Davy's gray with 0.6 opacity. */ /* Light gray with 0.3 opacity on white bg. */ /* Old silver with 0.5 opacity on white bg. */ /* (1/8)em ~ 2px */ /* (1/16)em ~ 1px */ /* Font size is too big to use 1rem for extrasmall line-height */ /* 7px inside the form element label. */ /* 8px with the checkbox width of 19px */ /* * Details. */ @@ -131,10 +131,17 @@ html:not(.js) .splitbutton { text-align: left; text-decoration: none; color: #545560; - border-right: 1px solid #d4d4d8; - border-left: 1px solid #d4d4d8; + /* + * Styling for borders require !important due to .button having border styles + * set to !important in button.pcss.css. + */ + border-top: none !important; + border-right: 1px solid #d4d4d8 !important; + border-bottom: none !important; + border-left: 1px solid #d4d4d8 !important; border-radius: 2px; background: #fff; + box-shadow: none; font-size: 1rem; font-weight: normal; line-height: 1rem; @@ -143,10 +150,13 @@ html:not(.js) .splitbutton { .js .splitbutton__operation-list-item:focus { z-index: 5; -} - -.js .splitbutton__operation-list-item:not(:focus) { - box-shadow: none; + padding: calc(1rem - 4px) calc(1rem - 3px); + /* + * Styling for borders require !important due to .button having border styles + * set to !important in button.pcss.css. + */ + border: 3px solid #26a769 !important; + outline: none; } .js .splitbutton__operation-list-item:hover { diff --git a/core/themes/claro/css/components/splitbutton.pcss.css b/core/themes/claro/css/components/splitbutton.pcss.css index fefa3cd51e..32899f4b3e 100644 --- a/core/themes/claro/css/components/splitbutton.pcss.css +++ b/core/themes/claro/css/components/splitbutton.pcss.css @@ -75,10 +75,17 @@ html:not(.js) .splitbutton { text-align: left; text-decoration: none; color: #545560; - border-right: 1px solid var(--color-lightgray); - border-left: 1px solid var(--color-lightgray); + /* + * Styling for borders require !important due to .button having border styles + * set to !important in button.pcss.css. + */ + border-top: none !important; + border-right: 1px solid var(--color-lightgray) !important; + border-bottom: none !important; + border-left: 1px solid var(--color-lightgray) !important; border-radius: 2px; background: #fff; + box-shadow: none; font-size: 1rem; font-weight: normal; line-height: 1rem; @@ -86,9 +93,13 @@ html:not(.js) .splitbutton { } .js .splitbutton__operation-list-item:focus { z-index: 5; -} -.js .splitbutton__operation-list-item:not(:focus) { - box-shadow: none; + padding: calc(1rem - 4px) calc(1rem - 3px); + /* + * Styling for borders require !important due to .button having border styles + * set to !important in button.pcss.css. + */ + border: 3px solid var(--color-focus) !important; + outline: none; } .js .splitbutton__operation-list-item:hover { color: #222330; diff --git a/core/themes/claro/templates/form/splitbutton.html.twig b/core/themes/claro/templates/form/splitbutton.html.twig index c4c39e0239..bddee00fb9 100644 --- a/core/themes/claro/templates/form/splitbutton.html.twig +++ b/core/themes/claro/templates/form/splitbutton.html.twig @@ -61,14 +61,16 @@ })}) %} {% endif %} +{% set toggle_with_title = main_element is empty and title is not empty %} +
- {{ main_element }} - {% if splitbutton_multiple and exclude_toggle == FALSE %} - - {% endif %} + {{ main_element }} + {% if splitbutton_multiple and exclude_toggle == FALSE %} + + {% endif %}
{{ splitbutton_item_list }} diff --git a/core/themes/seven/css/components/splitbutton.css b/core/themes/seven/css/components/splitbutton.css index 06f3ff3f60..8c8f5283f3 100644 --- a/core/themes/seven/css/components/splitbutton.css +++ b/core/themes/seven/css/components/splitbutton.css @@ -102,7 +102,7 @@ text-decoration: none; background: #aae0fe; box-shadow: none; - text-shadow: 0.25px 0 0.1px, -0.25px 0 0.1px; + text-shadow: 0.1px 0 0.1px, -0.1px 0 0.1px; } .js .splitbutton__operation-list li:first-child .splitbutton__operation-list-item { diff --git a/core/themes/seven/templates/splitbutton.html.twig b/core/themes/seven/templates/splitbutton.html.twig index c4c39e0239..bddee00fb9 100644 --- a/core/themes/seven/templates/splitbutton.html.twig +++ b/core/themes/seven/templates/splitbutton.html.twig @@ -61,14 +61,16 @@ })}) %} {% endif %} +{% set toggle_with_title = main_element is empty and title is not empty %} +
- {{ main_element }} - {% if splitbutton_multiple and exclude_toggle == FALSE %} - - {% endif %} + {{ main_element }} + {% if splitbutton_multiple and exclude_toggle == FALSE %} + + {% endif %}
{{ splitbutton_item_list }} diff --git a/core/themes/stable/templates/misc/splitbutton.html.twig b/core/themes/stable/templates/misc/splitbutton.html.twig index 3b7dcbdab1..1b1bcc51bf 100644 --- a/core/themes/stable/templates/misc/splitbutton.html.twig +++ b/core/themes/stable/templates/misc/splitbutton.html.twig @@ -22,15 +22,16 @@ * @see template_preprocess_splitbutton() */ #} +{% set toggle_with_title = main_element is empty and title is not empty %}
- {{ main_element }} - {% if splitbutton_multiple and exclude_toggle == FALSE %} - - {% endif %} + {{ main_element }} + {% if splitbutton_multiple and exclude_toggle == FALSE %} + + {% endif %}
{{ splitbutton_item_list }}
diff --git a/core/themes/stable9/templates/misc/splitbutton.html.twig b/core/themes/stable9/templates/misc/splitbutton.html.twig index 3b7dcbdab1..1b1bcc51bf 100644 --- a/core/themes/stable9/templates/misc/splitbutton.html.twig +++ b/core/themes/stable9/templates/misc/splitbutton.html.twig @@ -22,15 +22,16 @@ * @see template_preprocess_splitbutton() */ #} +{% set toggle_with_title = main_element is empty and title is not empty %}
- {{ main_element }} - {% if splitbutton_multiple and exclude_toggle == FALSE %} - - {% endif %} + {{ main_element }} + {% if splitbutton_multiple and exclude_toggle == FALSE %} + + {% endif %}
{{ splitbutton_item_list }}