diff options
author | Sarah German <sgerman@gitlab.com> | 2022-08-17 22:46:19 +0300 |
---|---|---|
committer | Suzanne Selhorn <sselhorn@gitlab.com> | 2022-08-17 22:46:19 +0300 |
commit | 00375ef7a3c7d0b5ff7381edd7eea0160312d539 (patch) | |
tree | 8083df6aea90397a015605f11e2835945c7c3a7f | |
parent | 859ed69d6c7fe812f6d17a5bfdccaba4e3b4a11d (diff) |
Support tabbed content on documentation pages
-rw-r--r-- | content/assets/stylesheets/stylesheet.scss | 21 | ||||
-rw-r--r-- | content/frontend/default/components/tabs_section.vue | 49 | ||||
-rw-r--r-- | content/frontend/default/default.js | 37 | ||||
-rw-r--r-- | content/frontend/shared/dom.js | 22 | ||||
-rw-r--r-- | spec/frontend/default/components/tabs_section_spec.js | 27 |
5 files changed, 154 insertions, 2 deletions
diff --git a/content/assets/stylesheets/stylesheet.scss b/content/assets/stylesheets/stylesheet.scss index 2faee710..92bc43fe 100644 --- a/content/assets/stylesheets/stylesheet.scss +++ b/content/assets/stylesheets/stylesheet.scss @@ -554,3 +554,24 @@ h6[id]::before { @include gl-line-height-24; } } + +a.gl-tab-nav-item, +a.gl-tab-nav-item:hover { + border: 0; + color: inherit; +} +.gl-tab-nav-item-active:active, +.gl-tab-nav-item-active:focus, +.gl-tab-nav-item-active:focus:active { + box-shadow: inset 0 -2px 0 0 $theme-indigo-500, 0 0 0 1px #fff; +} +.gl-tabs-nav li.nav-item { + margin: 0; +} +.gl-docs .gl-tab-content { + line-height: 1.5em; + + .tab-pane p { + margin-bottom: 1.5em; + } +} diff --git a/content/frontend/default/components/tabs_section.vue b/content/frontend/default/components/tabs_section.vue new file mode 100644 index 00000000..1d0d7266 --- /dev/null +++ b/content/frontend/default/components/tabs_section.vue @@ -0,0 +1,49 @@ +<script> +import { GlTabs, GlTab, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; + +export default { + components: { + GlTabs, + GlTab, + }, + directives: { + SafeHtml, + }, + props: { + tabTitles: { + type: Array, + required: true, + }, + tabContents: { + type: Array, + required: true, + }, + }, + data() { + return { + // Allows GitLab SVGs to render through v-safe-html + // https://gitlab.com/groups/gitlab-org/-/epics/4273#svgs + safe_html_config: { ADD_TAGS: ['use'] }, + }; + }, + computed: { + // Validate that the tabset has an equal number of tab titles and tab content sections. + // We won't render the content if this is false. + hasValidContent() { + return this.tabTitles.filter(Boolean).length === this.tabContents.filter(Boolean).length; + }, + }, +}; +</script> + +<template> + <gl-tabs v-if="hasValidContent" :sync-active-tab-with-query-params="true"> + <gl-tab + v-for="(title, key) in tabTitles" + :key="key" + v-safe-html:[safe_html_config]="tabContents[key]" + :title="title" + :query-param-value="title" + /> + </gl-tabs> +</template> diff --git a/content/frontend/default/default.js b/content/frontend/default/default.js index 119842e8..5290ab52 100644 --- a/content/frontend/default/default.js +++ b/content/frontend/default/default.js @@ -1,16 +1,20 @@ import Vue from 'vue'; +import { getNextUntil } from '../shared/dom'; import NavigationToggle from './components/navigation_toggle.vue'; import VersionBanner from './components/version_banner.vue'; import { setupTableOfContents } from './setup_table_of_contents'; import VersionsMenu from './components/versions_menu.vue'; +import TabsSection from './components/tabs_section.vue'; function fixScrollPosition() { if (!window.location.hash || !document.querySelector(window.location.hash)) return; const contentBody = document.querySelector('.gl-docs main'); const scrollPositionMutationObserver = new ResizeObserver(() => { - document.scrollingElement.scrollTop = - document.querySelector(window.location.hash).getBoundingClientRect().top + window.scrollY; + if (window.location.hash) { + document.scrollingElement.scrollTop = + document.querySelector(window.location.hash).getBoundingClientRect().top + window.scrollY; + } }); scrollPositionMutationObserver.observe(contentBody); @@ -75,3 +79,32 @@ document.addEventListener('DOMContentLoaded', () => { }, }); }); + +document.addEventListener('DOMContentLoaded', () => { + const tabsetSelector = '.js-tabs'; + document.querySelectorAll(tabsetSelector).forEach((tabset) => { + const tabTitles = []; + const tabContents = []; + + const tabTitleElement = tabset.firstElementChild.tagName; + tabset.querySelectorAll(`${tabTitleElement}`).forEach((tab) => { + tabTitles.push(tab.innerText); + tabContents.push(getNextUntil(tab, tabTitleElement)); + }); + + return new Vue({ + el: tabsetSelector, + components: { + TabsSection, + }, + render(createElement) { + return createElement(TabsSection, { + props: { + tabTitles, + tabContents, + }, + }); + }, + }); + }); +}); diff --git a/content/frontend/shared/dom.js b/content/frontend/shared/dom.js index 70a07546..f4dbe540 100644 --- a/content/frontend/shared/dom.js +++ b/content/frontend/shared/dom.js @@ -20,3 +20,25 @@ export const getOuterHeight = (el) => $(el).outerHeight(); */ export const findChildByTagName = (el, tagName) => Array.from(el.childNodes).find((x) => x.tagName === tagName.toUpperCase()); + +/** + * Get HTML between two elements. + * + * @param {Element} el + * @param {String} selector + * @returns {String} HTML between the two given elements + * + * @see https://gomakethings.com/how-to-get-all-siblings-of-an-element-until-a-selector-is-found-with-vanilla-js/ + */ +export const getNextUntil = (el, selector) => { + const siblings = []; + let next = el.nextElementSibling; + + while (next) { + if (selector && next.matches(selector)) break; + siblings.push(next.outerHTML); + next = next.nextElementSibling; + } + + return siblings.join(''); +}; diff --git a/spec/frontend/default/components/tabs_section_spec.js b/spec/frontend/default/components/tabs_section_spec.js new file mode 100644 index 00000000..ec5f5058 --- /dev/null +++ b/spec/frontend/default/components/tabs_section_spec.js @@ -0,0 +1,27 @@ +/** + * @jest-environment jsdom + */ + +import { shallowMount } from '@vue/test-utils'; +import { GlTabs } from '@gitlab/ui'; +import TabsSection from '../../../../content/frontend/default/components/tabs_section.vue'; + +describe('content/frontend/default/components/tabs_section.vue', () => { + it('Tabs are visible', () => { + const propsData = { + tabTitles: ['Tab one', 'Tab two'], + tabContents: ['Tab one content', 'Tab two content'], + }; + const wrapper = shallowMount(TabsSection, { propsData }); + expect(wrapper.findComponent(GlTabs).isVisible()).toBe(true); + }); + + it('validateTabContents', () => { + const propsData = { + tabTitles: ['Tab one', ''], + tabContents: ['Tab one content', 'Tab two content'], + }; + const wrapper = shallowMount(TabsSection, { propsData }); + expect(wrapper.findComponent(GlTabs).exists()).toBe(false); + }); +}); |