Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-docs.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSarah German <sgerman@gitlab.com>2022-08-17 22:46:19 +0300
committerSuzanne Selhorn <sselhorn@gitlab.com>2022-08-17 22:46:19 +0300
commit00375ef7a3c7d0b5ff7381edd7eea0160312d539 (patch)
tree8083df6aea90397a015605f11e2835945c7c3a7f
parent859ed69d6c7fe812f6d17a5bfdccaba4e3b4a11d (diff)
Support tabbed content on documentation pages
-rw-r--r--content/assets/stylesheets/stylesheet.scss21
-rw-r--r--content/frontend/default/components/tabs_section.vue49
-rw-r--r--content/frontend/default/default.js37
-rw-r--r--content/frontend/shared/dom.js22
-rw-r--r--spec/frontend/default/components/tabs_section_spec.js27
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);
+ });
+});