Skip to content

Commit 93b3e73

Browse files
committed
feat(CommandPalette): preserve group order in search results
1 parent d1afe90 commit 93b3e73

File tree

4 files changed

+70
-1
lines changed

4 files changed

+70
-1
lines changed

playgrounds/nuxt/app/pages/components/command-palette.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const searchTerm = ref('')
1111
// const searchTermDebounced = refDebounced(searchTerm, 200)
1212
const selected = ref([])
1313
const virtualize = ref(false)
14+
const preserveGroupOrder = ref(false)
1415
1516
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
1617
// params: { q: searchTermDebounced },
@@ -155,6 +156,7 @@ defineShortcuts({
155156
<template>
156157
<Navbar>
157158
<USwitch v-model="virtualize" label="Virtualize" />
159+
<USwitch v-model="preserveGroupOrder" label="Preserve group order" />
158160

159161
<UModal v-model:open="open">
160162
<UButton label="Open modal" color="neutral" variant="outline" />
@@ -176,7 +178,7 @@ defineShortcuts({
176178
<UButton label="Select label (popover)" color="neutral" variant="outline" />
177179

178180
<template #content>
179-
<UCommandPalette v-model="label" placeholder="Search labels..." :groups="[{ id: 'labels', items: labels }]" :ui="{ input: '[&>input]:h-8 [&>input]:text-sm' }" />
181+
<UCommandPalette v-model="label" placeholder="Search labels..." :groups="[{ id: 'labels', items: labels }]" :ui="{ input: '[&>input]:h-8 [&>input]:text-sm' }" :preserve-group-order />
180182
</template>
181183
</UPopover>
182184
</Navbar>
@@ -193,6 +195,7 @@ defineShortcuts({
193195
}
194196
}"
195197
multiple
198+
:preserve-group-order
196199
class="sm:max-h-96"
197200
@update:model-value="onSelect"
198201
>
@@ -224,6 +227,7 @@ defineShortcuts({
224227
<UCommandPalette
225228
v-if="virtualize"
226229
virtualize
230+
:preserve-group-order
227231
:fuse="{ resultLimit: 1000 }"
228232
placeholder="Search virtualized items..."
229233
:groups="[{ id: 'items', items: Array(1000).fill(0).map((_, i) => ({ label: `item-${i}`, value: i, icon: 'i-lucide-file' })) }]"

src/runtime/components/CommandPalette.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ export interface CommandPaletteProps<G extends CommandPaletteGroup<T> = CommandP
153153
* @defaultValue 'label'
154154
*/
155155
labelKey?: GetItemKeys<T>
156+
/**
157+
* Whether to preserve the order of groups as defined in the `groups` prop when filtering.
158+
* When `false`, groups will appear based on item matches.
159+
* @defaultValue false
160+
*/
161+
preserveGroupOrder?: boolean
156162
class?: any
157163
ui?: CommandPalette['slots']
158164
}
@@ -202,6 +208,7 @@ const props = withDefaults(defineProps<CommandPaletteProps<G, T>>(), {
202208
labelKey: 'label',
203209
autofocus: true,
204210
back: true,
211+
preserveGroupOrder: false,
205212
virtualize: false
206213
})
207214
const emits = defineEmits<CommandPaletteEmits<T>>()
@@ -295,6 +302,26 @@ const filteredGroups = computed(() => {
295302
296303
return acc
297304
}, {} as Record<string, (T & { matches?: FuseResult<T>['matches'] })[]>)
305+
if (props.preserveGroupOrder) {
306+
const processedGroups: Array<ReturnType<typeof getGroupWithItems>> = []
307+
308+
for (const group of groups.value || []) {
309+
if (!group.items?.length) {
310+
continue
311+
}
312+
313+
const items
314+
= group.ignoreFilter
315+
? group.items
316+
: groupsById[group.id]
317+
318+
if (items?.length) {
319+
processedGroups.push(getGroupWithItems(group, items))
320+
}
321+
}
322+
323+
return processedGroups
324+
}
298325
299326
const fuseGroups = Object.entries(groupsById).map(([id, items]) => {
300327
const group = groups.value?.find(group => group.id === id)

test/components/CommandPalette.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ describe('CommandPalette', () => {
8080
['with close', { props: { ...props, close: true } }],
8181
['with closeIcon', { props: { ...props, close: true, closeIcon: 'i-lucide-trash' } }],
8282
['with virtualize', { props: { ...props, virtualize: true } }],
83+
['with preserveGroupOrder', { props: { ...props, preserveGroupOrder: true } }],
8384
['with as', { props: { ...props, as: 'section' } }],
8485
['with class', { props: { ...props, class: 'divide-accented' } }],
8586
['with ui', { props: { ...props, ui: { input: '[&>input]:h-10' } } }],

test/components/__snapshots__/CommandPalette.spec.ts.snap

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,43 @@ exports[`CommandPalette > renders with placeholder correctly 1`] = `
756756
</div>"
757757
`;
758758

759+
exports[`CommandPalette > renders with preserveGroupOrder correctly 1`] = `
760+
"<div dir="ltr" class="flex flex-col min-h-0 min-w-0 divide-y divide-default">
761+
<div class="relative inline-flex items-center [&amp;>input]:h-12"><input type="text" placeholder="Type a command or search…" class="w-full rounded-md border-0 appearance-none placeholder:text-dimmed focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-highlighted bg-transparent ps-9" autocomplete="off" aria-disabled="false" value="" aria-activedescendant="reka-listbox-item-v-0-0-1"><span class="absolute inset-y-0 start-0 flex items-center ps-2.5"><span class="iconify i-lucide:search shrink-0 text-dimmed size-5" aria-hidden="true"></span></span>
762+
<!--v-if-->
763+
</div>
764+
<div class="relative overflow-hidden flex flex-col" role="listbox" aria-orientation="vertical" aria-multiselectable="false" data-orientation="vertical">
765+
<div role="presentation" class="relative scroll-py-1 overflow-y-auto flex-1 focus:outline-none divide-y divide-default">
766+
<div role="group" aria-labelledby="reka-listbox-group-v-0-0-0" class="p-1 isolate">
767+
<!--v-if--><button type="button" data-reka-collection-item="" id="reka-listbox-item-v-0-0-1" role="option" tabindex="-1" aria-selected="false" data-state="unchecked" class="group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-highlighted before:bg-elevated" data-highlighted=""><span class="iconify i-lucide:file-plus shrink-0 size-5 text-default" aria-hidden="true"></span><span class="truncate space-x-1 text-dimmed"><!--v-if--><span class="text-highlighted [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary">Add new file</span><span class="text-dimmed [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary">Create a new file in the current directory or workspace.</span></span><span class="ms-auto inline-flex gap-1.5 items-center"><span class="hidden lg:inline-flex items-center shrink-0 gap-0.5"><kbd class="inline-flex items-center justify-center px-1 rounded-sm font-medium font-sans h-5 min-w-[20px] text-[11px] ring ring-inset ring-accented text-default bg-default">Ctrl</kbd><kbd class="inline-flex items-center justify-center px-1 rounded-sm font-medium font-sans h-5 min-w-[20px] text-[11px] ring ring-inset ring-accented text-default bg-default">N</kbd></span>
768+
<!--v-if--></span>
769+
</button><button type="button" data-reka-collection-item="" id="reka-listbox-item-v-0-0-2" role="option" tabindex="-1" aria-selected="false" data-state="unchecked" class="group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50 transition-colors before:transition-colors"><span class="iconify i-lucide:folder-plus shrink-0 size-5 text-dimmed group-data-highlighted:not-group-data-disabled:text-default transition-colors" aria-hidden="true"></span><span class="truncate space-x-1 text-dimmed"><!--v-if--><span class="text-highlighted [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary">Add new folder</span><span class="text-dimmed [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary">Create a new folder in the current directory or workspace.</span></span><span class="ms-auto inline-flex gap-1.5 items-center"><span class="hidden lg:inline-flex items-center shrink-0 gap-0.5"><kbd class="inline-flex items-center justify-center px-1 rounded-sm font-medium font-sans h-5 min-w-[20px] text-[11px] ring ring-inset ring-accented text-default bg-default">Ctrl</kbd><kbd class="inline-flex items-center justify-center px-1 rounded-sm font-medium font-sans h-5 min-w-[20px] text-[11px] ring ring-inset ring-accented text-default bg-default">F</kbd></span>
770+
<!--v-if--></span>
771+
</button><button type="button" disabled="" data-reka-collection-item="" id="reka-listbox-item-v-0-0-3" role="option" tabindex="-1" aria-selected="false" data-disabled="" data-state="unchecked" class="group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50 transition-colors before:transition-colors"><span class="iconify i-lucide:hash shrink-0 size-5 text-dimmed group-data-highlighted:not-group-data-disabled:text-default transition-colors" aria-hidden="true"></span><span class="truncate space-x-1 text-dimmed"><!--v-if--><span class="text-highlighted [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary">Add hashtag</span><span class="text-dimmed [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary">Add a hashtag to the current item.</span></span><span class="ms-auto inline-flex gap-1.5 items-center"><span class="hidden lg:inline-flex items-center shrink-0 gap-0.5"><kbd class="inline-flex items-center justify-center px-1 rounded-sm font-medium font-sans h-5 min-w-[20px] text-[11px] ring ring-inset ring-accented text-default bg-default">Ctrl</kbd><kbd class="inline-flex items-center justify-center px-1 rounded-sm font-medium font-sans h-5 min-w-[20px] text-[11px] ring ring-inset ring-accented text-default bg-default">H</kbd></span>
772+
<!--v-if--></span>
773+
</button><button type="button" data-reka-collection-item="" id="reka-listbox-item-v-0-0-4" role="option" tabindex="-1" aria-selected="false" data-state="unchecked" class="group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50 transition-colors before:transition-colors"><span class="iconify i-lucide:tag shrink-0 size-5 text-dimmed group-data-highlighted:not-group-data-disabled:text-default transition-colors" aria-hidden="true"></span><span class="truncate space-x-1 text-dimmed"><!--v-if--><span class="text-highlighted [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary">Add label</span><span class="text-dimmed [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary">Add a label to the current item.</span></span><span class="ms-auto inline-flex gap-1.5 items-center"><span class="hidden lg:inline-flex items-center shrink-0 gap-0.5"><kbd class="inline-flex items-center justify-center px-1 rounded-sm font-medium font-sans h-5 min-w-[20px] text-[11px] ring ring-inset ring-accented text-default bg-default">Ctrl</kbd><kbd class="inline-flex items-center justify-center px-1 rounded-sm font-medium font-sans h-5 min-w-[20px] text-[11px] ring ring-inset ring-accented text-default bg-default">L</kbd></span>
774+
<!--v-if--></span>
775+
</button>
776+
</div>
777+
<div role="group" aria-labelledby="reka-listbox-group-v-0-0-5" class="p-1 isolate">
778+
<div id="reka-listbox-group-v-0-0-5" class="p-1.5 text-xs font-semibold text-highlighted">Labels</div><button type="button" data-reka-collection-item="" id="reka-listbox-item-v-0-0-6" role="option" tabindex="-1" aria-selected="false" data-state="unchecked" class="group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50 transition-colors before:transition-colors">
779+
<div class="relative inline-flex items-center justify-center shrink-0 size-5"><span class="rounded-full ring ring-bg flex items-center justify-center text-inverted font-medium whitespace-nowrap bg-error h-[8px] min-w-[8px] text-[8px] top-0 right-0"></span></div><span class="truncate space-x-1 text-dimmed"><!--v-if--><span class="text-highlighted [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary">bug</span><span class="text-dimmed [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary"></span></span><span class="ms-auto inline-flex gap-1.5 items-center"><!--v-if--><!--v-if--></span>
780+
</button><button type="button" data-reka-collection-item="" id="reka-listbox-item-v-0-0-7" role="option" tabindex="-1" aria-selected="false" data-state="unchecked" class="group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50 transition-colors before:transition-colors">
781+
<div class="relative inline-flex items-center justify-center shrink-0 size-5"><span class="rounded-full ring ring-bg flex items-center justify-center text-inverted font-medium whitespace-nowrap bg-success h-[8px] min-w-[8px] text-[8px] top-0 right-0"></span></div><span class="truncate space-x-1 text-dimmed"><!--v-if--><span class="text-highlighted [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary">feature</span><span class="text-dimmed [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary"></span></span><span class="ms-auto inline-flex gap-1.5 items-center"><!--v-if--><!--v-if--></span>
782+
</button><button type="button" data-reka-collection-item="" id="reka-listbox-item-v-0-0-8" role="option" tabindex="-1" aria-selected="false" data-state="unchecked" class="group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50 transition-colors before:transition-colors">
783+
<div class="relative inline-flex items-center justify-center shrink-0 size-5"><span class="rounded-full ring ring-bg flex items-center justify-center text-inverted font-medium whitespace-nowrap bg-info h-[8px] min-w-[8px] text-[8px] top-0 right-0"></span></div><span class="truncate space-x-1 text-dimmed"><!--v-if--><span class="text-highlighted [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary">enhancement</span><span class="text-dimmed [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary"></span></span><span class="ms-auto inline-flex gap-1.5 items-center"><!--v-if--><!--v-if--></span>
784+
</button>
785+
</div>
786+
<div role="group" aria-labelledby="reka-listbox-group-v-0-0-9" class="p-1 isolate">
787+
<div id="reka-listbox-group-v-0-0-9" class="p-1.5 text-xs font-semibold text-highlighted">Users</div><a href="https://github.com/benjamincanac" role="option" tabindex="-1" rel="noopener noreferrer" target="_blank" data-reka-collection-item="" id="reka-listbox-item-v-0-0-10" aria-selected="false" data-state="unchecked" class="group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50 transition-colors before:transition-colors"><span class="inline-flex items-center justify-center select-none rounded-full align-middle bg-elevated size-5 text-[10px] shrink-0"><img src="https://github.com/benjamincanac.png" width="20" height="20" class="h-full w-full rounded-[inherit] object-cover"></span><span class="truncate space-x-1 text-dimmed"><!--v-if--><span class="text-highlighted [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary">benjamincanac</span><span class="text-dimmed [&amp;>mark]:text-inverted [&amp;>mark]:bg-primary"></span></span><span class="ms-auto inline-flex gap-1.5 items-center"><!--v-if--><!--v-if--></span></a>
788+
</div>
789+
</div>
790+
</div>
791+
<!--v-if-->
792+
<!--v-if-->
793+
</div>"
794+
`;
795+
759796
exports[`CommandPalette > renders with selectedIcon correctly 1`] = `
760797
"<div dir="ltr" class="flex flex-col min-h-0 min-w-0 divide-y divide-default">
761798
<div class="relative inline-flex items-center [&amp;>input]:h-12"><input type="text" placeholder="Type a command or search…" class="w-full rounded-md border-0 appearance-none placeholder:text-dimmed focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-highlighted bg-transparent ps-9" autocomplete="off" aria-disabled="false" value="" aria-activedescendant="reka-listbox-item-v-0-0-10"><span class="absolute inset-y-0 start-0 flex items-center ps-2.5"><span class="iconify i-lucide:search shrink-0 text-dimmed size-5" aria-hidden="true"></span></span>

0 commit comments

Comments
 (0)