aboutsummaryrefslogtreecommitdiffstats
path: root/core/src/views/UnifiedSearch.vue
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/views/UnifiedSearch.vue')
-rw-r--r--core/src/views/UnifiedSearch.vue922
1 files changed, 119 insertions, 803 deletions
diff --git a/core/src/views/UnifiedSearch.vue b/core/src/views/UnifiedSearch.vue
index 62b5d034038..103e47b0425 100644
--- a/core/src/views/UnifiedSearch.vue
+++ b/core/src/views/UnifiedSearch.vue
@@ -1,866 +1,182 @@
- <!--
- - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+<!--
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <NcHeaderMenu id="unified-search"
- class="unified-search"
- exclude-click-outside-classes="popover"
- :open.sync="open"
- :aria-label="ariaLabel"
- @open="onOpen"
- @close="onClose">
- <!-- Header icon -->
- <template #trigger>
- <Magnify class="unified-search__trigger"
- :size="22/* fit better next to other 20px icons */"
- fill-color="var(--color-primary-text)" />
- </template>
-
- <!-- Search form & filters wrapper -->
- <div class="unified-search__input-wrapper">
- <label for="unified-search__input">{{ ariaLabel }}</label>
- <div class="unified-search__input-row">
- <form class="unified-search__form"
- role="search"
- :class="{'icon-loading-small': isLoading}"
- @submit.prevent.stop="onInputEnter"
- @reset.prevent.stop="onReset">
- <!-- Search input -->
- <input ref="input"
- id="unified-search__input"
- v-model="query"
- class="unified-search__form-input"
- type="search"
- :class="{'unified-search__form-input--with-reset': !!query}"
- :placeholder="t('core', 'Search {types} …', { types: typesNames.join(', ') })"
- aria-describedby="unified-search-desc"
- @input="onInputDebounced"
- @keypress.enter.prevent.stop="onInputEnter">
- <p id="unified-search-desc" class="hidden-visually">
- {{ t('core', 'Search starts once you start typing and results may be reached with the arrow keys') }}
- </p>
-
- <!-- Reset search button -->
- <input v-if="!!query && !isLoading"
- type="reset"
- class="unified-search__form-reset icon-close"
- :aria-label="t('core','Reset search')"
- value="">
-
- <input v-if="!!query && !isLoading && !enableLiveSearch"
- type="submit"
- class="unified-search__form-submit icon-confirm"
- :aria-label="t('core','Start search')"
- value="">
- </form>
-
- <!-- Search filters -->
- <NcActions v-if="availableFilters.length > 1"
- class="unified-search__filters"
- placement="bottom"
- container=".unified-search__input-wrapper">
- <!-- FIXME use element ref for container after https://github.com/nextcloud/nextcloud-vue/pull/3462 -->
- <NcActionButton v-for="type in availableFilters"
- :key="type"
- icon="icon-filter"
- :title="t('core', 'Search for {name} only', { name: typesMap[type] })"
- @click.stop="onClickFilter(`in:${type}`)">
- {{ `in:${type}` }}
- </NcActionButton>
- </NcActions>
- </div>
- </div>
-
- <template v-if="!hasResults">
- <!-- Loading placeholders -->
- <SearchResultPlaceholders v-if="isLoading" />
-
- <NcEmptyContent v-else-if="isValidQuery">
- <NcHighlight v-if="triggered" :text="t('core', 'No results for {query}', { query })" :search="query" />
- <div v-else>
- {{ t('core', 'Press enter to start searching') }}
- </div>
- <template #icon>
- <Magnify />
- </template>
- </NcEmptyContent>
-
- <NcEmptyContent v-else-if="!isLoading || isShortQuery">
- {{ t('core', 'Start typing to search') }}
- <template #icon>
- <Magnify />
- </template>
- <template v-if="isShortQuery" #desc>
- {{ n('core',
- 'Please enter {minSearchLength} character or more to search',
- 'Please enter {minSearchLength} characters or more to search',
- minSearchLength,
- {minSearchLength}) }}
- </template>
- </NcEmptyContent>
- </template>
-
- <!-- Grouped search results -->
- <template v-else>
- <ul v-for="({list, type}, typesIndex) in orderedResults"
- :key="type"
- class="unified-search__results"
- :class="`unified-search__results-${type}`"
- :aria-label="typesMap[type]">
- <h2 class="unified-search__results-header">
- {{ typesMap[type] }}
- </h2>
-
- <!-- Search results -->
- <li v-for="(result, index) in limitIfAny(list, type)" :key="result.resourceUrl">
- <SearchResult v-bind="result"
- :query="query"
- :focused="focused === 0 && typesIndex === 0 && index === 0"
- @focus="setFocusedIndex" />
- </li>
-
- <!-- Load more button -->
- <li>
- <SearchResult v-if="!reached[type]"
- class="unified-search__result-more"
- :title="loading[type]
- ? t('core', 'Loading more results …')
- : t('core', 'Load more results')"
- :icon-class="loading[type] ? 'icon-loading-small' : ''"
- @click.stop="loadMore(type)"
- @focus="setFocusedIndex" />
- </li>
- </ul>
- </template>
- </NcHeaderMenu>
+ <div class="unified-search-menu">
+ <NcHeaderButton v-show="!showLocalSearch"
+ :aria-label="t('core', 'Unified search')"
+ @click="toggleUnifiedSearch">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiMagnify" />
+ </template>
+ </NcHeaderButton>
+ <UnifiedSearchLocalSearchBar v-if="supportsLocalSearch"
+ :open.sync="showLocalSearch"
+ :query.sync="queryText"
+ @global-search="openModal" />
+ <UnifiedSearchModal :local-search="supportsLocalSearch"
+ :query.sync="queryText"
+ :open.sync="showUnifiedSearch" />
+ </div>
</template>
-<script>
+<script lang="ts">
+import { mdiMagnify } from '@mdi/js'
+import { emit, subscribe } from '@nextcloud/event-bus'
+import { t } from '@nextcloud/l10n'
+import { useBrowserLocation } from '@vueuse/core'
import debounce from 'debounce'
-import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
-import { showError } from '@nextcloud/dialogs'
-
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js'
-import NcHighlight from '@nextcloud/vue/dist/Components/NcHighlight.js'
-
-import Magnify from 'vue-material-design-icons/Magnify.vue'
-
-import SearchResult from '../components/UnifiedSearch/SearchResult.vue'
-import SearchResultPlaceholders from '../components/UnifiedSearch/SearchResultPlaceholders.vue'
-
-import { minSearchLength, getTypes, search, defaultLimit, regexFilterIn, regexFilterNot, enableLiveSearch } from '../services/UnifiedSearchService.js'
-
-const REQUEST_FAILED = 0
-const REQUEST_OK = 1
-const REQUEST_CANCELED = 2
-
-export default {
+import { defineComponent } from 'vue'
+import NcHeaderButton from '@nextcloud/vue/components/NcHeaderButton'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import UnifiedSearchModal from '../components/UnifiedSearch/UnifiedSearchModal.vue'
+import UnifiedSearchLocalSearchBar from '../components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue'
+import logger from '../logger.js'
+
+export default defineComponent({
name: 'UnifiedSearch',
components: {
- Magnify,
- NcActionButton,
- NcActions,
- NcEmptyContent,
- NcHeaderMenu,
- NcHighlight,
- SearchResult,
- SearchResultPlaceholders,
+ NcHeaderButton,
+ NcIconSvgWrapper,
+ UnifiedSearchModal,
+ UnifiedSearchLocalSearchBar,
},
- data() {
- return {
- types: [],
-
- // Cursors per types
- cursors: {},
- // Various search limits per types
- limits: {},
- // Loading types
- loading: {},
- // Reached search types
- reached: {},
- // Pending cancellable requests
- requests: [],
- // List of all results
- results: {},
+ setup() {
+ const currentLocation = useBrowserLocation()
- query: '',
- focused: null,
- triggered: false,
+ return {
+ currentLocation,
- defaultLimit,
- minSearchLength,
- enableLiveSearch,
+ mdiMagnify,
+ t,
+ }
+ },
- open: false,
+ data() {
+ return {
+ /** The current search query */
+ queryText: '',
+ /** Open state of the modal */
+ showUnifiedSearch: false,
+ /** Open state of the local search bar */
+ showLocalSearch: false,
}
},
computed: {
- typesIDs() {
- return this.types.map(type => type.id)
- },
- typesNames() {
- return this.types.map(type => type.name)
- },
- typesMap() {
- return this.types.reduce((prev, curr) => {
- prev[curr.id] = curr.name
- return prev
- }, {})
- },
-
- ariaLabel() {
- return t('core', 'Search')
- },
-
/**
- * Is there any result to display
- *
- * @return {boolean}
+ * Debounce emitting the search query by 250ms
*/
- hasResults() {
- return Object.keys(this.results).length !== 0
+ debouncedQueryUpdate() {
+ return debounce(this.emitUpdatedQuery, 250)
},
/**
- * Return ordered results
- *
- * @return {Array}
+ * Current page (app) supports local in-app search
*/
- orderedResults() {
- return this.typesIDs
- .filter(type => type in this.results)
- .map(type => ({
- type,
- list: this.results[type],
- }))
- },
-
- /**
- * Available filters
- * We only show filters that are available on the results
- *
- * @return {string[]}
- */
- availableFilters() {
- return Object.keys(this.results)
- },
-
- /**
- * Applied filters
- *
- * @return {string[]}
- */
- usedFiltersIn() {
- let match
- const filters = []
- while ((match = regexFilterIn.exec(this.query)) !== null) {
- filters.push(match[2])
- }
- return filters
- },
-
- /**
- * Applied anti filters
- *
- * @return {string[]}
- */
- usedFiltersNot() {
- let match
- const filters = []
- while ((match = regexFilterNot.exec(this.query)) !== null) {
- filters.push(match[2])
- }
- return filters
- },
-
- /**
- * Is the current search too short
- *
- * @return {boolean}
- */
- isShortQuery() {
- return this.query && this.query.trim().length < minSearchLength
- },
-
- /**
- * Is the current search valid
- *
- * @return {boolean}
- */
- isValidQuery() {
- return this.query && this.query.trim() !== '' && !this.isShortQuery
- },
-
- /**
- * Have we reached the end of all types searches
- *
- * @return {boolean}
- */
- isDoneSearching() {
- return Object.values(this.reached).every(state => state === false)
+ supportsLocalSearch() {
+ // TODO: Make this an API
+ const providerPaths = ['/settings/users', '/apps/deck', '/settings/apps']
+ return providerPaths.some((path) => this.currentLocation.pathname?.includes?.(path))
},
+ },
+ watch: {
/**
- * Is there any search in progress
- *
- * @return {boolean}
+ * Emit the updated query as eventbus events
+ * (This is debounced)
*/
- isLoading() {
- return Object.values(this.loading).some(state => state === true)
+ queryText() {
+ this.debouncedQueryUpdate()
},
},
- async created() {
- subscribe('files:navigation:changed', this.resetForm)
- this.types = await getTypes()
- this.logger.debug('Unified Search initialized with the following providers', this.types)
- },
-
- beforeDestroy() {
- unsubscribe('files:navigation:changed', this.resetForm)
- },
-
mounted() {
- if (OCP.Accessibility.disableKeyboardShortcuts()) {
- return
+ // register keyboard listener for search shortcut
+ if (window.OCP.Accessibility.disableKeyboardShortcuts() === false) {
+ window.addEventListener('keydown', this.onKeyDown)
}
- document.addEventListener('keydown', (event) => {
- // if not already opened, allows us to trigger default browser on second keydown
- if (event.ctrlKey && event.key === 'f' && !this.open) {
- event.preventDefault()
- this.open = true
- }
-
- // https://www.w3.org/WAI/GL/wiki/Using_ARIA_menus
- if (this.open) {
- // If arrow down, focus next result
- if (event.key === 'ArrowDown') {
- this.focusNext(event)
- }
-
- // If arrow up, focus prev result
- if (event.key === 'ArrowUp') {
- this.focusPrev(event)
- }
- }
+ // Allow external reset of the search / close local search
+ subscribe('nextcloud:unified-search:reset', () => {
+ this.showLocalSearch = false
+ this.queryText = ''
})
- },
-
- methods: {
- async onOpen() {
- // Update types list in the background
- this.types = await getTypes()
- },
- onClose() {
- emit('nextcloud:unified-search.close')
- },
-
- resetForm() {
- this.$el.querySelector('form[role="search"]').reset()
- },
-
- /**
- * Reset the search state
- */
- onReset() {
- emit('nextcloud:unified-search.reset')
- this.logger.debug('Search reset')
- this.query = ''
- this.resetState()
- this.focusInput()
- },
- async resetState() {
- this.cursors = {}
- this.limits = {}
- this.reached = {}
- this.results = {}
- this.focused = null
- this.triggered = false
- await this.cancelPendingRequests()
- },
-
- /**
- * Cancel any ongoing searches
- */
- async cancelPendingRequests() {
- // Cloning so we can keep processing other requests
- const requests = this.requests.slice(0)
- this.requests = []
-
- // Cancel all pending requests
- await Promise.all(requests.map(cancel => cancel()))
- },
-
- /**
- * Focus the search input on next tick
- */
- focusInput() {
- this.$nextTick(() => {
- this.$refs.input.focus()
- this.$refs.input.select()
- })
- },
-
- /**
- * If we have results already, open first one
- * If not, trigger the search again
- */
- onInputEnter() {
- if (this.hasResults) {
- const results = this.getResultsList()
- results[0].click()
- return
- }
- this.onInput()
- },
-
- /**
- * Start searching on input
- */
- async onInput() {
- // emit the search query
- emit('nextcloud:unified-search.search', { query: this.query })
-
- // Do not search if not long enough
- if (this.query.trim() === '' || this.isShortQuery) {
- for (const type of this.typesIDs) {
- this.$delete(this.results, type)
- }
- return
- }
-
- let types = this.typesIDs
- let query = this.query
-
- // Filter out types
- if (this.usedFiltersNot.length > 0) {
- types = this.typesIDs.filter(type => this.usedFiltersNot.indexOf(type) === -1)
- }
-
- // Only use those filters if any and check if they are valid
- if (this.usedFiltersIn.length > 0) {
- types = this.typesIDs.filter(type => this.usedFiltersIn.indexOf(type) > -1)
- }
-
- // Remove any filters from the query
- query = query.replace(regexFilterIn, '').replace(regexFilterNot, '')
-
- // Reset search if the query changed
- await this.resetState()
- this.triggered = true
-
- if (!types.length) {
- // no results since no types were selected
- this.logger.error('No types to search in')
- return
- }
-
- this.$set(this.loading, 'all', true)
- this.logger.debug(`Searching ${query} in`, types)
-
- Promise.all(types.map(async type => {
- try {
- // Init cancellable request
- const { request, cancel } = search({ type, query })
- this.requests.push(cancel)
-
- // Fetch results
- const { data } = await request()
-
- // Process results
- if (data.ocs.data.entries.length > 0) {
- this.$set(this.results, type, data.ocs.data.entries)
- } else {
- this.$delete(this.results, type)
- }
-
- // Save cursor if any
- if (data.ocs.data.cursor) {
- this.$set(this.cursors, type, data.ocs.data.cursor)
- } else if (!data.ocs.data.isPaginated) {
- // If no cursor and no pagination, we save the default amount
- // provided by server's initial state `defaultLimit`
- this.$set(this.limits, type, this.defaultLimit)
- }
-
- // Check if we reached end of pagination
- if (data.ocs.data.entries.length < this.defaultLimit) {
- this.$set(this.reached, type, true)
- }
-
- // If none already focused, focus the first rendered result
- if (this.focused === null) {
- this.focused = 0
- }
- return REQUEST_OK
- } catch (error) {
- this.$delete(this.results, type)
-
- // If this is not a cancelled throw
- if (error.response && error.response.status) {
- this.logger.error(`Error searching for ${this.typesMap[type]}`, error)
- showError(this.t('core', 'An error occurred while searching for {type}', { type: this.typesMap[type] }))
- return REQUEST_FAILED
- }
- return REQUEST_CANCELED
- }
- })).then(results => {
- // Do not declare loading finished if the request have been cancelled
- // This means another search was triggered and we're therefore still loading
- if (results.some(result => result === REQUEST_CANCELED)) {
- return
- }
- // We finished all searches
- this.loading = {}
- })
- },
- onInputDebounced: enableLiveSearch
- ? debounce(function(e) {
- this.onInput(e)
- }, 500)
- : function() {
- this.triggered = false
- },
-
- /**
- * Load more results for the provided type
- *
- * @param {string} type type
- */
- async loadMore(type) {
- // If already loading, ignore
- if (this.loading[type]) {
- return
- }
- if (this.cursors[type]) {
- // Init cancellable request
- const { request, cancel } = search({ type, query: this.query, cursor: this.cursors[type] })
- this.requests.push(cancel)
-
- // Fetch results
- const { data } = await request()
-
- // Save cursor if any
- if (data.ocs.data.cursor) {
- this.$set(this.cursors, type, data.ocs.data.cursor)
- }
-
- // Process results
- if (data.ocs.data.entries.length > 0) {
- this.results[type].push(...data.ocs.data.entries)
- }
-
- // Check if we reached end of pagination
- if (data.ocs.data.entries.length < this.defaultLimit) {
- this.$set(this.reached, type, true)
- }
- } else
-
- // If no cursor, we might have all the results already,
- // let's fake pagination and show the next xxx entries
- if (this.limits[type] && this.limits[type] >= 0) {
- this.limits[type] += this.defaultLimit
-
- // Check if we reached end of pagination
- if (this.limits[type] >= this.results[type].length) {
- this.$set(this.reached, type, true)
- }
- }
-
- // Focus result after render
- if (this.focused !== null) {
- this.$nextTick(() => {
- this.focusIndex(this.focused)
- })
- }
- },
+ // Deprecated events to be removed
+ subscribe('nextcloud:unified-search:reset', () => {
+ emit('nextcloud:unified-search.reset', { query: '' })
+ })
+ subscribe('nextcloud:unified-search:search', ({ query }) => {
+ emit('nextcloud:unified-search.search', { query })
+ })
- /**
- * Return a subset of the array if the search provider
- * doesn't supports pagination
- *
- * @param {Array} list the results
- * @param {string} type the type
- * @return {Array}
- */
- limitIfAny(list, type) {
- if (type in this.limits) {
- return list.slice(0, this.limits[type])
- }
- return list
- },
+ // all done
+ logger.debug('Unified search initialized!')
+ },
- getResultsList() {
- return this.$el.querySelectorAll('.unified-search__results .unified-search__result')
- },
+ beforeDestroy() {
+ // keep in mind to remove the event listener
+ window.removeEventListener('keydown', this.onKeyDown)
+ },
+ methods: {
/**
- * Focus the first result if any
- *
- * @param {Event} event the keydown event
+ * Handle the key down event to open search on `ctrl + F`
+ * @param event The keyboard event
*/
- focusFirst(event) {
- const results = this.getResultsList()
- if (results && results.length > 0) {
- if (event) {
+ onKeyDown(event: KeyboardEvent) {
+ if (event.ctrlKey && event.key === 'f') {
+ // only handle search if not already open - in this case the browser native search should be used
+ if (!this.showLocalSearch && !this.showUnifiedSearch) {
event.preventDefault()
}
- this.focused = 0
- this.focusIndex(this.focused)
+ this.toggleUnifiedSearch()
}
},
/**
- * Focus the next result if any
- *
- * @param {Event} event the keydown event
+ * Toggle the local search if available - otherwise open the unified search modal
*/
- focusNext(event) {
- if (this.focused === null) {
- this.focusFirst(event)
- return
- }
-
- const results = this.getResultsList()
- // If we're not focusing the last, focus the next one
- if (results && results.length > 0 && this.focused + 1 < results.length) {
- event.preventDefault()
- this.focused++
- this.focusIndex(this.focused)
+ toggleUnifiedSearch() {
+ if (this.supportsLocalSearch) {
+ this.showLocalSearch = !this.showLocalSearch
+ } else {
+ this.showUnifiedSearch = !this.showUnifiedSearch
+ this.showLocalSearch = false
}
},
/**
- * Focus the previous result if any
- *
- * @param {Event} event the keydown event
+ * Open the unified search modal
*/
- focusPrev(event) {
- if (this.focused === null) {
- this.focusFirst(event)
- return
- }
-
- const results = this.getResultsList()
- // If we're not focusing the first, focus the previous one
- if (results && results.length > 0 && this.focused > 0) {
- event.preventDefault()
- this.focused--
- this.focusIndex(this.focused)
- }
-
+ openModal() {
+ this.showUnifiedSearch = true
+ this.showLocalSearch = false
},
/**
- * Focus the specified result index if it exists
- *
- * @param {number} index the result index
+ * Emit the updated search query as eventbus events
*/
- focusIndex(index) {
- const results = this.getResultsList()
- if (results && results[index]) {
- results[index].focus()
+ emitUpdatedQuery() {
+ if (this.queryText === '') {
+ emit('nextcloud:unified-search:reset')
+ } else {
+ emit('nextcloud:unified-search:search', { query: this.queryText })
}
},
-
- /**
- * Set the current focused element based on the target
- *
- * @param {Event} event the focus event
- */
- setFocusedIndex(event) {
- const entry = event.target
- const results = this.getResultsList()
- const index = [...results].findIndex(search => search === entry)
- if (index > -1) {
- // let's not use focusIndex as the entry is already focused
- this.focused = index
- }
- },
-
- onClickFilter(filter) {
- this.query = `${this.query} ${filter}`
- .replace(/ {2}/g, ' ')
- .trim()
- this.onInput()
- },
},
-}
+})
</script>
<style lang="scss" scoped>
-@use "sass:math";
-
-$margin: 10px;
-$input-height: 34px;
-$input-padding: 6px;
-
-.unified-search {
- &__trigger {
- filter: var(--background-image-invert-if-bright);
- }
-
- &__input-wrapper {
- position: sticky;
- // above search results
- z-index: 2;
- top: 0;
- display: inline-flex;
- flex-direction: column;
- align-items: center;
- width: 100%;
- background-color: var(--color-main-background);
-
- label[for="unified-search__input"] {
- align-self: flex-start;
- font-weight: bold;
- font-size: 19px;
- margin-left: 13px;
- }
- }
-
- &__form-input {
- margin: 0 !important;
- }
-
- &__input-row {
- display: flex;
- width: 100%;
- align-items: center;
- }
-
- &__filters {
- margin: $margin 0 $margin math.div($margin, 2);
- ul {
- display: inline-flex;
- justify-content: space-between;
- }
- }
-
- &__form {
- position: relative;
- width: 100%;
- margin: $margin 0;
-
- // Loading spinner
- &::after {
- right: $input-padding;
- left: auto;
- }
-
- &-input,
- &-reset {
- margin: math.div($input-padding, 2);
- }
-
- &-input {
- width: 100%;
- height: $input-height;
- padding: $input-padding;
-
- &,
- &[placeholder],
- &::placeholder {
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
-
- // Hide webkit clear search
- &::-webkit-search-decoration,
- &::-webkit-search-cancel-button,
- &::-webkit-search-results-button,
- &::-webkit-search-results-decoration {
- -webkit-appearance: none;
- }
-
- // Ellipsis earlier if reset button is here
- .icon-loading-small &,
- &--with-reset {
- padding-right: $input-height;
- }
- }
-
- &-reset, &-submit {
- position: absolute;
- top: 0;
- right: 4px;
- width: $input-height - $input-padding;
- height: $input-height - $input-padding;
- min-height: 30px;
- padding: 0;
- opacity: .5;
- border: none;
- background-color: transparent;
- margin-right: 0;
-
- &:hover,
- &:focus,
- &:active {
- opacity: 1;
- }
- }
-
- &-submit {
- right: 28px;
- }
- }
-
- &__results {
- &-header {
- display: block;
- margin: $margin;
- margin-bottom: $margin - 4px;
- margin-left: 13px;
- color: var(--color-primary-element);
- font-size: 19px;
- font-weight: bold;
- }
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
-
- .unified-search__result-more::v-deep {
- color: var(--color-text-maxcontrast);
- }
-
- .empty-content {
- margin: 10vh 0;
-
- ::v-deep .empty-content__title {
- font-weight: normal;
- font-size: var(--default-font-size);
- padding: 0 15px;
- text-align: center;
- }
- }
+// this is needed to allow us overriding component styles (focus-visible)
+.unified-search-menu {
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
-
</style>