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:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-11-23 00:10:35 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-23 00:10:35 +0300
commitebaefcebccee0575e8dddde1fe17dabaae62459b (patch)
tree286bf98e899eb48a0b2a4bbd6f0c506f6409b5a2 /app/assets/javascripts/header_search
parent39cc8695fc20e17f4989fa99aa9fafc00f9e2953 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/header_search')
-rw-r--r--app/assets/javascripts/header_search/components/app.vue44
-rw-r--r--app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue26
-rw-r--r--app/assets/javascripts/header_search/components/header_search_default_items.vue19
-rw-r--r--app/assets/javascripts/header_search/components/header_search_scoped_items.vue19
-rw-r--r--app/assets/javascripts/header_search/constants.js4
-rw-r--r--app/assets/javascripts/header_search/store/getters.js23
-rw-r--r--app/assets/javascripts/header_search/store/mutations.js4
7 files changed, 121 insertions, 18 deletions
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
index c6590fd8eb3..a575b80facc 100644
--- a/app/assets/javascripts/header_search/components/app.vue
+++ b/app/assets/javascripts/header_search/components/app.vue
@@ -3,6 +3,8 @@ import { GlSearchBoxByType, GlOutsideDirective as Outside } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
+import { FIRST_DROPDOWN_INDEX, SEARCH_BOX_INDEX } from '../constants';
import HeaderSearchAutocompleteItems from './header_search_autocomplete_items.vue';
import HeaderSearchDefaultItems from './header_search_default_items.vue';
import HeaderSearchScopedItems from './header_search_scoped_items.vue';
@@ -18,15 +20,17 @@ export default {
HeaderSearchDefaultItems,
HeaderSearchScopedItems,
HeaderSearchAutocompleteItems,
+ DropdownKeyboardNavigation,
},
data() {
return {
showDropdown: false,
+ currentFocusIndex: SEARCH_BOX_INDEX,
};
},
computed: {
...mapState(['search']),
- ...mapGetters(['searchQuery']),
+ ...mapGetters(['searchQuery', 'searchOptions']),
searchText: {
get() {
return this.search;
@@ -35,12 +39,25 @@ export default {
this.setSearch(value);
},
},
+ currentFocusedOption() {
+ return this.searchOptions[this.currentFocusIndex];
+ },
+ isLoggedIn() {
+ return gon?.current_username;
+ },
showSearchDropdown() {
- return this.showDropdown && gon?.current_username;
+ return this.showDropdown && this.isLoggedIn;
},
showDefaultItems() {
return !this.searchText;
},
+ defaultIndex() {
+ if (this.showDefaultItems) {
+ return SEARCH_BOX_INDEX;
+ }
+
+ return FIRST_DROPDOWN_INDEX;
+ },
},
methods: {
...mapActions(['setSearch', 'fetchAutocompleteOptions']),
@@ -51,7 +68,7 @@ export default {
this.showDropdown = false;
},
submitSearch() {
- return visitUrl(this.searchQuery);
+ return visitUrl(this.currentFocusedOption?.url || this.searchQuery);
},
getAutocompleteOptions(searchTerm) {
if (!searchTerm) {
@@ -61,6 +78,7 @@ export default {
this.fetchAutocompleteOptions();
},
},
+ SEARCH_BOX_INDEX,
};
</script>
@@ -68,14 +86,14 @@ export default {
<section v-outside="closeDropdown" class="header-search gl-relative">
<gl-search-box-by-type
v-model="searchText"
+ class="gl-z-index-1"
:debounce="500"
autocomplete="off"
:placeholder="$options.i18n.searchPlaceholder"
@focus="openDropdown"
@click="openDropdown"
@input="getAutocompleteOptions"
- @keydown.enter="submitSearch"
- @keydown.esc="closeDropdown"
+ @keydown.enter.stop.prevent="submitSearch"
/>
<div
v-if="showSearchDropdown"
@@ -83,10 +101,20 @@ export default {
class="header-search-dropdown-menu gl-absolute gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0"
>
<div class="header-search-dropdown-content gl-overflow-y-auto gl-py-2">
- <header-search-default-items v-if="showDefaultItems" />
+ <dropdown-keyboard-navigation
+ v-model="currentFocusIndex"
+ :max="searchOptions.length - 1"
+ :min="$options.SEARCH_BOX_INDEX"
+ :default-index="defaultIndex"
+ @tab="closeDropdown"
+ />
+ <header-search-default-items
+ v-if="showDefaultItems"
+ :current-focused-option="currentFocusedOption"
+ />
<template v-else>
- <header-search-scoped-items />
- <header-search-autocomplete-items />
+ <header-search-scoped-items :current-focused-option="currentFocusedOption" />
+ <header-search-autocomplete-items :current-focused-option="currentFocusedOption" />
</template>
</div>
</div>
diff --git a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
index 9bea2b280f7..cf1f7c030e2 100644
--- a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
+++ b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
@@ -23,10 +23,26 @@ export default {
directives: {
SafeHtml,
},
+ props: {
+ currentFocusedOption: {
+ type: Object,
+ required: false,
+ default: () => null,
+ },
+ },
computed: {
...mapState(['search', 'loading']),
...mapGetters(['autocompleteGroupedSearchOptions']),
},
+ watch: {
+ currentFocusedOption() {
+ const focusedElement = this.$refs[this.currentFocusedOption?.html_id]?.[0]?.$el;
+
+ if (focusedElement) {
+ focusedElement.scrollIntoView(false);
+ }
+ },
+ },
methods: {
highlightedName(val) {
return highlight(val, this.search);
@@ -38,6 +54,9 @@ export default {
return SMALL_AVATAR_PX;
},
+ isOptionFocused(data) {
+ return this.currentFocusedOption?.html_id === data.html_id;
+ },
},
};
</script>
@@ -49,9 +68,10 @@ export default {
<gl-dropdown-divider />
<gl-dropdown-section-header>{{ option.category }}</gl-dropdown-section-header>
<gl-dropdown-item
- v-for="(data, index) in option.data"
- :id="`autocomplete-${option.category}-${index}`"
- :key="index"
+ v-for="data in option.data"
+ :ref="data.html_id"
+ :key="data.html_id"
+ :class="{ 'gl-bg-gray-50': isOptionFocused(data) }"
tabindex="-1"
:href="data.url"
>
diff --git a/app/assets/javascripts/header_search/components/header_search_default_items.vue b/app/assets/javascripts/header_search/components/header_search_default_items.vue
index 2871937ed3a..2228bc0c2a2 100644
--- a/app/assets/javascripts/header_search/components/header_search_default_items.vue
+++ b/app/assets/javascripts/header_search/components/header_search_default_items.vue
@@ -12,6 +12,13 @@ export default {
GlDropdownSectionHeader,
GlDropdownItem,
},
+ props: {
+ currentFocusedOption: {
+ type: Object,
+ required: false,
+ default: () => null,
+ },
+ },
computed: {
...mapState(['searchContext']),
...mapGetters(['defaultSearchOptions']),
@@ -23,6 +30,11 @@ export default {
);
},
},
+ methods: {
+ isOptionFocused(option) {
+ return this.currentFocusedOption?.html_id === option.html_id;
+ },
+ },
};
</script>
@@ -30,9 +42,10 @@ export default {
<div>
<gl-dropdown-section-header>{{ sectionHeader }}</gl-dropdown-section-header>
<gl-dropdown-item
- v-for="(option, index) in defaultSearchOptions"
- :id="`default-${index}`"
- :key="index"
+ v-for="option in defaultSearchOptions"
+ :ref="option.html_id"
+ :key="option.html_id"
+ :class="{ 'gl-bg-gray-50': isOptionFocused(option) }"
tabindex="-1"
:href="option.url"
>
diff --git a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue
index 645eba05148..d3def929752 100644
--- a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue
+++ b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue
@@ -7,19 +7,32 @@ export default {
components: {
GlDropdownItem,
},
+ props: {
+ currentFocusedOption: {
+ type: Object,
+ required: false,
+ default: () => null,
+ },
+ },
computed: {
...mapState(['search']),
...mapGetters(['scopedSearchOptions']),
},
+ methods: {
+ isOptionFocused(option) {
+ return this.currentFocusedOption?.html_id === option.html_id;
+ },
+ },
};
</script>
<template>
<div>
<gl-dropdown-item
- v-for="(option, index) in scopedSearchOptions"
- :id="`scoped-${index}`"
- :key="index"
+ v-for="option in scopedSearchOptions"
+ :ref="option.html_id"
+ :key="option.html_id"
+ :class="{ 'gl-bg-gray-50': isOptionFocused(option) }"
tabindex="-1"
:href="option.url"
>
diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js
index 2fadb1bd1ee..34777697863 100644
--- a/app/assets/javascripts/header_search/constants.js
+++ b/app/assets/javascripts/header_search/constants.js
@@ -23,3 +23,7 @@ export const PROJECTS_CATEGORY = 'Projects';
export const LARGE_AVATAR_PX = 32;
export const SMALL_AVATAR_PX = 16;
+
+export const FIRST_DROPDOWN_INDEX = 0;
+
+export const SEARCH_BOX_INDEX = -1;
diff --git a/app/assets/javascripts/header_search/store/getters.js b/app/assets/javascripts/header_search/store/getters.js
index 3f4e231ca55..85112a317cf 100644
--- a/app/assets/javascripts/header_search/store/getters.js
+++ b/app/assets/javascripts/header_search/store/getters.js
@@ -54,22 +54,27 @@ export const defaultSearchOptions = (state, getters) => {
return [
{
+ html_id: 'default-issues-assigned',
title: MSG_ISSUES_ASSIGNED_TO_ME,
url: `${getters.scopedIssuesPath}/?assignee_username=${userName}`,
},
{
+ html_id: 'default-issues-created',
title: MSG_ISSUES_IVE_CREATED,
url: `${getters.scopedIssuesPath}/?author_username=${userName}`,
},
{
+ html_id: 'default-mrs-assigned',
title: MSG_MR_ASSIGNED_TO_ME,
url: `${getters.scopedMRPath}/?assignee_username=${userName}`,
},
{
+ html_id: 'default-mrs-reviewer',
title: MSG_MR_IM_REVIEWER,
url: `${getters.scopedMRPath}/?reviewer_username=${userName}`,
},
{
+ html_id: 'default-mrs-created',
title: MSG_MR_IVE_CREATED,
url: `${getters.scopedMRPath}/?author_username=${userName}`,
},
@@ -122,6 +127,7 @@ export const scopedSearchOptions = (state, getters) => {
if (state.searchContext.project) {
options.push({
+ html_id: 'scoped-in-project',
scope: state.searchContext.project.name,
description: MSG_IN_PROJECT,
url: getters.projectUrl,
@@ -130,6 +136,7 @@ export const scopedSearchOptions = (state, getters) => {
if (state.searchContext.group) {
options.push({
+ html_id: 'scoped-in-group',
scope: state.searchContext.group.name,
description: MSG_IN_GROUP,
url: getters.groupUrl,
@@ -137,6 +144,7 @@ export const scopedSearchOptions = (state, getters) => {
}
options.push({
+ html_id: 'scoped-in-all',
description: MSG_IN_ALL_GITLAB,
url: getters.allUrl,
});
@@ -165,3 +173,18 @@ export const autocompleteGroupedSearchOptions = (state) => {
return results;
};
+
+export const searchOptions = (state, getters) => {
+ if (!state.search) {
+ return getters.defaultSearchOptions;
+ }
+
+ const sortedAutocompleteOptions = Object.values(getters.autocompleteGroupedSearchOptions).reduce(
+ (options, group) => {
+ return [...options, ...group.data];
+ },
+ [],
+ );
+
+ return getters.scopedSearchOptions.concat(sortedAutocompleteOptions);
+};
diff --git a/app/assets/javascripts/header_search/store/mutations.js b/app/assets/javascripts/header_search/store/mutations.js
index 175b5406540..7fe13600ac9 100644
--- a/app/assets/javascripts/header_search/store/mutations.js
+++ b/app/assets/javascripts/header_search/store/mutations.js
@@ -7,7 +7,9 @@ export default {
},
[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, data) {
state.loading = false;
- state.autocompleteOptions = data;
+ state.autocompleteOptions = data.map((d, i) => {
+ return { html_id: `autocomplete-${d.category}-${i}`, ...d };
+ });
},
[types.RECEIVE_AUTOCOMPLETE_ERROR](state) {
state.loading = false;