Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
09ff9b6
feat: added support for search context on dropdown menu
giuliana-gladeye Jul 14, 2026
d070b49
feat: added new examples on docs to test out search on dropdown menu
giuliana-gladeye Jul 14, 2026
ecd28f9
feat: added search to filter dropdown and radio dropdown
giuliana-gladeye Jul 14, 2026
a61c80d
Merge branch 'main' into feat/search-dropdown
tmccoy14 Jul 17, 2026
757cb6d
fix: removed unused imports
giuliana-gladeye Jul 19, 2026
3fd3a88
feat: updated radio dropdown docs
giuliana-gladeye Jul 19, 2026
6a6f0c5
feat: updated filter dropdown docs
giuliana-gladeye Jul 19, 2026
6e52002
feat: updated DropdownMenuEmpty for screen readers
giuliana-gladeye Jul 19, 2026
495ae5e
feat: removed search from dropdown menu with submenu example
giuliana-gladeye Jul 19, 2026
73fc547
fix: comment typo
giuliana-gladeye Jul 19, 2026
cb67313
feat: update ellipsis on dropdown menu
giuliana-gladeye Jul 20, 2026
7c6cd0e
feat: added aria label to input on dropdown menu
giuliana-gladeye Jul 20, 2026
faf8c9b
feat: stable registerItem/unregisterItem/enabled values instead of th…
giuliana-gladeye Jul 20, 2026
6894c2c
feat: added forward ref to dropdown menu search input
giuliana-gladeye Jul 20, 2026
8a5e969
feat: added searchable prop to both FilterDropdown and RadioDropdown
giuliana-gladeye Jul 20, 2026
2c5dde1
Use avatar for team member dropdown search example
Shrinks99 Jul 24, 2026
8eff504
Update avatar styling
Shrinks99 Jul 24, 2026
48b5936
Improve keyboard navigation when searching
Shrinks99 Jul 24, 2026
f414493
Add usage recommendation
Shrinks99 Jul 24, 2026
f8cba39
Adjust dropdown menu separator margin
Shrinks99 Jul 24, 2026
3bc3a0c
Improve submenu search rendering
Shrinks99 Jul 24, 2026
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
163 changes: 161 additions & 2 deletions packages/demo/src/components/demo/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,33 @@ import {
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuSearch,
DropdownMenuEmpty,
Avatar,
AvatarFallback,
} from "@eqtylab/equality";
import { Settings, User, LogOut } from "lucide-react";

const MEMBERS = [
{ name: "Ada Lovelace", initials: "AL" },
{ name: "Alan Turing", initials: "AT" },
{ name: "Grace Hopper", initials: "GH" },
{ name: "Katherine Johnson", initials: "KJ" },
{ name: "Linus Torvalds", initials: "LT" },
{ name: "Margaret Hamilton", initials: "MH" },
];

const COLUMN_LABELS: Record<string, string> = {
name: "Name",
email: "Email",
role: "Role",
status: "Status",
created: "Created",
lastActive: "Last active",
team: "Team",
location: "Location",
};

