diff options
Diffstat (limited to 'core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue')
-rw-r--r-- | core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue b/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue new file mode 100644 index 00000000000..171eada8a06 --- /dev/null +++ b/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue @@ -0,0 +1,166 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <Transition> + <div v-if="open" + class="local-unified-search animated-width" + :class="{ 'local-unified-search--open': open }"> + <!-- We can not use labels as it breaks the header layout so only aria-label and placeholder --> + <NcInputField ref="searchInput" + class="local-unified-search__input animated-width" + :aria-label="t('core', 'Search in current app')" + :placeholder="t('core', 'Search in current app')" + show-trailing-button + :trailing-button-label="t('core', 'Clear search')" + :value="query" + @update:value="$emit('update:query', $event)" + @trailing-button-click="clearAndCloseSearch"> + <template #trailing-button-icon> + <NcIconSvgWrapper :path="mdiClose" /> + </template> + </NcInputField> + + <NcButton ref="searchGlobalButton" + class="local-unified-search__global-search" + :aria-label="t('core', 'Search everywhere')" + :title="t('core', 'Search everywhere')" + type="tertiary-no-background" + @click="$emit('global-search')"> + <template v-if="!isMobile" #default> + {{ t('core', 'Search everywhere') }} + </template> + <template #icon> + <NcIconSvgWrapper :path="mdiCloudSearchOutline" /> + </template> + </NcButton> + </div> + </Transition> +</template> + +<script lang="ts" setup> +import type { ComponentPublicInstance } from 'vue' +import { mdiCloudSearchOutline, mdiClose } from '@mdi/js' +import { translate as t } from '@nextcloud/l10n' +import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile' +import { useElementSize } from '@vueuse/core' +import { computed, ref, watchEffect } from 'vue' + +import NcButton from '@nextcloud/vue/components/NcButton' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcInputField from '@nextcloud/vue/components/NcInputField' + +const props = defineProps<{ + query: string, + open: boolean +}>() + +const emit = defineEmits<{ + (e: 'update:open', open: boolean): void + (e: 'update:query', query: string): void + (e: 'global-search'): void +}>() + +// Hacky type until the library provides real Types +type FocusableComponent = ComponentPublicInstance<object, object, object, Record<string, never>, { focus: () => void }> +/** The input field component */ +const searchInput = ref<FocusableComponent>() +/** When the search bar is opened we focus the input */ +watchEffect(() => { + if (props.open && searchInput.value) { + searchInput.value.focus() + } +}) + +/** Current window size is below the "mobile" breakpoint (currently 1024px) */ +const isMobile = useIsMobile() + +const searchGlobalButton = ref<ComponentPublicInstance>() +/** Width of the search global button, used to resize the input field */ +const { width: searchGlobalButtonWidth } = useElementSize(searchGlobalButton) +const searchGlobalButtonCSSWidth = computed(() => searchGlobalButtonWidth.value ? `${searchGlobalButtonWidth.value}px` : 'var(--default-clickable-area)') + +/** + * Clear the search query and close the search bar + */ +function clearAndCloseSearch() { + emit('update:query', '') + emit('update:open', false) +} +</script> + +<style scoped lang="scss"> +.local-unified-search { + --local-search-width: min(calc(250px + v-bind('searchGlobalButtonCSSWidth')), 95vw); + box-sizing: border-box; + position: relative; + height: var(--header-height); + width: var(--local-search-width); + display: flex; + align-items: center; + // Ensure it overlays the other entries + z-index: 10; + // add some padding for the focus visible outline + padding-inline: var(--border-width-input-focused); + // hide the overflow - needed for the transition + overflow: hidden; + // Ensure the position is fixed also during "position: absolut" (transition) + inset-inline-end: 0; + + #{&} &__global-search { + position: absolute; + inset-inline-end: var(--default-clickable-area); + } + + #{&} &__input { + box-sizing: border-box; + // override some nextcloud-vue styles + margin: 0; + width: var(--local-search-width); + + // Fixup the spacing so we can fit in the "search globally" button + // this can break at any time the component library changes + :deep(input) { + // search global width + close button width + padding-inline-end: calc(v-bind('searchGlobalButtonCSSWidth') + var(--default-clickable-area)); + } + } +} + +.animated-width { + transition: width var(--animation-quick) linear; +} + +// Make the position absolute during the transition +// this is needed to "hide" the button behind it +.v-leave-active { + position: absolute !important; +} + +.v-enter, +.v-leave-to { + &.local-unified-search { + // Start with only the overlay button + --local-search-width: var(--clickable-area-large); + } +} + +@media screen and (max-width: 500px) { + .local-unified-search.local-unified-search--open { + // 100% but still show the menu toggle on the very right + --local-search-width: 100vw; + padding-inline: var(--default-grid-baseline); + } + + // when open we need to position it absolute to allow overlay the full bar + :global(.unified-search-menu:has(.local-unified-search--open)) { + position: absolute !important; + inset-inline: 0; + } + // Hide all other entries, especially the user menu as it might leak pixels + :global(.header-end:has(.local-unified-search--open) > :not(.unified-search-menu)) { + display: none; + } +} +</style> |