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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/nav/components')
-rw-r--r--app/assets/javascripts/nav/components/responsive_app.vue107
-rw-r--r--app/assets/javascripts/nav/components/responsive_header.vue37
-rw-r--r--app/assets/javascripts/nav/components/responsive_home.vue62
-rw-r--r--app/assets/javascripts/nav/components/top_nav_app.vue29
-rw-r--r--app/assets/javascripts/nav/components/top_nav_container_view.vue39
-rw-r--r--app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue84
-rw-r--r--app/assets/javascripts/nav/components/top_nav_menu_item.vue27
-rw-r--r--app/assets/javascripts/nav/components/top_nav_menu_sections.vue63
-rw-r--r--app/assets/javascripts/nav/components/top_nav_new_dropdown.vue55
9 files changed, 394 insertions, 109 deletions
diff --git a/app/assets/javascripts/nav/components/responsive_app.vue b/app/assets/javascripts/nav/components/responsive_app.vue
new file mode 100644
index 00000000000..d601586a3f8
--- /dev/null
+++ b/app/assets/javascripts/nav/components/responsive_app.vue
@@ -0,0 +1,107 @@
+<script>
+import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants';
+import { BV_DROPDOWN_SHOW, BV_DROPDOWN_HIDE } from '~/lib/utils/constants';
+import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
+import eventHub, { EVENT_RESPONSIVE_TOGGLE } from '../event_hub';
+import { resetMenuItemsActive, hasMenuExpanded } from '../utils';
+import ResponsiveHeader from './responsive_header.vue';
+import ResponsiveHome from './responsive_home.vue';
+import TopNavContainerView from './top_nav_container_view.vue';
+
+export default {
+ components: {
+ KeepAliveSlots,
+ ResponsiveHeader,
+ ResponsiveHome,
+ TopNavContainerView,
+ },
+ props: {
+ navData: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ activeView: 'home',
+ hasMobileOverlay: false,
+ };
+ },
+ computed: {
+ nav() {
+ return resetMenuItemsActive(this.navData);
+ },
+ },
+ created() {
+ eventHub.$on(EVENT_RESPONSIVE_TOGGLE, this.updateResponsiveOpen);
+ this.$root.$on(BV_DROPDOWN_SHOW, this.showMobileOverlay);
+ this.$root.$on(BV_DROPDOWN_HIDE, this.hideMobileOverlay);
+
+ this.updateResponsiveOpen();
+ },
+ beforeDestroy() {
+ eventHub.$off(EVENT_RESPONSIVE_TOGGLE, this.onToggle);
+ this.$root.$off(BV_DROPDOWN_SHOW, this.showMobileOverlay);
+ this.$root.$off(BV_DROPDOWN_HIDE, this.hideMobileOverlay);
+ },
+ methods: {
+ updateResponsiveOpen() {
+ if (hasMenuExpanded()) {
+ document.body.classList.add('top-nav-responsive-open');
+ } else {
+ document.body.classList.remove('top-nav-responsive-open');
+ }
+ },
+ onMenuItemClick({ view }) {
+ if (view) {
+ this.activeView = view;
+ }
+ },
+ showMobileOverlay() {
+ this.hasMobileOverlay = true;
+ },
+ hideMobileOverlay() {
+ this.hasMobileOverlay = false;
+ },
+ },
+ FREQUENT_ITEMS_PROJECTS,
+ FREQUENT_ITEMS_GROUPS,
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="mobile-overlay"
+ :class="{ 'mobile-nav-open': hasMobileOverlay }"
+ data-testid="mobile-overlay"
+ ></div>
+ <keep-alive-slots :slot-key="activeView">
+ <template #home>
+ <responsive-home :nav-data="nav" @menu-item-click="onMenuItemClick" />
+ </template>
+ <template #projects>
+ <responsive-header @menu-item-click="onMenuItemClick">
+ {{ __('Projects') }}
+ </responsive-header>
+ <top-nav-container-view
+ :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_PROJECTS.namespace"
+ :frequent-items-vuex-module="$options.FREQUENT_ITEMS_PROJECTS.vuexModule"
+ container-class="gl-px-3"
+ v-bind="nav.views.projects"
+ />
+ </template>
+ <template #groups>
+ <responsive-header @menu-item-click="onMenuItemClick">
+ {{ __('Groups') }}
+ </responsive-header>
+ <top-nav-container-view
+ :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_GROUPS.namespace"
+ :frequent-items-vuex-module="$options.FREQUENT_ITEMS_GROUPS.vuexModule"
+ container-class="gl-px-3"
+ v-bind="nav.views.groups"
+ />
+ </template>
+ </keep-alive-slots>
+ </div>
+</template>
diff --git a/app/assets/javascripts/nav/components/responsive_header.vue b/app/assets/javascripts/nav/components/responsive_header.vue
new file mode 100644
index 00000000000..8a1d21993b7
--- /dev/null
+++ b/app/assets/javascripts/nav/components/responsive_header.vue
@@ -0,0 +1,37 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+import TopNavMenuItem from './top_nav_menu_item.vue';
+
+export default {
+ components: {
+ TopNavMenuItem,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ computed: {
+ menuItem() {
+ return {
+ id: 'home',
+ view: 'home',
+ icon: 'angle-left',
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <header class="gl-py-4 gl-display-flex gl-align-items-center">
+ <top-nav-menu-item
+ v-gl-tooltip="{ title: s__('TopNav|Go back') }"
+ class="gl-p-3!"
+ :menu-item="menuItem"
+ icon-only
+ @click="$emit('menu-item-click', menuItem)"
+ />
+ <span class="gl-font-size-h2 gl-font-weight-bold gl-ml-2">
+ <slot></slot>
+ </span>
+ </header>
+</template>
diff --git a/app/assets/javascripts/nav/components/responsive_home.vue b/app/assets/javascripts/nav/components/responsive_home.vue
new file mode 100644
index 00000000000..c8f2f0bfb10
--- /dev/null
+++ b/app/assets/javascripts/nav/components/responsive_home.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+import TopNavMenuItem from './top_nav_menu_item.vue';
+import TopNavMenuSections from './top_nav_menu_sections.vue';
+import TopNavNewDropdown from './top_nav_new_dropdown.vue';
+
+const NEW_VIEW = 'new';
+const SEARCH_VIEW = 'search';
+
+export default {
+ components: {
+ TopNavMenuItem,
+ TopNavMenuSections,
+ TopNavNewDropdown,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ navData: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ menuSections() {
+ return [
+ { id: 'primary', menuItems: this.navData.primary },
+ { id: 'secondary', menuItems: this.navData.secondary },
+ ].filter((x) => x.menuItems?.length);
+ },
+ newDropdownViewModel() {
+ return this.navData.views[NEW_VIEW];
+ },
+ searchMenuItem() {
+ return this.navData.views[SEARCH_VIEW];
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <header class="gl-display-flex gl-align-items-center gl-py-4 gl-pl-4">
+ <h1 class="gl-m-0 gl-font-size-h2 gl-reset-color gl-mr-auto">{{ __('Menu') }}</h1>
+ <top-nav-menu-item
+ v-if="searchMenuItem"
+ v-gl-tooltip="{ title: searchMenuItem.title }"
+ class="gl-ml-3"
+ :menu-item="searchMenuItem"
+ icon-only
+ />
+ <top-nav-new-dropdown
+ v-if="newDropdownViewModel"
+ v-gl-tooltip="{ title: newDropdownViewModel.title }"
+ :view-model="newDropdownViewModel"
+ class="gl-ml-3"
+ />
+ </header>
+ <top-nav-menu-sections class="gl-h-full" :sections="menuSections" v-on="$listeners" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_app.vue b/app/assets/javascripts/nav/components/top_nav_app.vue
index f8f3ba26536..08a2c6952c8 100644
--- a/app/assets/javascripts/nav/components/top_nav_app.vue
+++ b/app/assets/javascripts/nav/components/top_nav_app.vue
@@ -1,16 +1,12 @@
<script>
-import { GlNav, GlNavItemDropdown, GlDropdownForm, GlTooltip } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { GlNav, GlNavItemDropdown, GlDropdownForm } from '@gitlab/ui';
import TopNavDropdownMenu from './top_nav_dropdown_menu.vue';
-const TOOLTIP = s__('TopNav|Switch to...');
-
export default {
components: {
GlNav,
GlNavItemDropdown,
GlDropdownForm,
- GlTooltip,
TopNavDropdownMenu,
},
props: {
@@ -19,15 +15,6 @@ export default {
required: true,
},
},
- methods: {
- findTooltipTarget() {
- // ### Why use a target function instead of `v-gl-tooltip`?
- // To get the tooltip to align correctly, we need it to target the actual
- // toggle button which we don't directly render.
- return this.$el.querySelector('.js-top-nav-dropdown-toggle');
- },
- },
- TOOLTIP,
};
</script>
@@ -35,10 +22,13 @@ export default {
<gl-nav class="navbar-sub-nav">
<gl-nav-item-dropdown
:text="navData.activeTitle"
- icon="dot-grid"
- menu-class="gl-mt-3! gl-max-w-none! gl-max-h-none! gl-sm-w-auto!"
+ data-qa-selector="navbar_dropdown"
+ :data-qa-title="navData.activeTitle"
+ icon="hamburger"
+ menu-class="gl-mt-3! gl-max-w-none! gl-max-h-none! gl-sm-w-auto! js-top-nav-dropdown-menu"
toggle-class="top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!"
no-flip
+ no-caret
>
<gl-dropdown-form>
<top-nav-dropdown-menu
@@ -48,12 +38,5 @@ export default {
/>
</gl-dropdown-form>
</gl-nav-item-dropdown>
- <gl-tooltip
- boundary="window"
- :boundary-padding="0"
- :target="findTooltipTarget"
- placement="right"
- :title="$options.TOOLTIP"
- />
</gl-nav>
</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_container_view.vue b/app/assets/javascripts/nav/components/top_nav_container_view.vue
index 21ff3ebcd7d..6f98f85ff90 100644
--- a/app/assets/javascripts/nav/components/top_nav_container_view.vue
+++ b/app/assets/javascripts/nav/components/top_nav_container_view.vue
@@ -2,14 +2,15 @@
import FrequentItemsApp from '~/frequent_items/components/app.vue';
import eventHub from '~/frequent_items/event_hub';
import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
-import TopNavMenuItem from './top_nav_menu_item.vue';
+import TopNavMenuSections from './top_nav_menu_sections.vue';
export default {
components: {
FrequentItemsApp,
- TopNavMenuItem,
+ TopNavMenuSections,
VuexModuleProvider,
},
+ inheritAttrs: false,
props: {
frequentItemsVuexModule: {
type: String,
@@ -19,6 +20,11 @@ export default {
type: String,
required: true,
},
+ containerClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
linksPrimary: {
type: Array,
required: false,
@@ -31,11 +37,11 @@ export default {
},
},
computed: {
- linkGroups() {
+ menuSections() {
return [
- { key: 'primary', links: this.linksPrimary },
- { key: 'secondary', links: this.linksSecondary },
- ].filter((x) => x.links?.length);
+ { id: 'primary', menuItems: this.linksPrimary },
+ { id: 'secondary', menuItems: this.linksSecondary },
+ ].filter((x) => x.menuItems?.length);
},
},
mounted() {
@@ -49,26 +55,17 @@ export default {
<template>
<div class="top-nav-container-view gl-display-flex gl-flex-direction-column">
- <div class="frequent-items-dropdown-container gl-w-auto">
+ <div
+ class="frequent-items-dropdown-container gl-w-auto"
+ :class="containerClass"
+ data-testid="frequent-items-container"
+ >
<div class="frequent-items-dropdown-content gl-w-full! gl-pt-0!">
<vuex-module-provider :vuex-module="frequentItemsVuexModule">
<frequent-items-app v-bind="$attrs" />
</vuex-module-provider>
</div>
</div>
- <div
- v-for="({ key, links }, groupIndex) in linkGroups"
- :key="key"
- :class="{ 'gl-mt-3': groupIndex !== 0 }"
- class="gl-mt-auto gl-pt-3 gl-border-1 gl-border-t-solid gl-border-gray-100"
- data-testid="menu-item-group"
- >
- <top-nav-menu-item
- v-for="(link, linkIndex) in links"
- :key="link.title"
- :menu-item="link"
- :class="{ 'gl-mt-1': linkIndex !== 0 }"
- />
- </div>
+ <top-nav-menu-sections class="gl-mt-auto" :sections="menuSections" with-top-border />
</div>
</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue b/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
index 1cbd64b501d..cac8fecb6b1 100644
--- a/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
+++ b/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
@@ -1,17 +1,15 @@
<script>
+import { cloneDeep } from 'lodash';
import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants';
import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
import TopNavContainerView from './top_nav_container_view.vue';
-import TopNavMenuItem from './top_nav_menu_item.vue';
-
-const ACTIVE_CLASS = 'gl-shadow-none! gl-font-weight-bold! active';
-const SECONDARY_GROUP_CLASS = 'gl-pt-3 gl-mt-3 gl-border-1 gl-border-t-solid gl-border-gray-100';
+import TopNavMenuSections from './top_nav_menu_sections.vue';
export default {
components: {
KeepAliveSlots,
TopNavContainerView,
- TopNavMenuItem,
+ TopNavMenuSections,
},
props: {
primary: {
@@ -31,29 +29,25 @@ export default {
},
},
data() {
+ // It's expected that primary & secondary never change, so these are treated as "init" props.
+ // We need to clone so that we can mutate the data without mutating the props
+ const menuSections = [
+ { id: 'primary', menuItems: cloneDeep(this.primary) },
+ { id: 'secondary', menuItems: cloneDeep(this.secondary) },
+ ].filter((x) => x.menuItems?.length);
+
return {
- activeId: '',
+ menuSections,
};
},
computed: {
- menuItemGroups() {
- return [
- { key: 'primary', items: this.primary, classes: '' },
- {
- key: 'secondary',
- items: this.secondary,
- classes: SECONDARY_GROUP_CLASS,
- },
- ].filter((x) => x.items?.length);
- },
allMenuItems() {
- return this.menuItemGroups.flatMap((x) => x.items);
- },
- activeMenuItem() {
- return this.allMenuItems.find((x) => x.id === this.activeId);
+ return this.menuSections.flatMap((x) => x.menuItems);
},
activeView() {
- return this.activeMenuItem?.view;
+ const active = this.allMenuItems.find((x) => x.active);
+
+ return active?.view;
},
menuClass() {
if (!this.activeView) {
@@ -63,67 +57,33 @@ export default {
return '';
},
},
- created() {
- // Initialize activeId based on initialization prop
- this.activeId = this.allMenuItems.find((x) => x.active)?.id;
- },
methods: {
- onClick({ id, href }) {
- // If we're a link, let's just do the default behavior so the view won't change
- if (href) {
- return;
- }
-
- this.activeId = id;
- },
- menuItemClasses(menuItem) {
- if (menuItem.id === this.activeId) {
- return ACTIVE_CLASS;
- }
-
- return '';
+ onMenuItemClick({ id }) {
+ this.allMenuItems.forEach((menuItem) => {
+ this.$set(menuItem, 'active', id === menuItem.id);
+ });
},
},
FREQUENT_ITEMS_PROJECTS,
FREQUENT_ITEMS_GROUPS,
- // expose for unit tests
- ACTIVE_CLASS,
- SECONDARY_GROUP_CLASS,
};
</script>
<template>
<div class="gl-display-flex gl-align-items-stretch">
<div
- class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10"
+ class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10 gl-py-3 gl-px-5"
:class="menuClass"
data-testid="menu-sidebar"
>
- <div
- class="gl-py-3 gl-px-5 gl-h-full gl-display-flex gl-align-items-stretch gl-flex-direction-column"
- >
- <div
- v-for="group in menuItemGroups"
- :key="group.key"
- :class="group.classes"
- data-testid="menu-item-group"
- >
- <top-nav-menu-item
- v-for="(menu, index) in group.items"
- :key="menu.id"
- data-testid="menu-item"
- :class="[{ 'gl-mt-1': index !== 0 }, menuItemClasses(menu)]"
- :menu-item="menu"
- @click="onClick(menu)"
- />
- </div>
- </div>
+ <top-nav-menu-sections :sections="menuSections" @menu-item-click="onMenuItemClick" />
</div>
<keep-alive-slots
v-show="activeView"
:slot-key="activeView"
class="gl-w-grid-size-40 gl-overflow-hidden gl-py-3 gl-px-5"
data-testid="menu-subview"
+ data-qa-selector="menu_subview_container"
>
<template #projects>
<top-nav-container-view
diff --git a/app/assets/javascripts/nav/components/top_nav_menu_item.vue b/app/assets/javascripts/nav/components/top_nav_menu_item.vue
index a0d92811a6f..08b2fbf2ed1 100644
--- a/app/assets/javascripts/nav/components/top_nav_menu_item.vue
+++ b/app/assets/javascripts/nav/components/top_nav_menu_item.vue
@@ -1,5 +1,10 @@
<script>
import { GlButton, GlIcon } from '@gitlab/ui';
+import { kebabCase, mapKeys } from 'lodash';
+
+const getDataKey = (key) => `data-${kebabCase(key)}`;
+
+const ACTIVE_CLASS = 'gl-shadow-none! gl-font-weight-bold! active';
export default {
components: {
@@ -11,7 +16,18 @@ export default {
type: Object,
required: true,
},
+ iconOnly: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ dataAttrs() {
+ return mapKeys(this.menuItem.data || {}, (value, key) => getDataKey(key));
+ },
},
+ ACTIVE_CLASS,
};
</script>
@@ -20,12 +36,17 @@ export default {
category="tertiary"
:href="menuItem.href"
class="top-nav-menu-item gl-display-block"
+ :class="[menuItem.css_class, { [$options.ACTIVE_CLASS]: menuItem.active }]"
+ :aria-label="menuItem.title"
+ v-bind="dataAttrs"
v-on="$listeners"
>
<span class="gl-display-flex">
- <gl-icon v-if="menuItem.icon" :name="menuItem.icon" class="gl-mr-2!" />
- {{ menuItem.title }}
- <gl-icon v-if="menuItem.view" name="chevron-right" class="gl-ml-auto" />
+ <gl-icon v-if="menuItem.icon" :name="menuItem.icon" :class="{ 'gl-mr-2!': !iconOnly }" />
+ <template v-if="!iconOnly">
+ {{ menuItem.title }}
+ <gl-icon v-if="menuItem.view" name="chevron-right" class="gl-ml-auto" />
+ </template>
</span>
</gl-button>
</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_menu_sections.vue b/app/assets/javascripts/nav/components/top_nav_menu_sections.vue
new file mode 100644
index 00000000000..442af512350
--- /dev/null
+++ b/app/assets/javascripts/nav/components/top_nav_menu_sections.vue
@@ -0,0 +1,63 @@
+<script>
+import TopNavMenuItem from './top_nav_menu_item.vue';
+
+const BORDER_CLASSES = 'gl-pt-3 gl-border-1 gl-border-t-solid gl-border-gray-100';
+
+export default {
+ components: {
+ TopNavMenuItem,
+ },
+ props: {
+ sections: {
+ type: Array,
+ required: true,
+ },
+ withTopBorder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ onClick(menuItem) {
+ // If we're a link, let's just do the default behavior so the view won't change
+ if (menuItem.href) {
+ return;
+ }
+
+ this.$emit('menu-item-click', menuItem);
+ },
+ getMenuSectionClasses(index) {
+ // This is a method instead of a computed so we don't have to incur the cost of
+ // creating a whole new array/object.
+ return {
+ [BORDER_CLASSES]: this.withTopBorder || index > 0,
+ 'gl-mt-3': index > 0,
+ };
+ },
+ },
+ // Expose for unit tests
+ BORDER_CLASSES,
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-stretch gl-flex-direction-column">
+ <div
+ v-for="({ id, menuItems }, sectionIndex) in sections"
+ :key="id"
+ :class="getMenuSectionClasses(sectionIndex)"
+ data-testid="menu-section"
+ >
+ <top-nav-menu-item
+ v-for="(menuItem, menuItemIndex) in menuItems"
+ :key="menuItem.id"
+ :menu-item="menuItem"
+ data-testid="menu-item"
+ class="gl-w-full"
+ :class="{ 'gl-mt-1': menuItemIndex > 0 }"
+ @click="onClick(menuItem)"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
new file mode 100644
index 00000000000..154bed81854
--- /dev/null
+++ b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlDropdown, GlDropdownDivider, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ },
+ props: {
+ viewModel: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ sections() {
+ return this.viewModel.menu_sections || [];
+ },
+ showHeaders() {
+ return this.sections.length > 1;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ toggle-class="top-nav-menu-item"
+ icon="plus"
+ :text="viewModel.title"
+ category="tertiary"
+ text-sr-only
+ no-caret
+ right
+ >
+ <template v-for="({ title, menu_items }, index) in sections">
+ <gl-dropdown-divider v-if="index > 0" :key="`${index}_divider`" data-testid="divider" />
+ <gl-dropdown-section-header v-if="showHeaders" :key="`${index}_header`" data-testid="header">
+ {{ title }}
+ </gl-dropdown-section-header>
+ <template v-for="menuItem in menu_items">
+ <gl-dropdown-item
+ :key="`${index}_item_${menuItem.id}`"
+ link-class="top-nav-menu-item"
+ :href="menuItem.href"
+ data-testid="item"
+ >
+ {{ menuItem.title }}
+ </gl-dropdown-item>
+ </template>
+ </template>
+ </gl-dropdown>
+</template>