search with key

This commit is contained in:
Nate Kelley 2025-10-06 15:07:27 -06:00
parent 0d98535936
commit fd2e61808c
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
5 changed files with 60 additions and 4 deletions

View File

@ -31,6 +31,7 @@ const mockSearchItems: SearchItem[] = [
value: 'document-1', value: 'document-1',
keywords: ['document', 'file', 'pdf'], keywords: ['document', 'file', 'pdf'],
type: 'item', type: 'item',
onSelect: fn(),
}, },
{ {
icon: '📊', icon: '📊',
@ -39,6 +40,7 @@ const mockSearchItems: SearchItem[] = [
value: 'dashboard-1', value: 'dashboard-1',
keywords: ['dashboard', 'analytics', 'charts'], keywords: ['dashboard', 'analytics', 'charts'],
type: 'item', type: 'item',
onSelect: fn(),
}, },
...Array.from({ length: 10 }).map<SearchItem>((_, index) => ({ ...Array.from({ length: 10 }).map<SearchItem>((_, index) => ({
icon: '📊', icon: '📊',
@ -47,6 +49,7 @@ const mockSearchItems: SearchItem[] = [
value: `testing-${index}`, value: `testing-${index}`,
keywords: ['dashboard', 'analytics', 'charts'], keywords: ['dashboard', 'analytics', 'charts'],
type: 'item' as const, type: 'item' as const,
onSelect: fn(),
})), })),
]; ];

View File

@ -4,7 +4,8 @@ import { SearchEmptyState } from './SearchEmptyState';
import { SearchFooter } from './SearchFooter'; import { SearchFooter } from './SearchFooter';
import { SearchInput } from './SearchInput'; import { SearchInput } from './SearchInput';
import { SearchModalContentItems } from './SearchModalContentItems'; import { SearchModalContentItems } from './SearchModalContentItems';
import type { SearchItem, SearchModalContentProps } from './search-modal.types'; import type { SearchModalContentProps } from './search-modal.types';
import { useViewSearchItem } from './useViewSearchItem';
export const SearchModalContent = <M, T extends string>({ export const SearchModalContent = <M, T extends string>({
searchItems, searchItems,
@ -20,6 +21,10 @@ export const SearchModalContent = <M, T extends string>({
secondaryContent, secondaryContent,
openSecondaryContent, openSecondaryContent,
}: SearchModalContentProps<M, T>) => { }: SearchModalContentProps<M, T>) => {
const { handleKeyDown, focusedValue, setFocusedValue } = useViewSearchItem({
searchItems,
onViewSearchItem,
});
const [searchValue, setSearchValue] = useState<string>(defaulSearchValue); const [searchValue, setSearchValue] = useState<string>(defaulSearchValue);
const onSearchChangePreflight = (searchValue: string) => { const onSearchChangePreflight = (searchValue: string) => {
@ -30,7 +35,9 @@ export const SearchModalContent = <M, T extends string>({
return ( return (
<Command <Command
className="min-w-[650px] min-h-[450px] max-h-[75vh] bg-background flex flex-col" className="min-w-[650px] min-h-[450px] max-h-[75vh] bg-background flex flex-col"
value={searchValue} value={focusedValue}
onValueChange={setFocusedValue}
onKeyDown={handleKeyDown}
> >
<SearchInput <SearchInput
searchValue={searchValue} searchValue={searchValue}

View File

@ -69,6 +69,7 @@ const SearchItemComponent = <M, T extends string>({
value={value} value={value}
disabled={disabled} disabled={disabled}
onSelect={() => { onSelect={() => {
console.log('onSelect', value);
onSelect?.(); onSelect?.();
}} }}
> >

View File

@ -6,7 +6,7 @@ export type SearchItem<M = unknown, T extends string = string> = {
value: T; value: T;
keywords?: string[]; keywords?: string[];
meta?: M; meta?: M;
onSelect?: () => void; onSelect?: () => void; //should only be used for side effects
loading?: boolean; loading?: boolean;
disabled?: boolean; disabled?: boolean;
type: 'item'; type: 'item';
@ -33,7 +33,7 @@ export type SearchModalContentProps<M = unknown, T extends string = string> = {
filterContent?: React.ReactNode; filterContent?: React.ReactNode;
searchItems: SearchItems<M, T>[]; searchItems: SearchItems<M, T>[];
onSearchChange: (searchValue: string) => void; onSearchChange: (searchValue: string) => void;
onSelect: (item: SearchItem<M, T>) => void; onSelect: (item: SearchItem<M, T>, modifier: 'select' | 'navigate') => void;
onViewSearchItem: (item: SearchItem<M, T>) => void; onViewSearchItem: (item: SearchItem<M, T>) => void;
emptyState?: React.ReactNode | string; emptyState?: React.ReactNode | string;
placeholder?: string; placeholder?: string;

View File

@ -0,0 +1,45 @@
import { useState } from 'react';
import type { SearchItem, SearchItems } from './search-modal.types';
export const useViewSearchItem = <M, T extends string>({
searchItems,
onViewSearchItem,
}: {
searchItems: SearchItems<M, T>[];
onViewSearchItem: (item: SearchItem<M, T>) => void;
}) => {
const [focusedValue, setFocusedValue] = useState<string>('');
const handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && onViewSearchItem) {
// Wait for next tick to get updated focusedValue
setTimeout(() => {
if (!focusedValue) return;
const findItem = (items: typeof searchItems): SearchItem<M, T> | null => {
for (const item of items) {
if (item.type === 'item' && item.value === focusedValue) {
return item;
}
if (item.type === 'group') {
const found = findItem(item.items);
if (found) return found;
}
}
return null;
};
const item = findItem(searchItems);
if (item) {
onViewSearchItem(item);
}
}, 0);
}
};
return {
handleKeyDown,
focusedValue,
setFocusedValue,
};
};