diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-03 03:11:20 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-03 03:11:20 +0300 |
commit | 498ba9dc41fcf2b4be30a8f3721543953efb3c3b (patch) | |
tree | ed33fbf37c0c2ae3a71042455f9b51800907a984 /app/assets/javascripts/header_search | |
parent | 515f39456fce82eb2ab811fa366167ad084a3b12 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/header_search')
5 files changed, 111 insertions, 20 deletions
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue index a575b80facc..67e3998bc97 100644 --- a/app/assets/javascripts/header_search/components/app.vue +++ b/app/assets/javascripts/header_search/components/app.vue @@ -2,9 +2,14 @@ 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 { s__, sprintf } from '~/locale'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; -import { FIRST_DROPDOWN_INDEX, SEARCH_BOX_INDEX } from '../constants'; +import { + FIRST_DROPDOWN_INDEX, + SEARCH_BOX_INDEX, + SEARCH_INPUT_DESCRIPTION, + SEARCH_RESULTS_DESCRIPTION, +} 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'; @@ -12,7 +17,21 @@ import HeaderSearchScopedItems from './header_search_scoped_items.vue'; export default { name: 'HeaderSearchApp', i18n: { - searchPlaceholder: __('Search or jump to...'), + searchPlaceholder: s__('GlobalSearch|Search or jump to...'), + searchAria: s__('GlobalSearch|Search GitLab'), + searchInputDescribeByNoDropdown: s__( + 'GlobalSearch|Type and press the enter key to submit search.', + ), + searchInputDescribeByWithDropdown: s__( + 'GlobalSearch|Type for new suggestions to appear below.', + ), + searchDescribedByDefault: s__( + 'GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list.', + ), + searchDescribedByUpdated: s__( + 'GlobalSearch|Results updated. %{count} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.', + ), + searchResultsLoading: s__('GlobalSearch|Search results are loading'), }, directives: { Outside }, components: { @@ -29,7 +48,7 @@ export default { }; }, computed: { - ...mapState(['search']), + ...mapState(['search', 'loading']), ...mapGetters(['searchQuery', 'searchOptions']), searchText: { get() { @@ -42,6 +61,9 @@ export default { currentFocusedOption() { return this.searchOptions[this.currentFocusIndex]; }, + currentFocusedId() { + return this.currentFocusedOption?.html_id; + }, isLoggedIn() { return gon?.current_username; }, @@ -58,6 +80,30 @@ export default { return FIRST_DROPDOWN_INDEX; }, + searchInputDescribeBy() { + if (this.isLoggedIn) { + return this.$options.i18n.searchInputDescribeByWithDropdown; + } + + return this.$options.i18n.searchInputDescribeByNoDropdown; + }, + dropdownResultsDescription() { + if (!this.showSearchDropdown) { + return ''; // This allows aria-live to see register an update when the dropdown is shown + } + + if (this.showDefaultItems) { + return sprintf(this.$options.i18n.searchDescribedByDefault, { + count: this.searchOptions.length, + }); + } + + return this.loading + ? this.$options.i18n.searchResultsLoading + : sprintf(this.$options.i18n.searchDescribedByUpdated, { + count: this.searchOptions.length, + }); + }, }, methods: { ...mapActions(['setSearch', 'fetchAutocompleteOptions']), @@ -79,22 +125,44 @@ export default { }, }, SEARCH_BOX_INDEX, + SEARCH_INPUT_DESCRIPTION, + SEARCH_RESULTS_DESCRIPTION, }; </script> <template> - <section v-outside="closeDropdown" class="header-search gl-relative"> + <form + v-outside="closeDropdown" + role="search" + :aria-label="$options.i18n.searchAria" + class="header-search gl-relative" + > <gl-search-box-by-type v-model="searchText" + role="searchbox" class="gl-z-index-1" :debounce="500" autocomplete="off" :placeholder="$options.i18n.searchPlaceholder" + :aria-activedescendant="currentFocusedId" + :aria-describedby="$options.SEARCH_INPUT_DESCRIPTION" @focus="openDropdown" @click="openDropdown" @input="getAutocompleteOptions" @keydown.enter.stop.prevent="submitSearch" /> + <span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only">{{ + searchInputDescribeBy + }}</span> + <span + role="region" + :data-testid="$options.SEARCH_RESULTS_DESCRIPTION" + class="gl-sr-only" + aria-live="polite" + aria-atomic="true" + > + {{ dropdownResultsDescription }} + </span> <div v-if="showSearchDropdown" data-testid="header-search-dropdown-menu" @@ -118,5 +186,5 @@ export default { </template> </div> </div> - </section> + </form> </template> 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 cf1f7c030e2..9f4f4768247 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 @@ -69,13 +69,16 @@ export default { <gl-dropdown-section-header>{{ option.category }}</gl-dropdown-section-header> <gl-dropdown-item v-for="data in option.data" + :id="data.html_id" :ref="data.html_id" :key="data.html_id" :class="{ 'gl-bg-gray-50': isOptionFocused(data) }" + :aria-selected="isOptionFocused(data)" + :aria-label="data.label" tabindex="-1" :href="data.url" > - <div class="gl-display-flex gl-align-items-center"> + <div class="gl-display-flex gl-align-items-center" aria-hidden="true"> <gl-avatar v-if="data.avatar_url !== undefined" :src="data.avatar_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 2228bc0c2a2..53e63bc6cca 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 @@ -43,13 +43,16 @@ export default { <gl-dropdown-section-header>{{ sectionHeader }}</gl-dropdown-section-header> <gl-dropdown-item v-for="option in defaultSearchOptions" + :id="option.html_id" :ref="option.html_id" :key="option.html_id" :class="{ 'gl-bg-gray-50': isOptionFocused(option) }" + :aria-selected="isOptionFocused(option)" + :aria-label="option.title" tabindex="-1" :href="option.url" > - {{ option.title }} + <span aria-hidden="true">{{ option.title }}</span> </gl-dropdown-item> </div> </template> 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 d3def929752..3aebee71509 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 @@ -1,6 +1,7 @@ <script> import { GlDropdownItem } from '@gitlab/ui'; import { mapState, mapGetters } from 'vuex'; +import { __, sprintf } from '~/locale'; export default { name: 'HeaderSearchScopedItems', @@ -22,6 +23,13 @@ export default { isOptionFocused(option) { return this.currentFocusedOption?.html_id === option.html_id; }, + ariaLabel(option) { + return sprintf(__('%{search} %{description} %{scope}'), { + search: this.search, + description: option.description, + scope: option.scope || '', + }); + }, }, }; </script> @@ -30,15 +38,20 @@ export default { <div> <gl-dropdown-item v-for="option in scopedSearchOptions" + :id="option.html_id" :ref="option.html_id" :key="option.html_id" :class="{ 'gl-bg-gray-50': isOptionFocused(option) }" + :aria-selected="isOptionFocused(option)" + :aria-label="ariaLabel(option)" tabindex="-1" :href="option.url" > - "<span class="gl-font-weight-bold">{{ search }}</span - >" {{ option.description }} - <span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span> + <span aria-hidden="true"> + "<span class="gl-font-weight-bold">{{ search }}</span + >" {{ option.description }} + <span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span> + </span> </gl-dropdown-item> </div> </template> diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js index 34777697863..b2e45fcd648 100644 --- a/app/assets/javascripts/header_search/constants.js +++ b/app/assets/javascripts/header_search/constants.js @@ -1,20 +1,20 @@ -import { __ } from '~/locale'; +import { s__ } from '~/locale'; -export const MSG_ISSUES_ASSIGNED_TO_ME = __('Issues assigned to me'); +export const MSG_ISSUES_ASSIGNED_TO_ME = s__('GlobalSearch|Issues assigned to me'); -export const MSG_ISSUES_IVE_CREATED = __("Issues I've created"); +export const MSG_ISSUES_IVE_CREATED = s__("GlobalSearch|Issues I've created"); -export const MSG_MR_ASSIGNED_TO_ME = __('Merge requests assigned to me'); +export const MSG_MR_ASSIGNED_TO_ME = s__('GlobalSearch|Merge requests assigned to me'); -export const MSG_MR_IM_REVIEWER = __("Merge requests that I'm a reviewer"); +export const MSG_MR_IM_REVIEWER = s__("GlobalSearch|Merge requests that I'm a reviewer"); -export const MSG_MR_IVE_CREATED = __("Merge requests I've created"); +export const MSG_MR_IVE_CREATED = s__("GlobalSearch|Merge requests I've created"); -export const MSG_IN_ALL_GITLAB = __('in all GitLab'); +export const MSG_IN_ALL_GITLAB = s__('GlobalSearch|in all GitLab'); -export const MSG_IN_GROUP = __('in group'); +export const MSG_IN_GROUP = s__('GlobalSearch|in group'); -export const MSG_IN_PROJECT = __('in project'); +export const MSG_IN_PROJECT = s__('GlobalSearch|in project'); export const GROUPS_CATEGORY = 'Groups'; @@ -27,3 +27,7 @@ export const SMALL_AVATAR_PX = 16; export const FIRST_DROPDOWN_INDEX = 0; export const SEARCH_BOX_INDEX = -1; + +export const SEARCH_INPUT_DESCRIPTION = 'search-input-description'; + +export const SEARCH_RESULTS_DESCRIPTION = 'search-results-description'; |