diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-23 00:10:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-23 00:10:35 +0300 |
commit | ebaefcebccee0575e8dddde1fe17dabaae62459b (patch) | |
tree | 286bf98e899eb48a0b2a4bbd6f0c506f6409b5a2 /app/assets/javascripts/header_search | |
parent | 39cc8695fc20e17f4989fa99aa9fafc00f9e2953 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/header_search')
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; |