Skip to content

Commit c2c990d

Browse files
authored
chore: search products (#4663)
1 parent 10831e9 commit c2c990d

File tree

3 files changed

+157
-80
lines changed

3 files changed

+157
-80
lines changed

packages/fern-docs/components/src/FernDropdown.tsx

Lines changed: 154 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useResizeObserver } from "@fern-ui/react-commons";
44

55
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
6-
import { Check, Info } from "lucide-react";
6+
import { Check, Info, Search } from "lucide-react";
77
import {
88
type ComponentProps,
99
cloneElement,
@@ -84,6 +84,7 @@ export declare namespace FernDropdown {
8484
};
8585
triggerAsChild?: boolean;
8686
radioGroupProps?: ComponentProps<typeof DropdownMenu.RadioGroup>;
87+
searchable?: boolean;
8788
}
8889
}
8990

@@ -105,19 +106,33 @@ export const FernDropdown = forwardRef<HTMLButtonElement, PropsWithChildren<Fern
105106
onClick,
106107
contentProps,
107108
triggerAsChild = true,
108-
radioGroupProps = {}
109+
radioGroupProps = {},
110+
searchable = false
109111
},
110112
ref
111113
): ReactElement => {
112114
const [isOpen, setOpen] = useState(defaultOpen);
115+
const [searchTerm, setSearchTerm] = useState("");
116+
const searchInputRef = useRef<HTMLInputElement>(null);
117+
113118
const handleOpenChange = useCallback(
114119
(toOpen: boolean) => {
115120
setOpen(toOpen);
116-
if (toOpen && onOpen != null) {
117-
onOpen();
121+
if (toOpen) {
122+
if (onOpen != null) {
123+
onOpen();
124+
}
125+
// Reset search when opening
126+
setSearchTerm("");
127+
// Focus search input when opening if searchable
128+
if (searchable) {
129+
setTimeout(() => {
130+
searchInputRef.current?.focus();
131+
}, 0);
132+
}
118133
}
119134
},
120-
[onOpen]
135+
[onOpen, searchable]
121136
);
122137

123138
const isValueSelected = useCallback(
@@ -126,81 +141,142 @@ export const FernDropdown = forwardRef<HTMLButtonElement, PropsWithChildren<Fern
126141
},
127142
[value]
128143
);
129-
const renderDropdownContent = () => (
130-
<DropdownMenu.Content
131-
sideOffset={4}
132-
collisionPadding={4}
133-
side={side}
134-
align={align}
135-
hideWhenDetached
136-
{...contentProps}
137-
className={cn("fern-dropdown [&_svg]:size-icon", contentProps?.className)}
138-
>
139-
<FernTooltipProvider>
140-
<FernScrollArea rootClassName="min-h-0 shrink" className="p-1" scrollbars="vertical">
141-
{Array.isArray(value) ? (
142-
<div onClick={onClick}>
143-
{options.map((option, idx) =>
144-
option.type === "value" ? (
145-
<FernDropdownItemMultiSelect
146-
key={option.value}
147-
option={option}
148-
isSelected={isValueSelected(option.value)}
149-
onToggle={
150-
onValueChange ??
151-
(() => {
152-
void 0;
153-
})
154-
}
155-
dropdownMenuElement={dropdownMenuElement}
156-
container={container}
157-
/>
158-
) : option.type === "separator" ? (
159-
<DropdownMenu.Separator
160-
key={idx}
161-
className="bg-border-default mx-2 my-1 h-px"
162-
/>
163-
) : null
164-
)}
144+
145+
// filter options based on search term
146+
const filteredOptions = useCallback(() => {
147+
if (!searchable || !searchTerm.trim()) {
148+
return options;
149+
}
150+
151+
const lowerSearchTerm = searchTerm.toLowerCase();
152+
return options.filter((option) => {
153+
if (option.type === "separator") {
154+
return true; // Always include separators
155+
}
156+
if (option.type === "auth") {
157+
return (
158+
option.key.toLowerCase().includes(lowerSearchTerm) ||
159+
option.value.toLowerCase().includes(lowerSearchTerm)
160+
);
161+
}
162+
if (option.type === "product") {
163+
return (
164+
option.title.toLowerCase().includes(lowerSearchTerm) ||
165+
(option.subtitle?.toLowerCase().includes(lowerSearchTerm) ?? false) ||
166+
option.value.toLowerCase().includes(lowerSearchTerm)
167+
);
168+
}
169+
if (option.type === "value") {
170+
const labelText = typeof option.label === "string" ? option.label : option.value;
171+
const helperText = typeof option.helperText === "string" ? option.helperText : "";
172+
return (
173+
labelText.toLowerCase().includes(lowerSearchTerm) ||
174+
helperText.toLowerCase().includes(lowerSearchTerm) ||
175+
option.value.toLowerCase().includes(lowerSearchTerm)
176+
);
177+
}
178+
return false;
179+
});
180+
}, [searchable, searchTerm, options]);
181+
182+
const renderDropdownContent = () => {
183+
const optionsToRender = filteredOptions();
184+
185+
return (
186+
<DropdownMenu.Content
187+
sideOffset={4}
188+
collisionPadding={4}
189+
side={side}
190+
align={align}
191+
hideWhenDetached
192+
{...contentProps}
193+
className={cn("fern-dropdown [&_svg]:size-icon", contentProps?.className)}
194+
>
195+
<FernTooltipProvider>
196+
{searchable && (
197+
<div className="border-border-default border-b p-2">
198+
<div className="relative flex items-center">
199+
<Search className="text-text-muted absolute left-2 size-4" />
200+
<input
201+
ref={searchInputRef}
202+
type="text"
203+
value={searchTerm}
204+
onChange={(e) => setSearchTerm(e.target.value)}
205+
placeholder="Search..."
206+
className="bg-background-default text-text-primary placeholder:text-text-muted w-full rounded border-none py-1.5 pl-8 pr-2 text-sm outline-none"
207+
onKeyDown={(e) => {
208+
// Prevent dropdown from closing on key events
209+
e.stopPropagation();
210+
}}
211+
/>
212+
</div>
165213
</div>
166-
) : (
167-
<DropdownMenu.RadioGroup
168-
value={value}
169-
onValueChange={onValueChange}
170-
onClick={onClick}
171-
{...radioGroupProps}
172-
>
173-
{options.map((option, idx) =>
174-
option.type === "value" ? (
175-
<FernDropdownItemValue
176-
key={option.value}
177-
option={option}
178-
value={value}
179-
dropdownMenuElement={dropdownMenuElement}
180-
container={container}
181-
/>
182-
) : option.type === "product" ? (
183-
<FernProductItem key={option.id} option={option} dense={option.dense} />
184-
) : option.type === "auth" ? (
185-
<FernDropdownItemAuth key={option.key} option={option} />
186-
) : option.type === "separator" ? (
187-
<DropdownMenu.Separator
188-
key={idx}
189-
className="bg-border-default mx-2 my-1 h-px"
190-
/>
191-
) : (
192-
<DropdownMenu.Separator
193-
key={idx}
194-
className="bg-border-default mx-2 my-1 h-px"
195-
/>
196-
)
197-
)}
198-
</DropdownMenu.RadioGroup>
199214
)}
200-
</FernScrollArea>
201-
</FernTooltipProvider>
202-
</DropdownMenu.Content>
203-
);
215+
<FernScrollArea rootClassName="min-h-0 shrink" className="p-1" scrollbars="vertical">
216+
{Array.isArray(value) ? (
217+
<div onClick={onClick}>
218+
{optionsToRender.map((option, idx) =>
219+
option.type === "value" ? (
220+
<FernDropdownItemMultiSelect
221+
key={option.value}
222+
option={option}
223+
isSelected={isValueSelected(option.value)}
224+
onToggle={
225+
onValueChange ??
226+
(() => {
227+
void 0;
228+
})
229+
}
230+
dropdownMenuElement={dropdownMenuElement}
231+
container={container}
232+
/>
233+
) : option.type === "separator" ? (
234+
<DropdownMenu.Separator
235+
key={idx}
236+
className="bg-border-default mx-2 my-1 h-px"
237+
/>
238+
) : null
239+
)}
240+
</div>
241+
) : (
242+
<DropdownMenu.RadioGroup
243+
value={value}
244+
onValueChange={onValueChange}
245+
onClick={onClick}
246+
{...radioGroupProps}
247+
>
248+
{optionsToRender.map((option, idx) =>
249+
option.type === "value" ? (
250+
<FernDropdownItemValue
251+
key={option.value}
252+
option={option}
253+
value={value}
254+
dropdownMenuElement={dropdownMenuElement}
255+
container={container}
256+
/>
257+
) : option.type === "product" ? (
258+
<FernProductItem key={option.id} option={option} dense={option.dense} />
259+
) : option.type === "auth" ? (
260+
<FernDropdownItemAuth key={option.key} option={option} />
261+
) : option.type === "separator" ? (
262+
<DropdownMenu.Separator
263+
key={idx}
264+
className="bg-border-default mx-2 my-1 h-px"
265+
/>
266+
) : (
267+
<DropdownMenu.Separator
268+
key={idx}
269+
className="bg-border-default mx-2 my-1 h-px"
270+
/>
271+
)
272+
)}
273+
</DropdownMenu.RadioGroup>
274+
)}
275+
</FernScrollArea>
276+
</FernTooltipProvider>
277+
</DropdownMenu.Content>
278+
);
279+
};
204280

205281
return (
206282
<DropdownMenu.Root onOpenChange={handleOpenChange} open={isOpen} modal={false} defaultOpen={defaultOpen}>

packages/fern-docs/components/src/header/ProductDropdown.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ export async function ProductDropdown({
5656
src={image?.src}
5757
alt={product.title}
5858
objectFit="cover"
59-
width={image.width || undefined}
60-
height={image.height || undefined}
59+
width={image.width || 32}
60+
height={image.height || 32}
6161
/>
6262
) : undefined
6363
};

packages/fern-docs/components/src/header/ProductDropdownClient.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export function ProductDropdownClient({
7979
radioGroupProps={{
8080
className: "fern-product-selector-radio-group"
8181
}}
82+
searchable={products.length > 10}
8283
>
8384
<div
8485
className={cn("product-dropdown-trigger hidden h-9", {

0 commit comments

Comments
 (0)