diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue')
-rw-r--r-- | app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue | 178 |
1 files changed, 178 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue new file mode 100644 index 00000000000..7ec420fa908 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue @@ -0,0 +1,178 @@ +<script> +import { mapState, mapGetters, mapActions } from 'vuex'; +import { GlLoadingIcon, GlButton, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui'; + +import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; + +export default { + components: { + GlLoadingIcon, + GlButton, + GlIcon, + GlSearchBoxByType, + GlLink, + }, + data() { + return { + searchKey: '', + currentHighlightItem: -1, + }; + }, + computed: { + ...mapState([ + 'labelsManagePath', + 'labels', + 'labelsFetchInProgress', + 'labelsListTitle', + 'footerCreateLabelTitle', + 'footerManageLabelTitle', + ]), + ...mapGetters(['selectedLabelsList']), + visibleLabels() { + if (this.searchKey) { + return this.labels.filter(label => + label.title.toLowerCase().includes(this.searchKey.toLowerCase()), + ); + } + return this.labels; + }, + }, + watch: { + searchKey(value) { + // When there is search string present + // and there are matching results, + // highlight first item by default. + if (value && this.visibleLabels.length) { + this.currentHighlightItem = 0; + } + }, + }, + mounted() { + this.fetchLabels(); + }, + methods: { + ...mapActions([ + 'toggleDropdownContents', + 'toggleDropdownContentsCreateView', + 'fetchLabels', + 'updateSelectedLabels', + ]), + getDropdownLabelBoxStyle(label) { + return { + backgroundColor: label.color, + }; + }, + isLabelSelected(label) { + return this.selectedLabelsList.includes(label.id); + }, + /** + * This method scrolls item from dropdown into + * the view if it is off the viewable area of the + * container. + */ + scrollIntoViewIfNeeded() { + const highlightedLabel = this.$refs.labelsListContainer.querySelector('.is-focused'); + + if (highlightedLabel) { + const rect = highlightedLabel.getBoundingClientRect(); + if (rect.bottom > this.$refs.labelsListContainer.clientHeight) { + highlightedLabel.scrollIntoView(false); + } + if (rect.top < 0) { + highlightedLabel.scrollIntoView(); + } + } + }, + /** + * This method enables keyboard navigation support for + * the dropdown. + */ + handleKeyDown(e) { + if (e.keyCode === UP_KEY_CODE && this.currentHighlightItem > 0) { + this.currentHighlightItem -= 1; + } else if ( + e.keyCode === DOWN_KEY_CODE && + this.currentHighlightItem < this.visibleLabels.length - 1 + ) { + this.currentHighlightItem += 1; + } else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) { + this.updateSelectedLabels([this.visibleLabels[this.currentHighlightItem]]); + } else if (e.keyCode === ESC_KEY_CODE) { + this.toggleDropdownContents(); + } + + if (e.keyCode !== ESC_KEY_CODE) { + // Scroll the list only after highlighting + // styles are rendered completely. + this.$nextTick(() => { + this.scrollIntoViewIfNeeded(); + }); + } + }, + handleLabelClick(label) { + this.updateSelectedLabels([label]); + }, + }, +}; +</script> + +<template> + <div class="labels-select-contents-list" @keydown="handleKeyDown"> + <gl-loading-icon + v-if="labelsFetchInProgress" + class="labels-fetch-loading position-absolute d-flex align-items-center w-100 h-100" + size="md" + /> + <div class="dropdown-title d-flex align-items-center pt-0 pb-2"> + <span class="flex-grow-1">{{ labelsListTitle }}</span> + <gl-button + :aria-label="__('Close')" + variant="link" + size="sm" + class="dropdown-header-button p-0" + @click="toggleDropdownContents" + > + <gl-icon name="close" /> + </gl-button> + </div> + <div class="dropdown-input"> + <gl-search-box-by-type v-model="searchKey" :autofocus="true" /> + </div> + <div v-if="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content"> + <ul class="list-unstyled mb-0"> + <li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left"> + <gl-link + class="d-flex align-items-baseline text-break-word label-item" + :class="{ 'is-focused': index === currentHighlightItem }" + @click="handleLabelClick(label)" + > + <gl-icon v-show="label.set" name="mobile-issue-close" class="mr-2 align-self-center" /> + <span v-show="!label.set" class="mr-3 pr-2"></span> + <span class="dropdown-label-box" :style="getDropdownLabelBoxStyle(label)"></span> + <span>{{ label.title }}</span> + </gl-link> + </li> + <li v-if="!visibleLabels.length" class="p-2 text-center"> + {{ __('No matching results') }} + </li> + </ul> + </div> + <div class="dropdown-footer"> + <ul class="list-unstyled"> + <li> + <gl-button + variant="link" + class="d-flex w-100 flex-row text-break-word label-item" + @click="toggleDropdownContentsCreateView" + >{{ footerCreateLabelTitle }}</gl-button + > + </li> + <li> + <gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item"> + {{ footerManageLabelTitle }} + </gl-link> + </li> + </ul> + </div> + </div> +</template> |