export const DropdownMenuDemo = ({
variant = "default",
}: {
Expand All @@ -28,12 +52,26 @@ export const DropdownMenuDemo = ({
| "with-radio"
| "with-shortcuts"
| "with-submenu"
| "with-groups";
| "with-groups"
| "with-search"
| "with-search-always"
| "with-search-submenu";
}) => {
const [showStatusBar, setShowStatusBar] = useState(true);
const [showActivityBar, setShowActivityBar] = useState(false);
const [showPanel, setShowPanel] = useState(false);
const [position, setPosition] = useState("bottom");
const [assignee, setAssignee] = useState<string | null>(null);
const [columns, setColumns] = useState<Record<string, boolean>>({
name: true,
email: true,
role: true,
status: false,
created: false,
lastActive: false,
team: false,
location: false,
});

if (variant === "default") {
return (
Expand Down Expand Up @@ -205,7 +243,25 @@ export const DropdownMenuDemo = ({
<DropdownMenuItem>Create Shortcut...</DropdownMenuItem>
<DropdownMenuItem>Name Window...</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Developer Tools</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span>Developer Tools</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Console</DropdownMenuItem>
<DropdownMenuItem>Network</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span>Profiling</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Performance</DropdownMenuItem>
<DropdownMenuItem>Memory</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
Expand Down Expand Up @@ -256,5 +312,108 @@ export const DropdownMenuDemo = ({
);
}

if (variant === "with-search") {
return (
<div style={{ margin: "1rem 0" }}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="tertiary">
{assignee ? `Assigned: ${assignee}` : "Assign reviewer"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuSearch placeholder="Search members..." />
<DropdownMenuLabel>Team members</DropdownMenuLabel>
{MEMBERS.map((person) => (
<DropdownMenuItem
key={person.name}
textValue={person.name}
onSelect={() => setAssignee(person.name)}
>
<Avatar size="sm">
<AvatarFallback>{person.initials}</AvatarFallback>
</Avatar>
<span>{person.name}</span>
</DropdownMenuItem>
))}
<DropdownMenuEmpty>No members found</DropdownMenuEmpty>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

if (variant === "with-search-always") {
return (
<div style={{ margin: "1rem 0" }}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="tertiary">
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuSearch alwaysVisible placeholder="Search columns..." />
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
{Object.entries(COLUMN_LABELS).map(([key, label]) => (
<DropdownMenuCheckboxItem
key={key}
checked={columns[key]}
onCheckedChange={(checked) =>
setColumns((prev) => ({ ...prev, [key]: checked }))
}
onSelect={(event) => event.preventDefault()}
>
{label}
</DropdownMenuCheckboxItem>
))}
<DropdownMenuEmpty>No columns found</DropdownMenuEmpty>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

if (variant === "with-search-submenu") {
return (
<div style={{ margin: "1rem 0" }}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="tertiary">
Edit
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuSearch placeholder="Search actions..." />
<DropdownMenuItem>Cut</DropdownMenuItem>
<DropdownMenuItem>Copy</DropdownMenuItem>
<DropdownMenuItem>Paste</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span>More Tools</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Save Page As...</DropdownMenuItem>
<DropdownMenuItem>Create Shortcut...</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span>Developer Tools</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Console</DropdownMenuItem>
<DropdownMenuItem>Network</DropdownMenuItem>
<DropdownMenuItem>Task Manager</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuEmpty>No actions found</DropdownMenuEmpty>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

return null;
};
52 changes: 43 additions & 9 deletions packages/demo/src/components/demo/filter-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,56 @@
import { useState } from "react";
import { useState, type Dispatch, type SetStateAction } from "react";

import { FilterDropdown } from "@eqtylab/equality";

export const FilterDropdownDemo = () => {
const TEAMS = [
{ value: "applied-research", label: "Applied research" },
{ value: "data-platform", label: "Data platform" },
{ value: "design-system", label: "Design system" },
{ value: "developer-relations", label: "Developer relations" },
{ value: "governance", label: "Governance" },
{ value: "infrastructure", label: "Infrastructure" },
{ value: "legal", label: "Legal" },
{ value: "security", label: "Security" },
{ value: "solutions-engineering", label: "Solutions engineering" },
{ value: "trust-and-safety", label: "Trust and safety" },
];

export const FilterDropdownDemo = ({
variant = "default",
}: {
variant?: "default" | "searchable";
}) => {
const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
const [selectedTeams, setSelectedTeams] = useState<string[]>([]);

const onToggleFilter = (value: string) => {
setSelectedFilters((prev: string[]) => {
const toggle = (
setter: Dispatch<SetStateAction<string[]>>,
value: string,
) => {
setter((prev: string[]) => {
if (prev.includes(value)) {
return prev.filter((v) => v !== value);
}
return [...prev, value];
});
};

const onClearAll = () => {
setSelectedFilters([]);
};
if (variant === "searchable") {
return (
<div className="mb-4">
<FilterDropdown
label="Team"
options={TEAMS}
selectedFilters={selectedTeams}
onToggleFilter={(value) => toggle(setSelectedTeams, value)}
onClearAll={() => setSelectedTeams([])}
searchable
searchPlaceholder="Search teams..."
emptyPlaceholder="No teams match"
/>
</div>
);
}

return (
<FilterDropdown
Expand All @@ -26,8 +60,8 @@ export const FilterDropdownDemo = () => {
{ value: "project", label: "Project" },
]}
selectedFilters={selectedFilters}
onToggleFilter={onToggleFilter}
onClearAll={onClearAll}
onToggleFilter={(value) => toggle(setSelectedFilters, value)}
onClearAll={() => setSelectedFilters([])}
/>
);
};
36 changes: 35 additions & 1 deletion packages/demo/src/components/demo/radio-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,49 @@
import { RadioDropdown } from "@eqtylab/equality";
import { useState } from "react";

export const RadioDropdownDemo = () => {
const CATEGORIES = [
{ value: "access-control", label: "Access control", count: 12 },
{ value: "audit-logging", label: "Audit logging", count: 4 },
{ value: "data-retention", label: "Data retention", count: 9 },
{ value: "encryption", label: "Encryption", count: 3 },
{ value: "incident-response", label: "Incident response", count: 7 },
{ value: "model-evaluation", label: "Model evaluation", count: 15 },
{ value: "privacy", label: "Privacy", count: 6 },
{ value: "third-party-risk", label: "Third party risk", count: 2 },
{ value: "training-data", label: "Training data", count: 11 },
{ value: "transparency", label: "Transparency", count: 8 },
];

export const RadioDropdownDemo = ({
variant = "default",
}: {
variant?: "default" | "searchable";
}) => {
const [selectedStatus, setSelectedStatus] = useState<"active" | "archived">(
"active",
);
const [category, setCategory] = useState("model-evaluation");

const handleStatusChange = (value: "active" | "archived") => {
setSelectedStatus(value);
};

if (variant === "searchable") {
return (
<div className="mb-4">
<RadioDropdown
label="Category"
options={CATEGORIES}
selectedValue={category}
onSelect={setCategory}
searchable
searchPlaceholder="Search categories..."
emptyPlaceholder="No categories match"
/>
</div>
);
}

return (
<RadioDropdown
label="Status"
Expand Down
2 changes: 1 addition & 1 deletion packages/demo/src/content/components/avatar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ The `shape` prop controls the border radius of the avatar.

### Square

<AvatarShapeDemo client:only="react" shape="square" />
<AvatarSizesDemo client:only="react" shape="square" />

```tsx
<Avatar shape="circle">...</Avatar>
Expand Down
Loading
Loading