diff --git a/claro.info.yml b/claro.info.yml index 7cfd751..f86f02b 100644 --- a/claro.info.yml +++ b/claro.info.yml @@ -44,6 +44,8 @@ libraries-override: css: component: /core/themes/stable/css/core/vertical-tabs.css: false + js: + misc/vertical-tabs.js: js/claro.vertical-tabs.js core/jquery.ui: css: @@ -118,10 +120,10 @@ libraries-extend: - claro/details-focus core/drupal.dropbutton: - claro/drupal-theme - core/drupal.vertical-tabs: - - claro/vertical-tabs core/drupal.tableselect: - claro/drupal.tableselect + core/drupal.vertical-tabs: + - claro/vertical-tabs core/jquery.ui: - claro/claro.jquery.ui media_library/view: diff --git a/claro.libraries.yml b/claro.libraries.yml index b7ee0bf..b5d7c39 100644 --- a/claro.libraries.yml +++ b/claro.libraries.yml @@ -103,16 +103,6 @@ drupal.responsive-detail: - core/jquery.once - core/collapse -vertical-tabs: - version: VERSION - css: - component: - css/dist/components/vertical-tabs.css: {} - js: - js/vertical-tabs.js: {} - dependencies: - - core/jquery - claro.jquery.ui: version: VERSION css: @@ -226,3 +216,16 @@ views: css: component: css/dist/components/views-exposed-form.css: {} + +vertical-tabs: + css: + component: + css/dist/components/vertical-tabs.css: {} + drupalSettings: + verticalTabs: + paneClasses: [] + # - vertical-tabs__pane + paneWrapperClasses: + - vertical-tabs__items--processed + dependencies: + - claro/global-styling diff --git a/claro.theme b/claro.theme index 4cd9316..80ca173 100644 --- a/claro.theme +++ b/claro.theme @@ -24,6 +24,15 @@ function claro_theme_suggestions_form_element_alter(&$suggestions, $variables) { } } +/** + * Implements hook_theme_suggestions_HOOK_alter() for details. + */ +function claro_theme_suggestions_details_alter(&$suggestions, $variables) { + if (!empty($variables['element']['#vertical_tab_item'])) { + $suggestions[] = 'details__vertical_tabs'; + } +} + /** * Implements hook_preprocess_HOOK() for HTML document templates. */ @@ -259,7 +268,10 @@ function claro_element_info_alter(&$type) { } if (isset($type['vertical_tabs'])) { - $type['vertical_tabs']['#pre_render'][] = '_claro_vertical_tabs_prerender'; + $vertical_tabs_pre_renders = !empty($type['vertical_tabs']['#pre_render']) ? $type['vertical_tabs']['#pre_render'] : []; + array_unshift($vertical_tabs_pre_renders, '_claro_vertical_tabs_prerender'); + + $type['vertical_tabs']['#pre_render'] = $vertical_tabs_pre_renders; } } @@ -269,27 +281,46 @@ function claro_element_info_alter(&$type) { function _claro_vertical_tabs_prerender($element) { // If the vertical tabs have a details group, add attributes to those details // elements so they are styled as accordion items and have BEM classes. - if (isset($element['group']['#type']) && $element['group']['#type'] === 'details') { - if (isset($element['group']['#groups']) && is_array($element['group']['#groups'])) { - $group_keys = Element::children($element['group']['#groups']); - foreach($group_keys as $group_key) { - $children_keys = Element::children($element['group']['#groups'][$group_key]); - foreach ($children_keys as $key) { - $type = isset($element['group']['#groups'][$group_key][$key]['#type']) ? $element['group']['#groups'][$group_key][$key]['#type'] : FALSE; - if ($type === 'details') { - // Add BEM class to specify the details element is in a vertical - // tabs group. - $element['group']['#groups'][$group_key][$key]['#attributes']['class'][] = 'vertical_tabs__details'; - - // Add attributes so the details elements are styled as accordion - // items. - $element['group']['#groups'][$group_key][$key]['#attributes']['class'][] = 'accordion__item'; - $element['group']['#groups'][$group_key][$key]['#accordion_item'] = TRUE; + if ( + isset($element['group']['#type']) && + $element['group']['#type'] === 'details' && + isset($element['group']['#groups']) && + is_array($element['group']['#groups']) + ) { + $group_keys = Element::children($element['group']['#groups'], TRUE); + $first_key = TRUE; + $last_group_with_child_key = NULL; + $last_group_with_child_key_last_child_key = NULL; + + foreach($group_keys as $group_key) { + $children_keys = Element::children($element['group']['#groups'][$group_key], TRUE); + + foreach ($children_keys as $child_key) { + $last_group_with_child_key = $group_key; + $type = isset($element['group']['#groups'][$group_key][$child_key]['#type']) ? $element['group']['#groups'][$group_key][$child_key]['#type'] : FALSE; + if ($type === 'details') { + // Add BEM class to specify the details element is in a vertical + // tabs group. + $element['group']['#groups'][$group_key][$child_key]['#attributes']['class'][] = 'vertical-tabs__item'; + $element['group']['#groups'][$group_key][$child_key]['#vertical_tab_item'] = TRUE; + + if ($first_key) { + $element['group']['#groups'][$group_key][$child_key]['#attributes']['class'][] = 'vertical-tabs__item--first'; + $first_key = FALSE; } + + $last_group_with_child_key_last_child_key = $child_key; } } } } + + if ($last_group_with_child_key && $last_group_with_child_key_last_child_key) { + $element['group']['#groups'][$last_group_with_child_key][$last_group_with_child_key_last_child_key]['#attributes']['class'][] = 'vertical-tabs__item--last'; + } + + $element['#attributes']['class'][] = 'vertical-tabs__items'; + return $element; } diff --git a/css/src/components/details.css b/css/src/components/details.css index 66bdfa8..7475d1a 100644 --- a/css/src/components/details.css +++ b/css/src/components/details.css @@ -9,13 +9,10 @@ * Available modifiers are: * - .claro-details--accordion * - .claro-details--accordion-item + * - .claro-details--vertical-tabs-item * - * 'Accordion item' is used for the details of the node edit sidebar. For creating - * accordion item list from a set of details, set the surrounding Container - * render element's '#accordion' key to TRUE. - * - * The other variant is 'accordion'. Despite the fact that it isn't used - * anywhere right now, we implemented it (since the design defines that). + * Despite the fact that 'accordion' isn't used anywhere right now, we + * implemented it (since the design defines that). * This variant can be used by setting the '#accordion' to TRUE for a * Details render element: * @code @@ -23,6 +20,13 @@ * '#type' => 'details', * '#accordion' => TRUE, * ]; + * + * 'Accordion item' is used for the details of the node edit sidebar. For + * creating accordion item list from a set of details, set the surrounding + * Container render element's '#accordion' key to TRUE. + * + * 'Vertical tabs item' is used for the children of the VerticalTabs render + * element. */ :root { @@ -41,7 +45,8 @@ box-shadow: var(--details-box-shadow); } -.claro-details--accordion-item { +.claro-details--accordion-item, +.claro-details--vertical-tabs-item { margin-top: 0; margin-bottom: 0; border-radius: 0; @@ -82,12 +87,14 @@ /* Modifiers */ .claro-details__summary--accordion, -.claro-details__summary--accordion-item { +.claro-details__summary--accordion-item, +.claro-details__summary--vertical-tabs-item { padding: var(--space-l) var(--space-l) var(--space-l) var(--details-desktop-wrapper-padding-start); /* LTR */ background: var(--color-white); } [dir="rtl"] .claro-details__summary--accordion, -[dir="rtl"] .claro-details__summary--accordion-item { +[dir="rtl"] .claro-details__summary--accordion-item, +[dir="rtl"] .claro-details__summary--vertical-tabs-item { padding-right: var(--details-desktop-wrapper-padding-start); padding-left: var(--space-l); } @@ -287,11 +294,12 @@ * Active has to be here for Firefox. * Merges standard collapse-processed selectors. */ - [open] > .claro-details__summary--accordion:not(:focus):not(:active)::after, [open] > .claro-details__summary--accordion-item:not(:focus):not(:active)::after, .collapse-processed[open] > .claro-details__summary--accordion .details-title:not(:focus)::after, -.collapse-processed[open] > .claro-details__summary--accordion-item .details-title:not(:focus)::after { +.collapse-processed[open] > .claro-details__summary--accordion-item .details-title:not(:focus)::after, +[open] > .claro-details__summary--vertical-tabs-item:not(:focus):not(:active)::after, +.collapse-processed[open] > .claro-details__summary--vertical-tabs-item .details-title:not(:focus)::after { box-shadow: none; border: 3px solid var(--color-absolutezero); border-width: 0 0 0 3px; /* LTR */ @@ -300,7 +308,9 @@ [dir="rtl"] [open] > .claro-details__summary--accordion:not(:focus)::after, [dir="rtl"] [open] > .claro-details__summary--accordion-item:not(:focus)::after, [dir="rtl"] .collapse-processed[open] > .claro-details__summary--accordion .details-title:not(:focus)::after, -[dir="rtl"] .collapse-processed[open] > .claro-details__summary--accordion-item .details-title:not(:focus)::after { +[dir="rtl"] .collapse-processed[open] > .claro-details__summary--accordion-item .details-title:not(:focus)::after, +[dir="rtl"] [open] > .claro-details__summary--vertical-tabs-item:not(:focus)::after, +[dir="rtl"] .collapse-processed[open] > .claro-details__summary--vertical-tabs-item .details-title:not(:focus)::after { border-width: 0 3px 0 0; } @@ -346,13 +356,16 @@ } .claro-details__wrapper--accordion, -.claro-details__wrapper--accordion-item { +.claro-details__wrapper--accordion-item, +.claro-details__wrapper--vertical-tabs-item { margin: 0; } .claro-details__wrapper--accordion::before, .claro-details__wrapper--accordion::after, .claro-details__wrapper--accordion-item::before, -.claro-details__wrapper--accordion-item::after { +.claro-details__wrapper--accordion-item::after, +.claro-details__wrapper--vertical-tabs-item::before, +.claro-details__wrapper--vertical-tabs-item::after { display: table; clear: both; content: ""; @@ -364,18 +377,21 @@ } .claro-details__wrapper--accordion, - .claro-details__wrapper--accordion-item { + .claro-details__wrapper--accordion-item, + .claro-details__wrapper--vertical-tabs-item { margin: 0; } } .claro-details__content--accordion, -.claro-details__content--accordion-item { +.claro-details__content--accordion-item, +.claro-details__content--vertical-tabs-item { margin: var(--space-m) var(--space-m) var(--space-l); } .claro-details__wrapper--accordion, -.claro-details__wrapper--accordion-item { +.claro-details__wrapper--accordion-item, +.claro-details__wrapper--vertical-tabs-item { background-color: var(--color-whitesmoke-o-40); border-top: var(--details-border-size) solid var(--details-border-color); } diff --git a/css/src/components/vertical-tabs.css b/css/src/components/vertical-tabs.css index c65bab5..acd4ca1 100644 --- a/css/src/components/vertical-tabs.css +++ b/css/src/components/vertical-tabs.css @@ -7,34 +7,88 @@ --vtabs-trigger-border: 1px solid rgba(216, 217, 224, 0.8); --vtabs-invisible-border: 1px solid transparent; --shadow-trigger-active-vertical-tab: 0 2px 4px rgba(0, 0, 0, 0.1); - --vtabs-minumum-width: 15rem; /* 240px */ + --vtabs-min-width: 15rem; /* 240px */ --vtabs-narrow-details-padding: 1.266rem; /* ~20px */ --vtabs-narrow-details-padding-disclosure: 2.8125rem; /* 45px */ --vtabs-narrow-summary-line-height: 1.266rem; /* ~20px */ --vtabs-active-tab-mask-width: 0.625rem; /* 10px */ } -.vertical-tabs { +.vertical-tabs__items { display: flex; margin: var(--space-s) 0; + flex-direction: column; + background-color: var(--color-white); + color: var(--color-text); + border: var(--details-border-size) solid var(--details-border-color); + border-radius: var(--details-accordion-border-size-radius); + box-shadow: var(--details-box-shadow); +} + +.vertical-tabs { + display: flex; + flex-direction: column; +} + +@media screen and (min-width: 36em) { + .vertical-tabs { + flex-direction: row; + } +} + +.vertical-tabs__item { + margin: 0 -1px; + border-radius: 0; +} + +.vertical-tabs__item--first { + margin-top: -1px; + border-top-right-radius: var(--details-accordion-border-size-radius); + border-top-left-radius: var(--details-accordion-border-size-radius); +} + +.vertical-tabs__item + .vertical-tabs__item { + margin-top: -1px; +} + +@media screen and (min-width: 36em) { + .vertical-tabs__item + .vertical-tabs__item { + margin-top: 0; + } +} + +.vertical-tabs__item--last { + margin-bottom: -1px; + border-bottom-right-radius: var(--details-accordion-border-size-radius); + border-bottom-left-radius: var(--details-accordion-border-size-radius); } /* Tab menu. */ .vertical-tabs__menu { list-style: none; max-width: 40%; - min-width: var(--vtabs-minumum-width); + min-width: var(--vtabs-min-width); line-height: 1; margin: 0; + display: none; } [dir="rtl"] .vertical-tabs__menu { margin: 0; /* Overrides default [dir="rtl"] ul margin */ } +@media screen and (min-width: 36em) { + .vertical-tabs__menu { + display: block; + } +} + /* Tab menu item. */ .vertical-tabs__menu-item { position: relative; - margin-bottom: -1px; +} + +.vertical-tabs__menu-item + .vertical-tabs__menu-item { + margin-top: -1px; } /* Tab menu item anchor. */ @@ -63,10 +117,12 @@ border-bottom: var(--vtabs-invisible-border); } -.vertical-tabs__menu-item-summary { +.vertical-tabs__menu-item-summary, +.vertical-tabs__summary-summary { display: block; font-size: var(--font-size-s); color: var(--color-davysgrey); + font-weight: normal; } /* Tab menu item selected. */ @@ -130,44 +186,45 @@ } /* Tab panes. */ -.vertical-tabs__panes { - padding: var(--space-l); +.vertical-tabs__items--processed { border: var(--vtabs-trigger-border); border-radius: 0 var(--base-border-radius) var(--base-border-radius) var(--base-border-radius); /* LTR */ background-color: var(--color-white); box-shadow: var(--shadow-trigger-active-vertical-tab); flex-grow: 1; z-index: 1; + margin-top: 0; + margin-bottom: 0; } -[dir="rtl"] .vertical-tabs__panes { +[dir="rtl"] .vertical-tabs__items--processed { border-radius: var(--base-border-radius) 0 var(--base-border-radius) var(--base-border-radius); } -.vertical-tabs__pane { - margin: 0; - padding: 0; - border: 0; - box-shadow: none; -} +@media screen and (min-width: 36em) { + .vertical-tabs__item { + border: 0; + margin: 0; + } -.vertical-tabs__pane > summary { - display: none; -} + .vertical-tabs__item.is-collapsed { + display: none; + } -.vertical-tabs__pane > .details-wrapper { - margin: 0; -} + .vertical-tabs__item > summary { + display: none; + } -.vertical-tabs__pane .claro-details__content > .form-item { - margin-top: 0; - margin-bottom: 0; + .claro-details__wrapper--vertical-tabs-item { + border-top: 0; + background: #fea; + } } /* * Set width to 100% so an input's size attribute does not cause the tabs to * exceed the width of the page. */ -.vertical-tabs__pane .claro-details__content .form-element { +.vertical-tabs__item .claro-details__content .form-element { width: 100%; } @@ -176,7 +233,7 @@ * should get a max-width that resembles the width dictated by the default * size attribute. */ -.vertical-tabs__pane .claro-details__content .form-element--type-text { +.vertical-tabs__item .claro-details__content .form-element--type-text { max-width: 31.25rem; } @@ -186,13 +243,15 @@ * greater than or equal to drupalSettings.widthBreakpoint (when defined) or * 640px (when drupalSettings.widthBreakpoint is not defined). */ + +/* .vertical-tabs--default > details { margin-top: 0; margin-bottom: 0; } .vertical-tabs--default > details > summary { - padding: var(--vtabs-narrow-details-padding) var(--vtabs-narrow-details-padding) var(--vtabs-narrow-details-padding) var(--vtabs-narrow-details-padding-disclosure); /* 20px 20px 20px 45px */ + padding: var(--vtabs-narrow-details-padding) var(--vtabs-narrow-details-padding) var(--vtabs-narrow-details-padding) var(--vtabs-narrow-details-padding-disclosure); } .vertical-tabs--default > details[open] > summary { @@ -225,4 +284,9 @@ .vertical-tabs--default .claro-details__content--accordion-item, .vertical-tabs--default .claro-details__content--accordion-item > .form-item { margin: 0; +} */ + +.claro-details__summary--vertical-tabs-item:focus { + position: relative; + z-index: 1; } diff --git a/js/claro.vertical-tabs.es6.js b/js/claro.vertical-tabs.es6.js new file mode 100644 index 0000000..04b1c64 --- /dev/null +++ b/js/claro.vertical-tabs.es6.js @@ -0,0 +1,351 @@ +/** + * @file + * Define vertical tabs functionality. + */ + +/** + * Triggers when form values inside a vertical tab changes. + * + * This is used to update the summary in vertical tabs in order to know what + * are the important fields' values. + * + * @event summaryUpdated + */ + +(($, Drupal) => { + /** + * 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('.js-vertical-tabs-pane').each((index, pane) => { + $(pane) + .data('verticalTab') + .focus(); + }); + }; + + /** + * This script transforms a set of details into a stack of vertical tabs. + * + * Each tab may have a summary which can be updated by another + * script. For that to work, each details element has an associated + * 'verticalTabCallback' (with jQuery.data() attached to the details), + * which is called every time the user performs an update to a form + * element inside the tab pane. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches behaviors for vertical tabs. + */ + Drupal.behaviors.claroVerticalTabs = { + attach(context, settings) { + const verticalTabsSettings = { + paneClasses: ['vertical-tabs__pane'], + paneWrapperClasses: ['vertical-tabs__panes'], + }; + $.extend(verticalTabsSettings, settings.verticalTabs || {}); + + /** + * 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 initializeVerticalTabs() { + const $this = $(this).addClass(verticalTabsSettings.paneWrapperClasses.join(' ')); + const focusID = $this.find(':hidden.vertical-tabs__active-tab').val(); + let tabFocus; + + // Check if there are some details that can be converted to + // vertical-tabs. + const $details = $this.find('> details'); + if ($details.length === 0) { + return; + } + + // Create the tab column. + const tabList = $(Drupal.theme.verticalTabListWrapper()); + $this + .wrap(Drupal.theme.verticalTabsWrapper()) + .before(tabList); + + // Transform each details into a tab. + $details.each(function initializeVerticalTabItems() { + const $that = $(this); + /* eslint-disable new-cap */ + const verticalTab = new Drupal.verticalTab({ + title: $that.find('> summary').text(), + details: $that, + }); + /* eslint-enable new-cap */ + tabList.append(verticalTab.item); + $that + // prop() can't be used on browsers not supporting details + // element, the style won't apply to them if prop() is used. + .removeAttr('open') + .addClass(verticalTabsSettings.paneClasses.join(' ')) + .addClass('js-vertical-tabs-pane') + .data('verticalTab', verticalTab); + if (this.id === focusID) { + tabFocus = $that; + } + }); + + if (!tabFocus) { + // If the current URL has a fragment and one of the tabs contains an + // element that matches the URL fragment, activate that tab. + const $locationHash = $this.find(window.location.hash); + if (window.location.hash && $locationHash.length) { + tabFocus = $locationHash.is('.js-vertical-tabs-pane') ? $locationHash : $locationHash.closest('.js-vertical-tabs-pane'); + } + else { + tabFocus = $this.find('> .js-vertical-tabs-pane').eq(0); + } + } + if (tabFocus.length) { + tabFocus.data('verticalTab').focus(); + tabFocus.data('verticalTab').details.find('summary').trigger('click.detailsAria'); + } + }); + }, + }; + + /** + * The vertical tab object represents a single tab within a tab group. + * + * @constructor + * + * @param {object} settings + * Settings object. + * @param {string} settings.title + * The name of the tab. + * @param {jQuery} settings.details + * The jQuery object of the details element that is the tab pane. + * + * @fires event:summaryUpdated + * + * @listens event:summaryUpdated + */ + Drupal.verticalTab = function verticalTab(settings) { + const self = this; + $.extend(this, settings, Drupal.theme('verticalTab', settings)); + + this.item.addClass('js-vertical-tabs-menu-item'); + + this.link.attr('href', `#${settings.details.attr('id')}`); + + this.detailsSummaryDescription = $(Drupal.theme.verticalTabDetailsDescription()) + .appendTo(this.details.children('summary')); + + this.link.on('click', (event) => { + event.preventDefault(); + self.focus(); + }); + + this.details.on('toggle', (event) => { + if (event.target.open) { + this.link.trigger('click'); + } + // If none of the details are opened, we have to reopen the current one. + else if (this.details.parent().find('details[open]').length === 0) { + this.details.find('summary').trigger('click'); + } + }); + + // Keyboard events added: + // Pressing the Enter key will open the tab pane. + this.link.on('keydown', (event) => { + if (event.keyCode === 13) { + event.preventDefault(); + self.focus(); + // Set focus on the first input field of the visible details/tab pane. + $('.js-vertical-tabs-pane :input:visible:enabled') + .eq(0) + .trigger('focus'); + } + }); + + this.details + .on('summaryUpdated', () => { + self.updateSummary(); + }); + }; + + Drupal.verticalTab.prototype = { + /** + * Displays the tab's content pane. + */ + focus() { + this.details + .siblings('.js-vertical-tabs-pane') + .each(function closeOtherTabs() { + const tab = $(this).data('verticalTab'); + if (tab.details.attr('open')) { + tab.details + .children('summary') + .trigger('click'); + tab.item.removeClass('is-selected'); + } + }) + .end() + .siblings(':hidden.vertical-tabs__active-tab') + .val(this.details.attr('id')); + + if (!this.details.attr('open')) { + this.details.children('summary').trigger('click'); + } + + this.item.addClass('is-selected'); + // Mark the active tab for screen readers. + $('#active-vertical-tab').remove(); + this.link.append($(Drupal.theme.verticalTabActiveTabIndicator()) + .attr('id', 'active-vertical-tab')); + }, + + /** + * Updates the tab's summary. + */ + updateSummary() { + const summary = this.details.drupalGetSummary(); + this.detailsSummaryDescription.html(summary); + this.summary.html(summary); + }, + + /** + * Shows a vertical tab pane. + * + * @return {Drupal.verticalTab} + * The verticalTab instance. + */ + tabShow() { + // Display the tab. + this.item.show(); + // Show the vertical tabs. + this.item.closest('.js-form-type-vertical-tabs').show(); + // Update .first marker for items. We need recurse from parent to retain + // the actual DOM element order as jQuery implements sortOrder, but not + // as public method. + this.item + .parent() + .children('.js-vertical-tabs-menu-item') + .filter(':visible') + .eq(0); + // Display the details element. + this.details.removeClass('vertical-tab--hidden').show(); + // Focus this tab. + this.focus(); + return this; + }, + + /** + * Hides a vertical tab pane. + * + * @return {Drupal.verticalTab} + * The verticalTab instance. + */ + tabHide() { + // Hide this tab. + this.item.hide(); + // Update .first marker for items. We need recurse from parent to retain + // the actual DOM element order as jQuery implements sortOrder, but not + // as public method. + this.item + .parent() + .children('.js-vertical-tabs-menu-item') + .removeClass('first') + .filter(':visible') + .eq(0) + .addClass('first'); + // Hide the details element. + this.details.addClass('vertical-tab--hidden').hide(); + // Focus the first visible tab (if there is one). + const $firstTab = this.details + .siblings('.js-vertical-tabs-pane:not(.vertical-tab--hidden)') + .eq(0); + if ($firstTab.length) { + $firstTab.data('verticalTab').focus(); + } + // Hide the vertical tabs (if no tabs remain). + else { + this.item.closest('.js-form-type-vertical-tabs').hide(); + } + return this; + }, + }; + + /** + * Theme function for a vertical tab. + * + * @param {object} settings + * An object with the following keys: + * @param {string} settings.title + * The name of the tab. + * + * @return {object} + * This function has to return an object with at least these keys: + * - item: The root tab jQuery element + * - link: The anchor tag that acts as the clickable area of the tab + * (jQuery version) + * - summary: The jQuery element that contains the tab summary + */ + Drupal.theme.verticalTab = (settings) => { + const tab = {}; + tab.item = $('
') + .append((tab.link = $('') + .append((tab.title = $('').text(settings.title))) + .append((tab.summary = $(''))))); + return tab; + }; + + /** + * Wrapper of the menu and the panes. + * + * @return {string} + * A string representing the DOM fragment. + */ + Drupal.theme.verticalTabsWrapper = () => + ''; + + /** + * The wrapper of the vertical tab menu items. + * + * @return {string} + * A string representing the DOM fragment. + */ + Drupal.theme.verticalTabListWrapper = () => + ''; + + /** + * The wrapper of the details summary message added to the summary element. + * + * @return {string} + * A string representing the DOM fragment. + */ + Drupal.theme.verticalTabDetailsDescription = () => + ''; + + /** + * Themes the active vertical tab menu item message. + * + * @return {string} + * A string representing the DOM fragment. + */ + Drupal.theme.verticalTabActiveTabIndicator = () => + `${Drupal.t('(active tab)')}`; +})(jQuery, Drupal); diff --git a/js/claro.vertical-tabs.js b/js/claro.vertical-tabs.js new file mode 100644 index 0000000..7588f76 --- /dev/null +++ b/js/claro.vertical-tabs.js @@ -0,0 +1,182 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($, Drupal) { + var handleFragmentLinkClickOrHashChange = function handleFragmentLinkClickOrHashChange(e, $target) { + $target.parents('.js-vertical-tabs-pane').each(function (index, pane) { + $(pane).data('verticalTab').focus(); + }); + }; + + Drupal.behaviors.claroVerticalTabs = { + attach: function attach(context, settings) { + var verticalTabsSettings = { + paneClasses: ['vertical-tabs__pane'], + paneWrapperClasses: ['vertical-tabs__panes'] + }; + $.extend(verticalTabsSettings, settings.verticalTabs || {}); + + $('body').once('vertical-tabs-fragments').on('formFragmentLinkClickOrHashChange.verticalTabs', handleFragmentLinkClickOrHashChange); + + $(context).find('[data-vertical-tabs-panes]').once('vertical-tabs').each(function initializeVerticalTabs() { + var $this = $(this).addClass(verticalTabsSettings.paneWrapperClasses.join(' ')); + var focusID = $this.find(':hidden.vertical-tabs__active-tab').val(); + var tabFocus = void 0; + + var $details = $this.find('> details'); + if ($details.length === 0) { + return; + } + + var tabList = $(Drupal.theme.verticalTabListWrapper()); + $this.wrap(Drupal.theme.verticalTabsWrapper()).before(tabList); + + $details.each(function initializeVerticalTabItems() { + var $that = $(this); + + var verticalTab = new Drupal.verticalTab({ + title: $that.find('> summary').text(), + details: $that + }); + + tabList.append(verticalTab.item); + $that.removeAttr('open').addClass(verticalTabsSettings.paneClasses.join(' ')).addClass('js-vertical-tabs-pane').data('verticalTab', verticalTab); + if (this.id === focusID) { + tabFocus = $that; + } + }); + + if (!tabFocus) { + var $locationHash = $this.find(window.location.hash); + if (window.location.hash && $locationHash.length) { + tabFocus = $locationHash.is('.js-vertical-tabs-pane') ? $locationHash : $locationHash.closest('.js-vertical-tabs-pane'); + } else { + tabFocus = $this.find('> .js-vertical-tabs-pane').eq(0); + } + } + if (tabFocus.length) { + tabFocus.data('verticalTab').focus(); + tabFocus.data('verticalTab').details.find('summary').trigger('click.detailsAria'); + } + }); + } + }; + + Drupal.verticalTab = function verticalTab(settings) { + var _this = this; + + var self = this; + $.extend(this, settings, Drupal.theme('verticalTab', settings)); + + this.item.addClass('js-vertical-tabs-menu-item'); + + this.link.attr('href', '#' + settings.details.attr('id')); + + this.detailsSummaryDescription = $(Drupal.theme.verticalTabDetailsDescription()).appendTo(this.details.children('summary')); + + this.link.on('click', function (event) { + event.preventDefault(); + self.focus(); + }); + + this.details.on('toggle', function (event) { + if (event.target.open) { + _this.link.trigger('click'); + } else if (_this.details.parent().find('details[open]').length === 0) { + _this.details.find('summary').trigger('click'); + } + }); + + this.link.on('keydown', function (event) { + if (event.keyCode === 13) { + event.preventDefault(); + self.focus(); + + $('.js-vertical-tabs-pane :input:visible:enabled').eq(0).trigger('focus'); + } + }); + + this.details.on('summaryUpdated', function () { + self.updateSummary(); + }); + }; + + Drupal.verticalTab.prototype = { + focus: function focus() { + this.details.siblings('.js-vertical-tabs-pane').each(function closeOtherTabs() { + var tab = $(this).data('verticalTab'); + if (tab.details.attr('open')) { + tab.details.children('summary').trigger('click'); + tab.item.removeClass('is-selected'); + } + }).end().siblings(':hidden.vertical-tabs__active-tab').val(this.details.attr('id')); + + if (!this.details.attr('open')) { + this.details.children('summary').trigger('click'); + } + + this.item.addClass('is-selected'); + + $('#active-vertical-tab').remove(); + this.link.append($(Drupal.theme.verticalTabActiveTabIndicator()).attr('id', 'active-vertical-tab')); + }, + updateSummary: function updateSummary() { + var summary = this.details.drupalGetSummary(); + this.detailsSummaryDescription.html(summary); + this.summary.html(summary); + }, + tabShow: function tabShow() { + this.item.show(); + + this.item.closest('.js-form-type-vertical-tabs').show(); + + this.item.parent().children('.js-vertical-tabs-menu-item').filter(':visible').eq(0); + + this.details.removeClass('vertical-tab--hidden').show(); + + this.focus(); + return this; + }, + tabHide: function tabHide() { + this.item.hide(); + + this.item.parent().children('.js-vertical-tabs-menu-item').removeClass('first').filter(':visible').eq(0).addClass('first'); + + this.details.addClass('vertical-tab--hidden').hide(); + + var $firstTab = this.details.siblings('.js-vertical-tabs-pane:not(.vertical-tab--hidden)').eq(0); + if ($firstTab.length) { + $firstTab.data('verticalTab').focus(); + } else { + this.item.closest('.js-form-type-vertical-tabs').hide(); + } + return this; + } + }; + + Drupal.theme.verticalTab = function (settings) { + var tab = {}; + tab.item = $('').append(tab.link = $('').append(tab.title = $('').text(settings.title)).append(tab.summary = $(''))); + return tab; + }; + + Drupal.theme.verticalTabsWrapper = function () { + return ''; + }; + + Drupal.theme.verticalTabListWrapper = function () { + return ''; + }; + + Drupal.theme.verticalTabDetailsDescription = function () { + return ''; + }; + + Drupal.theme.verticalTabActiveTabIndicator = function () { + return '' + Drupal.t('(active tab)') + ''; + }; +})(jQuery, Drupal); \ No newline at end of file diff --git a/js/vertical-tabs.es6.js b/js/vertical-tabs.es6.js deleted file mode 100644 index 0cb8c3e..0000000 --- a/js/vertical-tabs.es6.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @file - * Claro-added vertical tabs functionality. - */ - -(function($, Drupal, drupalSettings) { - /** - * This script provides Claro-specific customizations for vertical tabs. - * - * @type {Drupal~behavior} - * - * @prop {Drupal~behaviorAttach} attach - * Attaches Claro-specific behaviors for vertical tabs. - * - * @see Drupal.behaviors.verticalTabs - */ - Drupal.behaviors.claroVerticalTabs = { - attach(context) { - const width = drupalSettings.widthBreakpoint || 640; - const mq = `(max-width: ${width}px)`; - - // See if the viewport is wider than 640px or (when present) - // drupalSettings.widthBreakpoint. - if (!window.matchMedia(mq).matches) { - $(context) - .find('[data-vertical-tabs-panes]') - .each((index, element) => { - // Drupal.behaviors.verticalTabs converts details elements to - // vertical tabs. Claro adds several classes specific to these - // details elements that need to be removed as part of this - // conversion. - const $element = $(element); - $element.removeClass('vertical-tabs--default'); - const classes = [ - 'claro-details--accordion-item', - 'accordion__item', - 'claro-details__wrapper--accordion-item', - 'claro-details__content--accordion-item', - ]; - classes.forEach(className => { - $element.find(`.${className}`).removeClass(className); - }); - }); - return; - } - - // The conditional above inclues a return; statement which means the code - // below only runs if the viewport is less than - // drupalSettings.widthBreakpoint (when present) or 640px. - $(context) - .find('[data-vertical-tabs-panes]') - .once('vertical-tabs') - .each((index, element) => { - // The vertical tabs have informational content that is added via - // Javascript. An example of this can be found on the vertical tabs - // for visibility conditions, as the descriptive text found below - // the label for the visibility condition (Not restricted, restricted - // only on certain pages, etc). - // Because Drupal.behaviors.verticalTabs does not add these labels on - // narrower viewports, the logic below adds them for Claro. - const $details = $(element).find('> details'); - if ($details.length === 0) { - return; - } - - // Get the additional tab content via drupalGetSummary(). - const summaryAdditional = $details.drupalGetSummary(); - - // If additional tab content present, add it to the summary element. - if (summaryAdditional.length > 0) { - $details - .find('summary') - .append( - ``, - ); - } - }); - }, - }; -})(jQuery, Drupal, drupalSettings); diff --git a/js/vertical-tabs.js b/js/vertical-tabs.js deleted file mode 100644 index ace4c19..0000000 --- a/js/vertical-tabs.js +++ /dev/null @@ -1,40 +0,0 @@ -/** -* DO NOT EDIT THIS FILE. -* See the following change record for more information, -* https://www.drupal.org/node/2815083 -* @preserve -**/ - -(function ($, Drupal, drupalSettings) { - Drupal.behaviors.claroVerticalTabs = { - attach: function attach(context) { - var width = drupalSettings.widthBreakpoint || 640; - var mq = '(max-width: ' + width + 'px)'; - - if (!window.matchMedia(mq).matches) { - $(context).find('[data-vertical-tabs-panes]').each(function (index, element) { - var $element = $(element); - $element.removeClass('vertical-tabs--default'); - var classes = ['claro-details--accordion-item', 'accordion__item', 'claro-details__wrapper--accordion-item', 'claro-details__content--accordion-item']; - classes.forEach(function (className) { - $element.find('.' + className).removeClass(className); - }); - }); - return; - } - - $(context).find('[data-vertical-tabs-panes]').once('vertical-tabs').each(function (index, element) { - var $details = $(element).find('> details'); - if ($details.length === 0) { - return; - } - - var summaryAdditional = $details.drupalGetSummary(); - - if (summaryAdditional.length > 0) { - $details.find('summary').append(''); - } - }); - } - }; -})(jQuery, Drupal, drupalSettings); \ No newline at end of file diff --git a/templates/navigation/details--vertical-tabs.html.twig b/templates/navigation/details--vertical-tabs.html.twig new file mode 100644 index 0000000..3a3154b --- /dev/null +++ b/templates/navigation/details--vertical-tabs.html.twig @@ -0,0 +1,77 @@ +{# +/** + * @file + * Theme override for a details element. + * + * Available variables + * - attributes: A list of HTML attributes for the details element. + * - errors: (optional) Any errors for this details element, may not be set. + * - title: (optional) The title of the element, may not be set. + * - description: (optional) The description of the element, may not be set. + * - children: (optional) The children of the element, may not be set. + * - value: (optional) The value of the element, may not be set. + * - accordion: whether the details element should look as an accordion. + * - accordion_item: whether the details element is an item of an accordion + * list. + * + * @see template_preprocess_details() + * @see claro_preprocess_details() + */ +#} +{# + Prefix 'details' class to avoid collision with Modernizr. + + @todo Remove prefix after https://www.drupal.org/node/2981732 has been solved. +#} +{% + set classes = [ + 'claro-details', + 'claro-details--vertical-tabs-item', + ] +%} +{% + set content_wrapper_classes = [ + 'claro-details__wrapper', + 'details-wrapper', + 'claro-details__wrapper--vertical-tabs-item', + ] +%} +{% + set inner_wrapper_classes = [ + 'claro-details__content', + 'claro-details__content--vertical-tabs-item', + ] +%} +