<script setup> import { PerfectScrollbar } from 'vue3-perfect-scrollbar' import { VList, VListItem, } from 'vuetify/components/VList' const props = defineProps({ isDialogVisible: { type: Boolean, required: true, }, searchResults: { type: Array, required: true, }, }) const emit = defineEmits([ 'update:isDialogVisible', 'search', ]) // 👉 Hotkey // eslint-disable-next-line camelcase const { ctrl_k, meta_k } = useMagicKeys({ passive: false, onEventFired(e) { if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') e.preventDefault() }, }) const refSearchList = ref() const refSearchInput = ref() const searchQueryLocal = ref('') // 👉 watching control + / to open dialog /* eslint-disable camelcase */ watch([ ctrl_k, meta_k, ], () => { emit('update:isDialogVisible', true) }) /* eslint-enable */ // 👉 clear search result and close the dialog const clearSearchAndCloseDialog = () => { searchQueryLocal.value = '' emit('update:isDialogVisible', false) } const getFocusOnSearchList = e => { if (e.key === 'ArrowDown') { e.preventDefault() refSearchList.value?.focus('next') } else if (e.key === 'ArrowUp') { e.preventDefault() refSearchList.value?.focus('prev') } } const dialogModelValueUpdate = val => { searchQueryLocal.value = '' emit('update:isDialogVisible', val) } watch(() => props.isDialogVisible, () => { searchQueryLocal.value = '' }) </script> <template> <VDialog max-width="600" :model-value="props.isDialogVisible" :height="$vuetify.display.smAndUp ? '537' : '100%'" :fullscreen="$vuetify.display.width < 600" class="app-bar-search-dialog" @update:model-value="dialogModelValueUpdate" @keyup.esc="clearSearchAndCloseDialog" > <VCard height="100%" width="100%" class="position-relative" > <VCardText class="py-3 px-4"> <!-- 👉 Search Input --> <VTextField ref="refSearchInput" v-model="searchQueryLocal" autofocus density="compact" variant="plain" class="app-bar-search-input" @keyup.esc="clearSearchAndCloseDialog" @keydown="getFocusOnSearchList" @update:model-value="$emit('search', searchQueryLocal)" > <!-- 👉 Prepend Inner --> <template #prepend-inner> <div class="d-flex align-center text-high-emphasis me-1"> <VIcon size="24" icon="ri-search-line" style=" margin-block-start: 1px; opacity: 1;" /> </div> </template> <!-- 👉 Append Inner --> <template #append-inner> <div class="d-flex align-start"> <div class="text-base text-disabled cursor-pointer me-1" @click="clearSearchAndCloseDialog" > [esc] </div> <IconBtn class="mt-n2" color="medium-emphasis" @click="clearSearchAndCloseDialog" > <VIcon icon="ri-close-line" /> </IconBtn> </div> </template> </VTextField> </VCardText> <!-- 👉 Divider --> <VDivider /> <!-- 👉 Perfect Scrollbar --> <PerfectScrollbar :options="{ wheelPropagation: false, suppressScrollX: true }" class="h-100" > <!-- 👉 Search List --> <VList v-show="searchQueryLocal.length && !!props.searchResults.length" ref="refSearchList" density="compact" class="app-bar-search-list py-0" > <!-- 👉 list Item /List Sub header --> <template v-for="item in props.searchResults" :key="item" > <slot name="searchResult" :item="item" > <VListItem> {{ item }} </VListItem> </slot> </template> </VList> <!-- 👉 Suggestions --> <div v-show="!!props.searchResults && !searchQueryLocal && $slots.suggestions" class="h-100" > <slot name="suggestions" /> </div> <!-- 👉 No Data found --> <div v-show="!props.searchResults.length && searchQueryLocal.length" class="h-100" > <slot name="noData"> <VCardText class="h-100"> <div class="app-bar-search-suggestions d-flex flex-column align-center justify-center text-high-emphasis pa-12"> <VIcon size="64" icon="ri-file-forbid-line" /> <div class="d-flex align-center flex-wrap justify-center gap-2 text-h5 my-3"> <span>No Result For </span> <span>"{{ searchQueryLocal }}"</span> </div> <slot name="noDataSuggestion" /> </div> </VCardText> </slot> </div> </PerfectScrollbar> </VCard> </VDialog> </template> <style lang="scss"> .app-bar-search-suggestions { .app-bar-search-suggestion { &:hover { color: rgb(var(--v-theme-primary)); } } } .app-bar-search-dialog { .app-bar-search-input { .v-field__input { padding-block-start: 0.2rem; } } .v-overlay__scrim { backdrop-filter: blur(4px); } .app-bar-search-list { .v-list-item, .v-list-subheader { font-size: 0.75rem; padding-inline: 1rem; } .v-list-item { .v-list-item__append { .enter-icon { visibility: hidden; } } &:hover, &:active, &:focus { .v-list-item__append { .enter-icon { visibility: visible; } } } } .v-list-subheader { line-height: 1; min-block-size: auto; padding-block: 16px 8px; padding-inline-start: 1rem; text-transform: uppercase; } } } @supports selector(:focus-visible) { .app-bar-search-dialog { .v-list-item:focus-visible::after { content: none; } } } </style> <style lang="scss" scoped> .card-list { --v-card-list-gap: 16px; } </style>