diff options
author | Suzanne Selhorn <sselhorn@gitlab.com> | 2023-05-05 22:37:23 +0300 |
---|---|---|
committer | Suzanne Selhorn <sselhorn@gitlab.com> | 2023-05-05 22:37:23 +0300 |
commit | c9f3fda122fb5ee39fe3fff546d52cae008f24da (patch) | |
tree | f7d6bbd83b516060cb02d3679a2563697369d6ff | |
parent | 0a602766667fc93591571f37f63b9bc03d568fd3 (diff) | |
parent | 3e376ccdfca9bd4d36388cdcdf865289365369ae (diff) |
Merge branch 'gps-keyboard-nav' into 'main'
Add keyboard shortcuts for search
Closes #1562
See merge request https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/3810
Merged-by: Suzanne Selhorn <sselhorn@gitlab.com>
Approved-by: David O'Regan <doregan@gitlab.com>
Co-authored-by: Sarah German <sgerman@gitlab.com>
-rw-r--r-- | content/assets/stylesheets/_variables.scss | 3 | ||||
-rw-r--r-- | content/assets/stylesheets/stylesheet.scss | 13 | ||||
-rw-r--r-- | content/frontend/search/components/google_search_form.vue | 89 | ||||
-rw-r--r-- | content/frontend/search/google.js | 8 |
4 files changed, 93 insertions, 20 deletions
diff --git a/content/assets/stylesheets/_variables.scss b/content/assets/stylesheets/_variables.scss index 23c37e82..b16a44cf 100644 --- a/content/assets/stylesheets/_variables.scss +++ b/content/assets/stylesheets/_variables.scss @@ -154,9 +154,6 @@ $help-gray-100: #dbdbdb; $help-gray-200: #bfbfbf; $help-gray-900: #303030; -// Other colors -$search-border: rgba(0, 0, 0, 0.25); - // Social colors $youtube: #f00; diff --git a/content/assets/stylesheets/stylesheet.scss b/content/assets/stylesheets/stylesheet.scss index 59fd666f..b6b30bad 100644 --- a/content/assets/stylesheets/stylesheet.scss +++ b/content/assets/stylesheets/stylesheet.scss @@ -329,10 +329,21 @@ ol { .gl-search-box-by-type-input-borderless:not(.form-control-plaintext):focus { box-shadow: inset 0 0 0 2px $blue-400; } -.gs-results a:hover { +.gs-wrapper kbd { + display: none; + @media (min-width: $bp-md) { + display: inline; + top: 0.25rem; + right: 0.3rem; + font-size: 0.75rem; + } +} +.gs-results a:hover, +.gs-results a:focus { text-decoration: none; background-color: $gray-50; color: $gray-700; + outline: none; } .gl-search-box-by-click { width: $search-lg-width; diff --git a/content/frontend/search/components/google_search_form.vue b/content/frontend/search/components/google_search_form.vue index 6f9f6f82..1f941685 100644 --- a/content/frontend/search/components/google_search_form.vue +++ b/content/frontend/search/components/google_search_form.vue @@ -1,5 +1,10 @@ <script> -import { GlSearchBoxByType, GlLink, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import { + GlSearchBoxByType, + GlLink, + GlSafeHtmlDirective as SafeHtml, + GlTooltipDirective as GlTooltip, +} from '@gitlab/ui'; import { debounce } from 'lodash'; import { directive as clickOutside } from 'v-click-outside'; import { fetchResults, MAX_RESULTS_PER_PAGE } from '../../services/google_search_api'; @@ -12,6 +17,7 @@ export default { directives: { clickOutside, SafeHtml, + GlTooltip, }, props: { borderless: { @@ -28,6 +34,8 @@ export default { showResultPanel: false, submitted: false, totalCount: 0, + activeLink: -1, + showTooltip: true, }; }, computed: { @@ -40,6 +48,9 @@ export default { }, watch: { searchQuery() { + this.showTooltip = this.searchQuery.length === 0; + this.submitted = false; + this.moreResultsPath = `/search/?q=${encodeURI(this.searchQuery)}`; this.debouncedGetResults(); }, }, @@ -49,7 +60,6 @@ export default { methods: { async getResults() { this.showResultPanel = false; - this.isLoading = true; const response = await fetchResults(this.searchQuery, [], 1, 10); this.isLoading = false; @@ -59,13 +69,43 @@ export default { ? response.searchInformation.totalCount : 0; this.results = response.items ? response.items : []; - this.moreResultsPath = `/search/?q=${encodeURI(this.searchQuery)}`; this.submitted = true; this.showResultPanel = true; }, showAllResults() { // Sends the user to the advanced search page if they hit Enter. - window.location.href = this.moreResultsPath; + if (this.searchQuery) { + window.location.href = this.moreResultsPath; + } + }, + keyboardNav(e) { + const isArrowUp = e.key === 'ArrowUp'; + const isArrowDown = e.key === 'ArrowDown'; + const searchBox = document.querySelector('input[type=search]'); + + if (isArrowUp || isArrowDown) { + const activeIndex = this.activeLink + (isArrowUp ? -1 : 1); + + // If we're at the top or bottom of the list, go back to the search box. + if (activeIndex < 0 || activeIndex > this.results.length) { + this.activeLink = -1; + searchBox.focus(); + // Reset the value after focus so that the cursor is at the end of the text. + searchBox.value = this.searchQuery; + return; + } + // Otherwise, select the previous or next link. + this.setActiveResult(document.querySelector(`[data-link-index="${activeIndex}"]`)); + } + }, + setActiveResult(result) { + result.focus(); + this.activeLink = Number(result.dataset.linkIndex); + }, + deactivate() { + this.showResultPanel = false; + this.activeLink = -1; + this.showTooltip = this.searchQuery.length === 0; }, }, }; @@ -73,31 +113,48 @@ export default { <template> <div - v-click-outside="() => (showResultPanel = false)" + v-click-outside="() => deactivate()" class="gs-wrapper gl-m-auto gl-my-3 gl-md-mt-0 gl-md-mb-0" + @keydown.arrow-down.prevent="keyboardNav" + @keydown.arrow-up.prevent="keyboardNav" + @keydown.escape="deactivate()" > - <gl-search-box-by-type - v-model="searchQuery" - :is-loading="isLoading" - :borderless="borderless" - placeholder="" - @focus="showResultPanel = true" - @keyup.enter="showAllResults()" - /> + <form class="gl-relative"> + <gl-search-box-by-type + v-model="searchQuery" + :is-loading="isLoading" + :borderless="borderless" + placeholder="" + autocomplete="off" + aria-label="Search" + @focus="showResultPanel = true" + @keydown.enter.prevent="showAllResults()" + /> + <kbd + v-show="showTooltip && !isLoading" + v-gl-tooltip.bottom.hover.html + class="gl-absolute gl-z-index-1 gl-bg-gray-100 gl-text-gray-700" + title="Use the shortcut key <kbd>/</kbd> to start a search" + >/</kbd + > + </form> + <div - v-if="showResultPanel && submitted" + v-show="showResultPanel && submitted" class="gs-results gl-absolute gl-z-index-200 gl-bg-white gl-rounded gl-px-3 gl-shadow" > - <ul v-if="results.length" data-testid="search-results" class="gl-pl-0 gl-mb-0"> - <li v-for="result in results" :key="result.cacheId" class="gl-list-style-none"> + <ul v-show="results.length" data-testid="search-results" class="gl-pl-0 gl-mb-3 gl-pt-3"> + <li v-for="(result, index) in results" :key="result.cacheId" class="gl-list-style-none"> <gl-link v-safe-html="result.formattedTitle" :href="result.relativeLink" + :data-link-index="index" class="gl-text-gray-700 gl-py-3 gl-px-2 gl-display-block gl-text-left" /> </li> <li v-if="hasMoreResults" class="gl-list-style-none gl-border-t gl-my-2 gl-py-2"> <gl-link + :data-link-index="results.length" data-testid="more-results" :href="moreResultsPath" class="gl-text-gray-700 gl-py-3 gl-pb-2 gl-px-2 gl-display-block gl-text-left" diff --git a/content/frontend/search/google.js b/content/frontend/search/google.js index 88e450dd..bfb30a77 100644 --- a/content/frontend/search/google.js +++ b/content/frontend/search/google.js @@ -26,3 +26,11 @@ document.addEventListener('DOMContentLoaded', () => { // Lunr.js mountVue('.js-search-form', SearchForm); }); + +// Keyboard shortcut: forward slash focuses on search forms +document.addEventListener('keyup', (e) => { + if (e.key !== '/' || e.ctrlKey || e.metaKey) return; + if (/^(?:input|textarea|select|button)$/i.test(e.target.tagName)) return; + e.preventDefault(); + document.querySelector('input[type="search"]').focus(); +}); |