Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/fern-docs/components/src/FernDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export declare namespace FernDropdown {
};
triggerAsChild?: boolean;
radioGroupProps?: ComponentProps<typeof DropdownMenu.RadioGroup>;
header?: ReactNode;
}
}

Expand All @@ -105,7 +106,8 @@ export const FernDropdown = forwardRef<HTMLButtonElement, PropsWithChildren<Fern
onClick,
contentProps,
triggerAsChild = true,
radioGroupProps = {}
radioGroupProps = {},
header
},
ref
): ReactElement => {
Expand Down Expand Up @@ -138,6 +140,11 @@ export const FernDropdown = forwardRef<HTMLButtonElement, PropsWithChildren<Fern
>
<FernTooltipProvider>
<FernScrollArea rootClassName="min-h-0 shrink" className="p-1" scrollbars="vertical">
{header ? (
<div className="sticky top-0 z-10 border-b border-(color:--grayscale-a3) bg-(color:--surface) p-2">
{header}
</div>
) : null}
{Array.isArray(value) ? (
<div onClick={onClick}>
{options.map((option, idx) =>
Expand Down
40 changes: 40 additions & 0 deletions packages/fern-docs/components/src/header/DropdownSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client";

import * as React from "react";
import { cn } from "../cn";

export interface DropdownSearchProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
isOpen?: boolean;
listboxId?: string;
}

export const DropdownSearch = React.forwardRef<HTMLInputElement, DropdownSearchProps>(
({ className, label = "Search products", isOpen, listboxId, ...rest }, ref) => {
return (
<div className="w-full">
<label htmlFor={rest.id} className="sr-only">
{label}
</label>
<input
ref={ref}
{...rest}
role="combobox"
aria-autocomplete="list"
aria-expanded={isOpen}
aria-controls={listboxId}
autoCorrect="off"
autoCapitalize="none"
spellCheck={false}
className={cn(
"h-9 w-full rounded-md border border-(color:--grayscale-a4) bg-(color:--surface) px-3 text-sm outline-none",
"focus:border-(color:--grayscale-a6)",
className
)}
/>
</div>
);
}
);

DropdownSearch.displayName = "DropdownSearch";
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { slugToHref } from "@fern-api/docs-utils";
import type { FernNavigation } from "@fern-api/fdr-sdk";
import { useIsDesktop } from "@fern-ui/react-commons";
import { ChevronDown, ChevronsUpDown } from "lucide-react";
// === NEW ===
import * as React from "react";
import { cn } from "../cn";
import { FernDropdown } from "../FernDropdown";
import { FernSelectionItem } from "../FernSelectionItem";
import { useCurrentProductId, useCurrentProductSlug } from "../state/navigation";
import { DropdownSearch } from "./DropdownSearch";

export interface ProductDropdownItem {
productId: string;
Expand Down Expand Up @@ -39,14 +42,23 @@ export function ProductDropdownClient({
products.find((product) => product.default) ??
products.find((product) => product.productId === fallbackProduct.productId);

// === NEW: search (only used for large menus) ===
const enableSearch = products.length > 10;
const [query, setQuery] = React.useState("");
const normalized = query.trim().toLowerCase();
const filteredProducts = React.useMemo(() => {
if (!enableSearch || !normalized) return products;
return products.filter((p) => `${p.title} ${p.subtitle ?? ""}`.toLowerCase().includes(normalized));
}, [enableSearch, normalized, products]);

if (!currentProduct) {
return null;
}

return (
<FernDropdown
value={currentProductId}
options={products.map(({ icon, image, productId, title, slug, subtitle, defaultSlug, href }) => {
options={filteredProducts.map(({ icon, image, productId, title, slug, subtitle, defaultSlug, href }) => {
const productHref =
href ??
slugToHref(
Expand Down Expand Up @@ -79,6 +91,19 @@ export function ProductDropdownClient({
radioGroupProps={{
className: "fern-product-selector-radio-group"
}}
header={
enableSearch ? (
<DropdownSearch
id="product-search"
placeholder="Search products…"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
) : null
}
onValueChange={() => {
if (enableSearch) setQuery("");
}}
>
<div
className={cn("product-dropdown-trigger hidden h-9", {
Expand Down