UI: Support onSelect prop in <SearchDialog /> component

This commit is contained in:
Fuma Nama
2025-12-24 16:20:03 +08:00
parent b752affbcb
commit da98fe2d4e
3 changed files with 58 additions and 50 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'fumadocs-ui': patch
'@fumadocs/base-ui': patch
---
Support `onSelect` prop in `<SearchDialog />` component
@@ -6,8 +6,8 @@ import {
createContext,
Fragment,
type ReactNode,
use,
useCallback,
useContext,
useEffect,
useEffectEvent,
useMemo,
@@ -45,6 +45,7 @@ export type { SharedProps };
export interface SearchDialogProps extends SharedProps {
search: string;
onSearchChange: (v: string) => void;
onSelect?: (item: SearchItemType) => void;
isLoading?: boolean;
children: ReactNode;
@@ -55,7 +56,7 @@ const Context = createContext<{
onOpenChange: (open: boolean) => void;
search: string;
onSearchChange: (v: string) => void;
onSelect: (item: SearchItemType) => void;
isLoading: boolean;
} | null>(null);
@@ -76,9 +77,22 @@ export function SearchDialog({
search,
onSearchChange,
isLoading = false,
onSelect: onSelectProp,
children,
}: SearchDialogProps) {
const [active, setActive] = useState<string | null>(null);
const router = useRouter();
const onSelect = useEffectEvent((item: SearchItemType) => {
if (item.type === 'action') {
item.onSelect();
} else if (item.external) {
window.open(item.url, '_blank')?.focus();
} else {
router.push(item.url);
}
onOpenChange(false);
onSelectProp?.(item);
});
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
@@ -89,11 +103,11 @@ export function SearchDialog({
onOpenChange,
search,
onSearchChange,
active,
setActive,
// eslint-disable-next-line react-hooks/rules-of-hooks -- used in child components
onSelect,
isLoading,
}),
[active, isLoading, onOpenChange, onSearchChange, open, search],
[isLoading, onOpenChange, onSearchChange, open, search],
)}
>
{children}
@@ -226,23 +240,10 @@ export function SearchDialogList({
Item?: (props: { item: SearchItemType; onClick: () => void }) => ReactNode;
}) {
const ref = useRef<HTMLDivElement>(null);
const { onSelect } = useSearch();
const [active, setActive] = useState<string | null>(() =>
items && items.length > 0 ? items[0].id : null,
);
const { onOpenChange } = useSearch();
const router = useRouter();
const onOpen = (item: SearchItemType) => {
if (item.type === 'action') {
item.onSelect();
} else if (item.external) {
window.open(item.url, '_blank')?.focus();
} else {
router.push(item.url);
}
onOpenChange(false);
};
const onKey = useEffectEvent((e: KeyboardEvent) => {
if (!items || e.isComposing) return;
@@ -260,7 +261,7 @@ export function SearchDialogList({
if (e.key === 'Enter') {
const selected = items.find((item) => item.id === active);
if (selected) onOpen(selected);
if (selected) onSelect(selected);
e.preventDefault();
}
});
@@ -323,7 +324,7 @@ export function SearchDialogList({
{items?.map((item) => (
<Fragment key={item.id}>
{Item({ item, onClick: () => onOpen(item) })}
{Item({ item, onClick: () => onSelect(item) })}
</Fragment>
))}
</ListContext.Provider>
@@ -514,19 +515,19 @@ function renderHighlights(highlights: HighlightedText<ReactNode>[]): ReactNode {
}
export function useSearch() {
const ctx = useContext(Context);
const ctx = use(Context);
if (!ctx) throw new Error('Missing <SearchDialog />');
return ctx;
}
export function useTagsList() {
const ctx = useContext(TagsListContext);
const ctx = use(TagsListContext);
if (!ctx) throw new Error('Missing <TagsList />');
return ctx;
}
export function useSearchList() {
const ctx = useContext(ListContext);
const ctx = use(ListContext);
if (!ctx) throw new Error('Missing <SearchDialogList />');
return ctx;
}
@@ -6,8 +6,8 @@ import {
createContext,
Fragment,
type ReactNode,
use,
useCallback,
useContext,
useEffect,
useEffectEvent,
useMemo,
@@ -50,6 +50,7 @@ export type { SharedProps };
export interface SearchDialogProps extends SharedProps {
search: string;
onSearchChange: (v: string) => void;
onSelect?: (item: SearchItemType) => void;
isLoading?: boolean;
children: ReactNode;
@@ -60,7 +61,7 @@ const Context = createContext<{
onOpenChange: (open: boolean) => void;
search: string;
onSearchChange: (v: string) => void;
onSelect: (item: SearchItemType) => void;
isLoading: boolean;
} | null>(null);
@@ -81,9 +82,22 @@ export function SearchDialog({
search,
onSearchChange,
isLoading = false,
onSelect: onSelectProp,
children,
}: SearchDialogProps) {
const [active, setActive] = useState<string | null>(null);
const router = useRouter();
const onSelect = useEffectEvent((item: SearchItemType) => {
if (item.type === 'action') {
item.onSelect();
} else if (item.external) {
window.open(item.url, '_blank')?.focus();
} else {
router.push(item.url);
}
onOpenChange(false);
onSelectProp?.(item);
});
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -94,11 +108,11 @@ export function SearchDialog({
onOpenChange,
search,
onSearchChange,
active,
setActive,
// eslint-disable-next-line react-hooks/rules-of-hooks -- used in child components
onSelect,
isLoading,
}),
[active, isLoading, onOpenChange, onSearchChange, open, search],
[isLoading, onOpenChange, onSearchChange, open, search],
)}
>
{children}
@@ -223,23 +237,10 @@ export function SearchDialogList({
Item?: (props: { item: SearchItemType; onClick: () => void }) => ReactNode;
}) {
const ref = useRef<HTMLDivElement>(null);
const { onSelect } = useSearch();
const [active, setActive] = useState<string | null>(() =>
items && items.length > 0 ? items[0].id : null,
);
const { onOpenChange } = useSearch();
const router = useRouter();
const onOpen = (item: SearchItemType) => {
if (item.type === 'action') {
item.onSelect();
} else if (item.external) {
window.open(item.url, '_blank')?.focus();
} else {
router.push(item.url);
}
onOpenChange(false);
};
const onKey = useEffectEvent((e: KeyboardEvent) => {
if (!items || e.isComposing) return;
@@ -257,7 +258,7 @@ export function SearchDialogList({
if (e.key === 'Enter') {
const selected = items.find((item) => item.id === active);
if (selected) onOpen(selected);
if (selected) onSelect(selected);
e.preventDefault();
}
});
@@ -320,7 +321,7 @@ export function SearchDialogList({
{items?.map((item) => (
<Fragment key={item.id}>
{Item({ item, onClick: () => onOpen(item) })}
{Item({ item, onClick: () => onSelect(item) })}
</Fragment>
))}
</ListContext.Provider>
@@ -511,19 +512,19 @@ function renderHighlights(highlights: HighlightedText<ReactNode>[]): ReactNode {
}
export function useSearch() {
const ctx = useContext(Context);
const ctx = use(Context);
if (!ctx) throw new Error('Missing <SearchDialog />');
return ctx;
}
export function useTagsList() {
const ctx = useContext(TagsListContext);
const ctx = use(TagsListContext);
if (!ctx) throw new Error('Missing <TagsList />');
return ctx;
}
export function useSearchList() {
const ctx = useContext(ListContext);
const ctx = use(ListContext);
if (!ctx) throw new Error('Missing <SearchDialogList />');
return ctx;
}