Skip to content

Commit bb37246

Browse files
committed
Change Dropdowns to use Radix's DropdownMenu
This allows for keyboard navigation, deliberate hover- or click-only triggering, and multi-level dropdowns. It also fixes visual overlapping and positioning bugs like the one demonstrated in #3164
1 parent 3119066 commit bb37246

File tree

8 files changed

+341
-217
lines changed

8 files changed

+341
-217
lines changed

bun.lock

+112-1
Large diffs are not rendered by default.

packages/gitbook/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@gitbook/react-math": "workspace:*",
2828
"@gitbook/react-openapi": "workspace:*",
2929
"@radix-ui/react-checkbox": "^1.0.4",
30+
"@radix-ui/react-dropdown-menu": "^2.1.12",
3031
"@radix-ui/react-navigation-menu": "^1.2.3",
3132
"@radix-ui/react-popover": "^1.0.7",
3233
"@radix-ui/react-tooltip": "^1.1.8",
@@ -37,6 +38,7 @@
3738
"assert-never": "^1.2.1",
3839
"bun-types": "^1.1.20",
3940
"classnames": "^2.5.1",
41+
"event-iterator": "^2.0.0",
4042
"framer-motion": "^10.16.14",
4143
"js-cookie": "^3.0.5",
4244
"jsontoxml": "^1.0.1",
@@ -52,6 +54,7 @@
5254
"openapi-types": "^12.1.3",
5355
"p-map": "^7.0.0",
5456
"parse-cache-control": "^1.0.1",
57+
"partial-json": "^0.1.7",
5558
"react": "^19.0.0",
5659
"react-dom": "^19.0.0",
5760
"react-hotkeys-hook": "^4.4.1",
@@ -70,8 +73,6 @@
7073
"usehooks-ts": "^3.1.0",
7174
"zod": "^3.24.2",
7275
"zod-to-json-schema": "^3.24.5",
73-
"event-iterator": "^2.0.0",
74-
"partial-json": "^0.1.7",
7576
"zustand": "^5.0.3"
7677
},
7778
"devDependencies": {

packages/gitbook/src/components/Header/Dropdown.tsx

-143
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
'use client';
2+
3+
import { Icon } from '@gitbook/icons';
4+
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
5+
import { useState } from 'react';
6+
7+
import { type ClassValue, tcls } from '@/lib/tailwind';
8+
9+
import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
10+
11+
import { Link, type LinkInsightsProps } from '../primitives';
12+
13+
export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit<
14+
Partial<DetailedHTMLProps<HTMLAttributes<E>, E>>,
15+
'ref'
16+
>;
17+
18+
/**
19+
* Button with a dropdown.
20+
*/
21+
export function DropdownMenu(props: {
22+
/** Content of the button */
23+
button: React.ReactNode;
24+
/** Content of the dropdown */
25+
children: React.ReactNode;
26+
/** Custom styles */
27+
className?: ClassValue;
28+
/** Open the dropdown on hover */
29+
openOnHover?: boolean;
30+
}) {
31+
const { button, children, className, openOnHover = false } = props;
32+
const [hovered, setHovered] = useState(false);
33+
const [clicked, setClicked] = useState(false);
34+
35+
return (
36+
<RadixDropdownMenu.Root
37+
modal={false}
38+
open={openOnHover ? clicked || hovered : clicked}
39+
onOpenChange={setClicked}
40+
>
41+
<RadixDropdownMenu.Trigger
42+
asChild
43+
onMouseEnter={() => setHovered(true)}
44+
onMouseLeave={() => setHovered(false)}
45+
onClick={() => (openOnHover ? setClicked(!clicked) : null)}
46+
className="group/dropdown"
47+
>
48+
{button}
49+
</RadixDropdownMenu.Trigger>
50+
51+
<RadixDropdownMenu.Portal>
52+
<RadixDropdownMenu.Content
53+
collisionPadding={8}
54+
onMouseEnter={() => setHovered(true)}
55+
onMouseLeave={() => setHovered(false)}
56+
align="start"
57+
className="z-40 animate-present pt-2"
58+
>
59+
<div className="max-h-80 flex flex-col gap-1 min-w-40 sm:min-w-52 max-w-[40vw] sm:max-w-80 overflow-auto rounded-lg straight-corners:rounded-sm bg-tint-base p-2 shadow-lg ring-1 ring-tint-subtle">
60+
{children}
61+
</div>
62+
</RadixDropdownMenu.Content>
63+
</RadixDropdownMenu.Portal>
64+
</RadixDropdownMenu.Root>
65+
);
66+
}
67+
68+
/**
69+
* Animated chevron to display in the dropdown button.
70+
*/
71+
export function DropdownChevron() {
72+
return (
73+
<Icon
74+
icon="chevron-down"
75+
className={tcls(
76+
'shrink-0',
77+
'opacity-6',
78+
'size-3',
79+
'ms-1',
80+
'transition-all',
81+
'group-hover/dropdown:opacity-11',
82+
'group-data-[state=open]/dropdown:opacity-11',
83+
'group-data-[state=open]/dropdown:rotate-180'
84+
)}
85+
/>
86+
);
87+
}
88+
89+
/**
90+
* Button with a chevron for use in dropdowns.
91+
*/
92+
export function DropdownButton(props: {
93+
children: React.ReactNode;
94+
className?: ClassValue;
95+
}) {
96+
const { children, className } = props;
97+
98+
return (
99+
<div className={tcls('group/dropdown', 'flex', 'items-center', className)}>
100+
{children}
101+
<DropdownChevron />
102+
</div>
103+
);
104+
}
105+
106+
/**
107+
* Menu item in a dropdown.
108+
*/
109+
export function DropdownMenuItem(
110+
props: {
111+
href: string | null;
112+
active?: boolean;
113+
className?: ClassValue;
114+
children: React.ReactNode;
115+
} & LinkInsightsProps
116+
) {
117+
const { children, active = false, href, className, insights } = props;
118+
119+
const itemClassName = tcls(
120+
'rounded straight-corners:rounded-sm px-3 py-1 text-sm',
121+
active
122+
? 'bg-primary text-primary-strong data-[highlighted]:bg-primary-hover'
123+
: 'data-[highlighted]:bg-tint-hover',
124+
'focus:outline-none',
125+
className
126+
);
127+
128+
if (href) {
129+
return (
130+
<RadixDropdownMenu.Item asChild>
131+
<Link href={href} insights={insights} className={itemClassName}>
132+
{children}
133+
</Link>
134+
</RadixDropdownMenu.Item>
135+
);
136+
}
137+
138+
return (
139+
<RadixDropdownMenu.Item
140+
className={tcls('px-3 py-1 font-medium text-tint text-xs', className)}
141+
>
142+
{children}
143+
</RadixDropdownMenu.Item>
144+
);
145+
}
146+
147+
export function DropdownSubMenu(props: { children: React.ReactNode; label: React.ReactNode }) {
148+
const { children, label } = props;
149+
150+
return (
151+
<RadixDropdownMenu.Sub>
152+
<RadixDropdownMenu.SubTrigger className="flex cursor-pointer items-center justify-between rounded straight-corners:rounded-sm px-3 py-1 text-sm focus:outline-none data-[highlighted]:bg-tint-hover">
153+
{label}
154+
<Icon icon="chevron-right" className="size-3 shrink-0 opacity-6" />
155+
</RadixDropdownMenu.SubTrigger>
156+
<RadixDropdownMenu.Portal>
157+
<RadixDropdownMenu.SubContent collisionPadding={8} className="z-40 animate-present">
158+
<div className="flex max-h-80 min-w-40 sm:min-w-52 max-w-[40vw] sm:max-w-80 flex-col gap-1 overflow-auto rounded-lg straight-corners:rounded-sm bg-tint-base p-2 shadow-lg ring-1 ring-tint-subtle">
159+
{children}
160+
</div>
161+
</RadixDropdownMenu.SubContent>
162+
</RadixDropdownMenu.Portal>
163+
</RadixDropdownMenu.Sub>
164+
);
165+
}

0 commit comments

Comments
 (0)