diff --git a/claro.libraries.yml b/claro.libraries.yml index 1171daa..93ee186 100644 --- a/claro.libraries.yml +++ b/claro.libraries.yml @@ -108,6 +108,10 @@ vertical-tabs: css: component: css/dist/components/vertical-tabs.css: {} + js: + js/vertical-tabs.js: {} + dependencies: + - core/jquery claro.jquery.ui: version: VERSION diff --git a/claro.theme b/claro.theme index 5d95d3c..9ba2c96 100644 --- a/claro.theme +++ b/claro.theme @@ -242,6 +242,40 @@ function claro_element_info_alter(&$type) { if (isset($type['dropbutton'])) { $type['dropbutton']['#pre_render'][] = '_claro_dropbutton_prerender'; } + + if (isset($type['vertical_tabs'])) { + $type['vertical_tabs']['#pre_render'][] = '_claro_vertical_tabs_prerender'; + } +} + +/** + * Prerender callback for Vertical Tabs element + */ +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; + } + } + } + } + } + return $element; } /** diff --git a/css/src/base/variables.css b/css/src/base/variables.css index dbbf3b1..1f979cf 100644 --- a/css/src/base/variables.css +++ b/css/src/base/variables.css @@ -135,8 +135,4 @@ --jui-dropdown--active-bg-color: var(--color-absolutezero); --jui-dropdown-border-color: rgba(216, 217, 224, 0.8); /* Light gray with 0.8 opacity. */ --jui-dropdown-shadow-color: rgba(34, 35, 48, 0.1); /* Text color with 0.1 opacity. */ - /** - * Vertical tabs. - */ - --shadow-trigger-active-vertical-tab: 0 0 0 rgba(0, 0, 0, 0.1); } diff --git a/css/src/components/vertical-tabs.css b/css/src/components/vertical-tabs.css index 5ab317b..c65bab5 100644 --- a/css/src/components/vertical-tabs.css +++ b/css/src/components/vertical-tabs.css @@ -3,25 +3,32 @@ * Override of misc/vertical-tabs.css. */ -/* Tab links. */ +:root { + --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-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 { - position: relative; - margin-top: 10px; - padding-bottom: 10px; /* apply padding for pane box shadow */ + display: flex; + margin: var(--space-s) 0; } /* Tab menu. */ .vertical-tabs__menu { list-style: none; - float: left; /* LTR */ - width: 320px; - margin: 0 -100% -1px 0; /* LTR */ - padding: 0; + max-width: 40%; + min-width: var(--vtabs-minumum-width); line-height: 1; + margin: 0; } [dir="rtl"] .vertical-tabs__menu { - float: right; - margin: 0 0 -1px -100%; + margin: 0; /* Overrides default [dir="rtl"] ul margin */ } /* Tab menu item. */ @@ -29,60 +36,57 @@ position: relative; margin-bottom: -1px; } -.vertical-tabs__menu-item:focus, -.vertical-tabs__menu-item:active { - z-index: 2; -} -.vertical-tabs__menu-item.first { - margin-top: 0; -} -.vertical-tabs__menu-item.is-selected { - z-index: 1; - width: 100%; - box-shadow: var(--shadow-trigger-active-vertical-tab); -} /* Tab menu item anchor. */ .vertical-tabs__menu-item a { display: block; padding: var(--space-s) var(--space-l); line-height: var(--line-height); - border-top: var(--border-tabs-trigger); - border-bottom: var(--border-tabs-trigger); + border-top: var(--vtabs-trigger-border); + border-bottom: var(--vtabs-trigger-border); text-decoration: none; color: var(--color-text); margin-left: 1px; /* LTR */ } -.vertical-tabs__menu-item a:hover { - color: var(--color-absolutezero); - background-color: var(--color-bgblue-hover); -} [dir="rtl"] .vertical-tabs__menu-item a { margin-left: 0; margin-right: 1px; } +.vertical-tabs__menu-item a:hover { + color: var(--color-absolutezero); + background-color: var(--color-bgblue-hover); +} .vertical-tabs__menu-item.first a { - border-top: 1px solid transparent; + border-top: var(--vtabs-invisible-border); } .vertical-tabs__menu-item.last a { - border-bottom: 1px solid transparent; + border-bottom: var(--vtabs-invisible-border); +} + +.vertical-tabs__menu-item-summary { + display: block; + font-size: var(--font-size-s); + color: var(--color-davysgrey); } /* Tab menu item selected. */ +.vertical-tabs__menu-item.is-selected { + box-shadow: var(--shadow-trigger-active-vertical-tab); +} + .vertical-tabs__menu-item.is-selected a { - text-decoration: none; color: var(--color-absolutezero); - box-shadow: var(--shadow-tabs-base); - border-radius: var(--base-border-radius) 0 0 var(--base-border-radius); /* LTR */ background-color: white; } [dir=rtl] .vertical-tabs__menu-item.is-selected a { - border-radius: 0 var(--base-border-radius) var(--base-border-radius) 0; + border-radius: 0 var(--tabs-base-border) var(--base-border-radius) 0; } -.vertical-tabs__menu-item.is-selected a:hover, -.vertical-tabs__menu-item.is-selected a:focus { + +.vertical-tabs__menu-item.is-selected a:hover { + background-color: var(--color-bgblue-hover); color: var(--color-absolutezero-hover); } + .vertical-tabs__menu-item.is-selected a::before { content: ''; position: absolute; @@ -90,72 +94,135 @@ left: 0; /* LTR */ bottom: 0; height: 100%; - border-left: 4px solid var(--color-input-border-focus); /* LTR */ + border-left: 4px solid var(--color-absolutezero); /* LTR */ border-radius: var(--base-border-radius) 0 0 var(--base-border-radius); /* LTR */ } [dir=rtl] .vertical-tabs__menu-item.is-selected a::before { left: 0; right: 0; border-left: none; - border-right: 4px solid var(--color-input-border-focus); + border-right: 4px solid var(--color-absolutezero); border-radius: 0 var(--base-border-radius) var(--base-border-radius) 0; } -.vertical-tabs__menu-item.is-selected a::after { + +.vertical-tabs__menu-item.is-selected::after { content: ''; z-index: 100; position: absolute; top: 1px; - right: -5px; /* LTR */ + right: -8px; /* LTR */ bottom: 1px; - border-right: 10px solid var(--color-white); /* LTR */ + border-right: var(--vtabs-active-tab-mask-width) solid var(--color-white); /* LTR */ } [dir=rtl] .vertical-tabs__menu-item.is-selected a::after { right: 0; - left: -5px; + left: -8px; border-right: none; - border-left: 10px solid var(--color-white); + border-left: var(--vtabs-active-tab-mask-width) solid var(--color-white); } + .vertical-tabs__menu-item.is-selected.first a { - border-top: var(--border-tabs-trigger); -} -.vertical-tabs__menu-item.is-selected.last a { - border-bottom: var(--border-tabs-trigger); + border-top: var(--vtabs-trigger-border); } -.vertical-tabs__menu-item-summary { - display: block; - line-height: var(--line-height); - font-size: var(--font-size-s); - color: var(--color-davysgrey); +.vertical-tabs__menu-item.is-selected.last a { + border-bottom: var(--vtabs-trigger-border); } /* Tab panes. */ .vertical-tabs__panes { - margin: 0 0 0 320px; /* LTR */ padding: var(--space-l); - border: var(--border-tabs-trigger); + 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-tabs-base); + box-shadow: var(--shadow-trigger-active-vertical-tab); + flex-grow: 1; + z-index: 1; } [dir="rtl"] .vertical-tabs__panes { - margin: 0 320px 0 0; border-radius: var(--base-border-radius) 0 var(--base-border-radius) var(--base-border-radius); } -.vertical-tabs__panes:after { - content: ""; - display: table; - clear: both; -} + .vertical-tabs__pane { margin: 0; padding: 0; border: 0; box-shadow: none; } + .vertical-tabs__pane > summary { display: none; } + .vertical-tabs__pane > .details-wrapper { margin: 0; } + +.vertical-tabs__pane .claro-details__content > .form-item { + margin-top: 0; + margin-bottom: 0; +} + +/* + * 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 { + width: 100%; +} + +/* + * Widths are set to 100% in the above rule, so single line text inputs + * 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 { + max-width: 31.25rem; +} + +/** + * Styling for .vertical-tabs--default is for mobile and non-js users. The + * vertical-tabs--default is removed by Javascript when the viewport width is + * 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 */ +} + +.vertical-tabs--default > details[open] > summary { + color: var(--color-absolutezero); + background-color: var(--color-white); + padding-top: var(--space-s); + padding-bottom: var(--space-s); +} + +.vertical-tabs--default .claro-details__summary--accordion-item { + line-height: var(--vtabs-narrow-summary-line-height); +} + +.vertical-tabs--default > details:last-of-type { + border-bottom: 1px solid var(--color-lightgray-o-80); +} + +.vertical-tabs--default > details .claro-details__wrapper { + background-color: var(--color-whitesmoke); + margin: 0; + padding: 1em; +} + +.vertical-tabs__menu-item-summary--narrow, +.vertical-tabs--default span.summary { + display: block; + font-weight: normal; +} + +.vertical-tabs--default .claro-details__content--accordion-item, +.vertical-tabs--default .claro-details__content--accordion-item > .form-item { + margin: 0; +} diff --git a/js/vertical-tabs.es6.js b/js/vertical-tabs.es6.js new file mode 100644 index 0000000..0cb8c3e --- /dev/null +++ b/js/vertical-tabs.es6.js @@ -0,0 +1,80 @@ +/** + * @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 new file mode 100644 index 0000000..ace4c19 --- /dev/null +++ b/js/vertical-tabs.js @@ -0,0 +1,40 @@ +/** +* 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/vertical-tabs.html.twig b/templates/navigation/vertical-tabs.html.twig new file mode 100644 index 0000000..d0022a8 --- /dev/null +++ b/templates/navigation/vertical-tabs.html.twig @@ -0,0 +1,13 @@ +{# +/** + * @file + * Theme override for vertical tabs. + * + * Available variables + * - attributes: A list of HTML attributes for the wrapper element. + * - children: The rendered tabs. + * + * @see template_preprocess_vertical_tabs() + */ +#} +