|
2 | 2 |
|
3 | 3 | import { Icon } from '@gitbook/icons';
|
4 | 4 | import type { DetailedHTMLProps, HTMLAttributes } from 'react';
|
5 |
| -import { useState } from 'react'; |
| 5 | +import { createContext, useContext, useState } from 'react'; |
6 | 6 |
|
7 | 7 | import { type ClassValue, tcls } from '@/lib/tailwind';
|
8 | 8 |
|
9 | 9 | import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
|
10 | 10 |
|
| 11 | +import { assert } from 'ts-essentials'; |
11 | 12 | import { Link, type LinkInsightsProps } from '.';
|
12 | 13 |
|
13 | 14 | export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit<
|
14 | 15 | Partial<DetailedHTMLProps<HTMLAttributes<E>, E>>,
|
15 | 16 | 'ref'
|
16 | 17 | >;
|
17 | 18 |
|
| 19 | +const DropdownMenuContext = createContext<{ |
| 20 | + open: boolean; |
| 21 | + setOpen: (open: boolean) => void; |
| 22 | +}>({ |
| 23 | + open: false, |
| 24 | + setOpen: () => {}, |
| 25 | +}); |
| 26 | + |
18 | 27 | /**
|
19 | 28 | * Button with a dropdown.
|
20 | 29 | */
|
@@ -47,46 +56,46 @@ export function DropdownMenu(props: {
|
47 | 56 | align = 'start',
|
48 | 57 | } = props;
|
49 | 58 | const [hovered, setHovered] = useState(false);
|
50 |
| - const [clicked, setClicked] = useState(false); |
| 59 | + const [open, setOpen] = useState(false); |
51 | 60 |
|
52 |
| - return ( |
53 |
| - <RadixDropdownMenu.Root |
54 |
| - modal={false} |
55 |
| - open={openOnHover ? clicked || hovered : clicked} |
56 |
| - onOpenChange={setClicked} |
57 |
| - > |
58 |
| - <RadixDropdownMenu.Trigger |
59 |
| - asChild |
60 |
| - onMouseEnter={() => setHovered(true)} |
61 |
| - onMouseLeave={() => setHovered(false)} |
62 |
| - onClick={() => (openOnHover ? setClicked(!clicked) : null)} |
63 |
| - className="group/dropdown" |
64 |
| - > |
65 |
| - {button} |
66 |
| - </RadixDropdownMenu.Trigger> |
| 61 | + const isOpen = openOnHover ? open || hovered : open; |
67 | 62 |
|
68 |
| - <RadixDropdownMenu.Portal> |
69 |
| - <RadixDropdownMenu.Content |
70 |
| - data-testid="dropdown-menu" |
71 |
| - hideWhenDetached |
72 |
| - collisionPadding={8} |
| 63 | + return ( |
| 64 | + <DropdownMenuContext.Provider value={{ open: isOpen, setOpen }}> |
| 65 | + <RadixDropdownMenu.Root modal={false} open={isOpen} onOpenChange={setOpen}> |
| 66 | + <RadixDropdownMenu.Trigger |
| 67 | + asChild |
73 | 68 | onMouseEnter={() => setHovered(true)}
|
74 | 69 | onMouseLeave={() => setHovered(false)}
|
75 |
| - align={align} |
76 |
| - side={side} |
77 |
| - className="z-40 animate-scaleIn border-tint pt-2" |
| 70 | + onClick={() => (openOnHover ? setOpen(!open) : null)} |
| 71 | + className="group/dropdown" |
78 | 72 | >
|
79 |
| - <div |
80 |
| - className={tcls( |
81 |
| - 'flex max-h-80 min-w-40 max-w-[40vw] flex-col gap-1 overflow-auto circular-corners:rounded-xl rounded-md straight-corners:rounded-none border border-tint bg-tint-base p-2 shadow-lg sm:min-w-52 sm:max-w-80', |
82 |
| - className |
83 |
| - )} |
| 73 | + {button} |
| 74 | + </RadixDropdownMenu.Trigger> |
| 75 | + |
| 76 | + <RadixDropdownMenu.Portal> |
| 77 | + <RadixDropdownMenu.Content |
| 78 | + data-testid="dropdown-menu" |
| 79 | + hideWhenDetached |
| 80 | + collisionPadding={8} |
| 81 | + onMouseEnter={() => setHovered(true)} |
| 82 | + onMouseLeave={() => setHovered(false)} |
| 83 | + align={align} |
| 84 | + side={side} |
| 85 | + className="z-40 animate-scaleIn border-tint pt-2" |
84 | 86 | >
|
85 |
| - {children} |
86 |
| - </div> |
87 |
| - </RadixDropdownMenu.Content> |
88 |
| - </RadixDropdownMenu.Portal> |
89 |
| - </RadixDropdownMenu.Root> |
| 87 | + <div |
| 88 | + className={tcls( |
| 89 | + 'flex max-h-80 min-w-40 max-w-[40vw] flex-col gap-1 overflow-auto circular-corners:rounded-xl rounded-md straight-corners:rounded-none border border-tint bg-tint-base p-2 shadow-lg sm:min-w-52 sm:max-w-80', |
| 90 | + className |
| 91 | + )} |
| 92 | + > |
| 93 | + {children} |
| 94 | + </div> |
| 95 | + </RadixDropdownMenu.Content> |
| 96 | + </RadixDropdownMenu.Portal> |
| 97 | + </RadixDropdownMenu.Root> |
| 98 | + </DropdownMenuContext.Provider> |
90 | 99 | );
|
91 | 100 | }
|
92 | 101 |
|
@@ -193,3 +202,12 @@ export function DropdownSubMenu(props: { children: React.ReactNode; label: React
|
193 | 202 | </RadixDropdownMenu.Sub>
|
194 | 203 | );
|
195 | 204 | }
|
| 205 | + |
| 206 | +/** |
| 207 | + * Hook to close the dropdown menu. |
| 208 | + */ |
| 209 | +export function useDropdownMenuClose() { |
| 210 | + const context = useContext(DropdownMenuContext); |
| 211 | + assert(context, 'DropdownMenuContext not found'); |
| 212 | + return () => context.setOpen(false); |
| 213 | +} |
0 commit comments