debian-mirror-gitlab/app/assets/javascripts/super_sidebar/components/nav_item.vue
2023-06-20 00:43:36 +05:30

220 lines
6 KiB
Vue

<script>
import { kebabCase } from 'lodash';
import { GlButton, GlCollapse, GlIcon, GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
import {
CLICK_MENU_ITEM_ACTION,
TRACKING_UNKNOWN_ID,
TRACKING_UNKNOWN_PANEL,
} from '~/super_sidebar/constants';
export default {
i18n: {
pinItem: s__('Navigation|Pin item'),
unpinItem: s__('Navigation|Unpin item'),
},
name: 'NavItem',
components: {
GlButton,
GlCollapse,
GlIcon,
GlBadge,
},
inject: {
pinnedItemIds: { default: { ids: [] } },
panelSupportsPins: { default: false },
panelType: { default: '' },
},
props: {
draggable: {
type: Boolean,
required: false,
default: false,
},
isStatic: {
type: Boolean,
required: false,
default: false,
},
item: {
type: Object,
required: true,
},
linkClasses: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
expanded: this.item.is_active,
};
},
computed: {
elem() {
return this.isSection ? 'button' : 'a';
},
collapseIcon() {
return this.expanded ? 'chevron-up' : 'chevron-down';
},
isSection() {
return Boolean(this.item?.items?.length);
},
itemId() {
return kebabCase(this.item.title);
},
pillData() {
return this.item.pill_count;
},
hasPill() {
return (
Number.isFinite(this.pillData) ||
(typeof this.pillData === 'string' && this.pillData !== '')
);
},
isActive() {
if (this.isSection) {
return !this.expanded && this.item.is_active;
}
return this.item.is_active;
},
isPinnable() {
return this.panelSupportsPins && !this.isSection && !this.isStatic;
},
isPinned() {
return this.pinnedItemIds.ids.includes(this.item.id);
},
trackingProps() {
// Set extra event data to debug missing IDs / Panel Types
const extraData =
!this.item.id || !this.panelType
? { 'data-track-extra': JSON.stringify({ title: this.item.title }) }
: {};
return {
'data-track-action': CLICK_MENU_ITEM_ACTION,
'data-track-label': this.item.id ?? TRACKING_UNKNOWN_ID,
'data-track-property': this.panelType
? `nav_panel_${this.panelType}`
: TRACKING_UNKNOWN_PANEL,
...extraData,
};
},
linkProps() {
if (this.isSection) {
return {
'aria-controls': this.itemId,
'aria-expanded': String(this.expanded),
'data-qa-menu-item': this.item.title,
};
}
return {
...this.$attrs,
...this.trackingProps,
href: this.item.link,
'aria-current': this.isActive ? 'page' : null,
'data-qa-submenu-item': this.item.title,
};
},
computedLinkClasses() {
return {
// Reset user agent styles on <button>
'gl-appearance-none gl-border-0 gl-bg-transparent gl-text-left': this.isSection,
'gl-w-full gl-focus--focus': this.isSection,
'nav-item-link': !this.isSection,
'gl-bg-t-gray-a-08': this.isActive,
'gl-py-2': this.isPinnable,
'gl-py-3': !this.isPinnable,
...this.linkClasses,
};
},
},
methods: {
click(event) {
if (this.isSection) {
event.preventDefault();
this.expanded = !this.expanded;
}
},
},
};
</script>
<template>
<li>
<component
:is="elem"
v-bind="linkProps"
class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-px-0 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none!"
:class="computedLinkClasses"
data-qa-selector="nav_item_link"
data-testid="nav-item-link"
@click="click"
>
<div
:class="[isActive ? 'gl-bg-blue-500' : 'gl-bg-transparent']"
class="gl-absolute gl-left-2 gl-top-2 gl-bottom-2 gl-transition-slow"
aria-hidden="true"
style="width: 3px; border-radius: 3px; margin-right: 1px"
></div>
<div class="gl-flex-shrink-0 gl-w-6 gl-mx-3">
<slot name="icon">
<gl-icon v-if="item.icon" :name="item.icon" class="gl-ml-2 item-icon" />
<gl-icon
v-else-if="draggable"
name="grip"
class="gl-text-gray-400 gl-ml-2 draggable-icon"
/>
</slot>
</div>
<div class="gl-pr-3 gl-text-gray-900 gl-truncate-end">
{{ item.title }}
<div v-if="item.subtitle" class="gl-font-sm gl-text-gray-500 gl-truncate-end">
{{ item.subtitle }}
</div>
</div>
<slot name="actions"></slot>
<span v-if="isSection || hasPill || isPinnable" class="gl-flex-grow-1 gl-text-right gl-mr-3">
<gl-badge v-if="hasPill" size="sm" variant="info">
{{ pillData }}
</gl-badge>
<gl-icon v-else-if="isSection" :name="collapseIcon" />
<gl-button
v-else-if="isPinnable && !isPinned"
size="small"
category="tertiary"
icon="thumbtack"
:aria-label="$options.i18n.pinItem"
@click.prevent="$emit('pin-add', item.id)"
/>
<gl-button
v-else-if="isPinnable && isPinned"
size="small"
category="tertiary"
:aria-label="$options.i18n.unpinItem"
icon="thumbtack-solid"
@click.prevent="$emit('pin-remove', item.id)"
/>
</span>
</component>
<gl-collapse
v-if="isSection"
:id="itemId"
v-model="expanded"
data-qa-selector="menu_section"
:data-qa-section="item.title"
:aria-label="item.title"
class="gl-list-style-none gl-p-0"
tag="ul"
>
<nav-item
v-for="subItem of item.items"
:key="`${item.title}-${subItem.title}`"
:item="subItem"
@pin-add="(itemId) => $emit('pin-add', itemId)"
@pin-remove="(itemId) => $emit('pin-remove', itemId)"
/>
</gl-collapse>
</li>
</template>