-
Notifications
You must be signed in to change notification settings - Fork 593
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(CommandPalette): implement component
- Loading branch information
1 parent
1a8ca6f
commit 18dceb7
Showing
6 changed files
with
215 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
<template> | ||
<Combobox @update:modelValue="onSelect"> | ||
<div class="flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800"> | ||
<div class="relative flex items-center"> | ||
<UIcon :name="inputIcon" class="pointer-events-none absolute top-3.5 left-5 h-5 w-5 u-text-gray-400" aria-hidden="true" /> | ||
<ComboboxInput | ||
ref="comboboxInput" | ||
:value="query" | ||
class="w-full h-12 pr-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 pl-[3.25rem] u-text-gray-900 focus:ring-0 sm:text-sm" | ||
:placeholder="inputPlaceholder" | ||
autocomplete="off" | ||
@change="query = $event.target.value" | ||
/> | ||
|
||
<UButton v-if="closeIcon" :icon="closeIcon" variant="transparent" class="absolute right-3" @click="onClear" /> | ||
</div> | ||
|
||
<ComboboxOptions v-if="results.length" static hold class="relative flex-1 overflow-y-auto divide-y u-divide-gray-100 scroll-py-2"> | ||
<CommandPaletteGroup v-for="group of groupedResults" :key="group.key" :group="group" /> | ||
</ComboboxOptions> | ||
|
||
<div v-else class="flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14"> | ||
<UIcon :name="emptyIcon" class="w-6 h-6 mx-auto u-text-gray-400" aria-hidden="true" /> | ||
<p class="mt-4 text-sm u-text-gray-900"> | ||
{{ query ? "We couldn't find any items with that term. Please try again." : "We couldn't find any items." }} | ||
</p> | ||
</div> | ||
</div> | ||
</Combobox> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { ref, computed, onMounted } from 'vue' | ||
import { Combobox, ComboboxInput, ComboboxOptions } from '@headlessui/vue' | ||
import type { PropType, ComponentPublicInstance } from 'vue' | ||
import { useFuse } from '@vueuse/integrations/useFuse' | ||
import type { UseFuseOptions } from '@vueuse/integrations/useFuse' | ||
import type { Group, Command } from '../../types/command-palette' | ||
import CommandPaletteGroup from './CommandPaletteGroup.vue' | ||
const props = defineProps({ | ||
groups: { | ||
type: Array as PropType<Group[]>, | ||
default: () => [] | ||
}, | ||
closeIcon: { | ||
type: String, | ||
default: null | ||
}, | ||
inputIcon: { | ||
type: String, | ||
default: 'heroicons-outline:search' | ||
}, | ||
inputPlaceholder: { | ||
type: String, | ||
default: 'Search...' | ||
}, | ||
emptyIcon: { | ||
type: String, | ||
default: 'heroicons-outline:search' | ||
}, | ||
options: { | ||
type: Object as PropType<UseFuseOptions<Command>>, | ||
default: () => ({ | ||
fuseOptions: { | ||
keys: ['label'], | ||
isCaseSensitive: false, | ||
threshold: 0.1 | ||
}, | ||
resultLimit: 12, | ||
matchAllWhenSearchEmpty: true | ||
}) | ||
} | ||
}) | ||
const emit = defineEmits(['select', 'close']) | ||
const query = ref('') | ||
const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>() | ||
onMounted(() => { | ||
activateFirstOption() | ||
}) | ||
// Computed | ||
const commands = computed(() => props.groups.flatMap(group => group.commands.map(command => ({ ...command, group: group.key })))) | ||
const { results } = useFuse(query, commands, props.options) | ||
const groupedResults = computed(() => { | ||
return props.groups.map(group => ({ | ||
key: group.key, | ||
label: group.label, | ||
commands: results.value.map(result => result.item).filter(item => item.group === group.key) | ||
})).filter(group => group.commands.length) | ||
}) | ||
// Methods | ||
function activateFirstOption () { | ||
// hack combobox by using keyboard event | ||
// https://github.com/tailwindlabs/headlessui/blob/main/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L692 | ||
setTimeout(() => { | ||
comboboxInput.value?.$el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })) | ||
}, 0) | ||
} | ||
function onSelect (option) { | ||
if (option.disabled) { | ||
return | ||
} | ||
emit('select', option, { query: query.value }) | ||
// waiting for modal to be closed | ||
setTimeout(() => { | ||
query.value = '' | ||
}, 300) | ||
} | ||
function onClear () { | ||
if (query.value) { | ||
query.value = '' | ||
} else { | ||
emit('close') | ||
} | ||
} | ||
</script> | ||
|
||
<script lang="ts"> | ||
export default { name: 'UCommandPalette' } | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<template> | ||
<li class="p-2"> | ||
<h2 v-if="group.label" class="px-3 my-2 text-xs font-semibold u-text-gray-900"> | ||
{{ group.label }} | ||
</h2> | ||
|
||
<ul class="text-sm u-text-gray-700"> | ||
<ComboboxOption | ||
v-for="(command, index) of group.commands" | ||
:key="`${group.key}-${index}`" | ||
v-slot="{ active }" | ||
:value="command" | ||
:disabled="command.disabled" | ||
as="template" | ||
> | ||
<li :class="['flex justify-between select-none commands-center rounded-md px-3 py-2 u-text-gray-400', active && 'bg-gray-100 dark:bg-gray-800 u-text-gray-900', command.disabled ? 'cursor-not-allowed' : 'cursor-pointer']"> | ||
<div class="flex commands-center flex-1 min-w-0"> | ||
<UIcon v-if="command.icon" :name="command.icon" :class="['h-4 w-4', command.iconColor, command.iconClass]" class="flex-shrink-0" aria-hidden="true" /> | ||
<UAvatar v-else-if="command.avatar" :src="command.avatar" :alt="command.label" :rounded="false" size="xxxs" /> | ||
<div class="flex commands-center flex-1 w-full ml-3 truncate u-text-gray-400" :class="{ 'opacity-50': command.disabled }"> | ||
<span class="u-text-gray-700">{{ command.label }}</span> | ||
</div> | ||
</div> | ||
</li> | ||
</ComboboxOption> | ||
</ul> | ||
</li> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { ComboboxOption } from '@headlessui/vue' | ||
import type { PropType } from 'vue' | ||
import type { Group } from '../../types/command-palette' | ||
defineProps({ | ||
group: { | ||
type: Object as PropType<Group>, | ||
required: true | ||
} | ||
}) | ||
</script> | ||
|
||
<script lang="ts"> | ||
export default { name: 'UCommandPaletteGroup' } | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
export interface Command { | ||
disabled?: boolean | ||
icon?: string | ||
iconColor?: string | ||
iconClass?: string | ||
avatar?: string | ||
label: string | ||
group?: string | ||
} | ||
|
||
export interface Group { | ||
key: string | ||
label: string | ||
commands: Command[] | ||
} |