Skip to content

Commit d146651

Browse files
committed
feat: add shadcnui popover, deprecate MUI popover
1 parent 202f7f7 commit d146651

File tree

36 files changed

+663
-300
lines changed

36 files changed

+663
-300
lines changed

site/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@mui/x-tree-view": "7.18.0",
5353
"@radix-ui/react-dialog": "1.1.2",
5454
"@radix-ui/react-label": "2.1.0",
55+
"@radix-ui/react-popover": "1.1.3",
5556
"@radix-ui/react-slider": "1.2.1",
5657
"@radix-ui/react-slot": "1.1.0",
5758
"@radix-ui/react-switch": "1.1.1",

site/pnpm-lock.yaml

+267
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/components/FeatureStageBadge/FeatureStageBadge.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Interpolation, Theme } from "@emotion/react";
22
import Link from "@mui/material/Link";
33
import { visuallyHidden } from "@mui/utils";
44
import { HelpTooltipContent } from "components/HelpTooltip/HelpTooltip";
5-
import { Popover, PopoverTrigger } from "components/Popover/Popover";
5+
import { Popover, PopoverTrigger } from "components/deprecated/Popover/Popover";
66
import type { FC, HTMLAttributes, ReactNode } from "react";
77
import { docs } from "utils/docs";
88

site/src/components/HelpTooltip/HelpTooltip.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import {
88
import HelpIcon from "@mui/icons-material/HelpOutline";
99
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
1010
import Link from "@mui/material/Link";
11+
import { Stack } from "components/Stack/Stack";
1112
import {
1213
Popover,
1314
PopoverContent,
1415
type PopoverContentProps,
1516
type PopoverProps,
1617
PopoverTrigger,
1718
usePopover,
18-
} from "components/Popover/Popover";
19-
import { Stack } from "components/Stack/Stack";
19+
} from "components/deprecated/Popover/Popover";
2020
import {
2121
type FC,
2222
type HTMLAttributes,

site/src/components/IconField/IconField.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import { visuallyHidden } from "@mui/utils";
66
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
77
import { ExternalImage } from "components/ExternalImage/ExternalImage";
88
import { Loader } from "components/Loader/Loader";
9+
import { Stack } from "components/Stack/Stack";
910
import {
1011
Popover,
1112
PopoverContent,
1213
PopoverTrigger,
13-
} from "components/Popover/Popover";
14-
import { Stack } from "components/Stack/Stack";
14+
} from "components/deprecated/Popover/Popover";
1515
import { type FC, Suspense, lazy, useState } from "react";
1616

1717
// See: https://github.com/missive/emoji-mart/issues/51#issuecomment-287353222

site/src/components/Popover/Popover.stories.tsx

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import Button from "@mui/material/Button";
21
import type { Meta, StoryObj } from "@storybook/react";
32
import { expect, screen, userEvent, waitFor, within } from "@storybook/test";
3+
import { Button } from "components/Button/Button";
44
import { Popover, PopoverContent, PopoverTrigger } from "./Popover";
55

66
const meta: Meta<typeof Popover> = {
@@ -17,15 +17,15 @@ Its wings are too small to get its fat little body off the ground. The bee, of c
1717
flies anyway because bees don't care what humans think is impossible.
1818
`;
1919

20-
export const Example: Story = {
20+
export const Default: Story = {
2121
args: {
2222
children: (
23-
<>
24-
<PopoverTrigger>
25-
<Button>Click here!</Button>
23+
<Popover>
24+
<PopoverTrigger asChild>
25+
<Button className="ml-20">Click here!</Button>
2626
</PopoverTrigger>
2727
<PopoverContent>{content}</PopoverContent>
28-
</>
28+
</Popover>
2929
),
3030
},
3131
play: async ({ canvasElement, step }) => {
@@ -42,16 +42,16 @@ export const Example: Story = {
4242
},
4343
};
4444

45-
export const Horizontal: Story = {
45+
export const AlignStart: Story = {
4646
args: {
4747
children: (
48-
<>
49-
<PopoverTrigger>
50-
<Button>Click here!</Button>
48+
<Popover>
49+
<PopoverTrigger asChild>
50+
<Button className="ml-20">Click here!</Button>
5151
</PopoverTrigger>
52-
<PopoverContent horizontal="right">{content}</PopoverContent>
53-
</>
52+
<PopoverContent align="start">{content}</PopoverContent>
53+
</Popover>
5454
),
5555
},
56-
play: Example.play,
56+
play: Default.play,
5757
};
+32-237
Original file line numberDiff line numberDiff line change
@@ -1,240 +1,35 @@
1-
import MuiPopover, {
2-
type PopoverProps as MuiPopoverProps,
3-
// biome-ignore lint/nursery/noRestrictedImports: This is the base component that our custom popover is based on
4-
} from "@mui/material/Popover";
1+
import * as PopoverPrimitive from "@radix-ui/react-popover";
52
import {
6-
type FC,
7-
type HTMLAttributes,
8-
type PointerEvent,
9-
type PointerEventHandler,
10-
type ReactElement,
11-
type ReactNode,
12-
type RefObject,
13-
cloneElement,
14-
createContext,
15-
useContext,
16-
useEffect,
17-
useId,
18-
useRef,
19-
useState,
3+
type ComponentPropsWithoutRef,
4+
type ElementRef,
5+
forwardRef,
206
} from "react";
21-
22-
type TriggerMode = "hover" | "click";
23-
24-
type TriggerRef = RefObject<HTMLElement>;
25-
26-
// Have to append ReactNode type to satisfy React's cloneElement function. It
27-
// has absolutely no bearing on what happens at runtime
28-
type TriggerElement = ReactNode &
29-
ReactElement<{
30-
ref: TriggerRef;
31-
onClick?: () => void;
32-
}>;
33-
34-
type PopoverContextValue = {
35-
id: string;
36-
open: boolean;
37-
setOpen: (open: boolean) => void;
38-
triggerRef: TriggerRef;
39-
mode: TriggerMode;
40-
};
41-
42-
const PopoverContext = createContext<PopoverContextValue | undefined>(
43-
undefined,
44-
);
45-
46-
type BasePopoverProps = {
47-
children: ReactNode;
48-
mode?: TriggerMode;
49-
};
50-
51-
// By separating controlled and uncontrolled props, we achieve more accurate
52-
// type inference.
53-
type UncontrolledPopoverProps = BasePopoverProps & {
54-
open?: undefined;
55-
onOpenChange?: undefined;
56-
};
57-
58-
type ControlledPopoverProps = BasePopoverProps & {
59-
open: boolean;
60-
onOpenChange: (open: boolean) => void;
61-
};
62-
63-
export type PopoverProps = UncontrolledPopoverProps | ControlledPopoverProps;
64-
65-
export const Popover: FC<PopoverProps> = (props) => {
66-
const hookId = useId();
67-
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
68-
const triggerRef: TriggerRef = useRef(null);
69-
70-
// Helps makes sure that popovers close properly when the user switches to
71-
// a different tab. This won't help with controlled instances of the
72-
// component, but this is basically the most we can do from here
73-
useEffect(() => {
74-
const closeOnTabSwitch = () => setUncontrolledOpen(false);
75-
window.addEventListener("blur", closeOnTabSwitch);
76-
return () => window.removeEventListener("blur", closeOnTabSwitch);
77-
}, []);
78-
79-
const value: PopoverContextValue = {
80-
triggerRef,
81-
id: `${hookId}-popover`,
82-
mode: props.mode ?? "click",
83-
open: props.open ?? uncontrolledOpen,
84-
setOpen: props.onOpenChange ?? setUncontrolledOpen,
85-
};
86-
87-
return (
88-
<PopoverContext.Provider value={value}>
89-
{props.children}
90-
</PopoverContext.Provider>
91-
);
92-
};
93-
94-
export const usePopover = () => {
95-
const context = useContext(PopoverContext);
96-
if (!context) {
97-
throw new Error(
98-
"Popover compound components cannot be rendered outside the Popover component",
99-
);
100-
}
101-
return context;
102-
};
103-
104-
type PopoverTriggerRenderProps = Readonly<{
105-
isOpen: boolean;
106-
}>;
107-
108-
type PopoverTriggerProps = Readonly<
109-
Omit<HTMLAttributes<HTMLElement>, "children"> & {
110-
children:
111-
| TriggerElement
112-
| ((props: PopoverTriggerRenderProps) => TriggerElement);
113-
}
114-
>;
115-
116-
export const PopoverTrigger: FC<PopoverTriggerProps> = (props) => {
117-
const popover = usePopover();
118-
const { children, onClick, onPointerEnter, onPointerLeave, ...elementProps } =
119-
props;
120-
121-
const clickProps = {
122-
onClick: (event: PointerEvent<HTMLElement>) => {
123-
popover.setOpen(true);
124-
onClick?.(event);
125-
},
126-
};
127-
128-
const hoverProps = {
129-
onPointerEnter: (event: PointerEvent<HTMLElement>) => {
130-
popover.setOpen(true);
131-
onPointerEnter?.(event);
132-
},
133-
onPointerLeave: (event: PointerEvent<HTMLElement>) => {
134-
popover.setOpen(false);
135-
onPointerLeave?.(event);
136-
},
137-
};
138-
139-
const evaluatedChildren =
140-
typeof children === "function"
141-
? children({ isOpen: popover.open })
142-
: children;
143-
144-
return cloneElement(evaluatedChildren, {
145-
...elementProps,
146-
...(popover.mode === "click" ? clickProps : hoverProps),
147-
"aria-haspopup": true,
148-
"aria-owns": popover.id,
149-
"aria-expanded": popover.open,
150-
ref: popover.triggerRef,
151-
});
152-
};
153-
154-
type Horizontal = "left" | "right";
155-
156-
export type PopoverContentProps = Omit<
157-
MuiPopoverProps,
158-
"open" | "onClose" | "anchorEl"
159-
> & {
160-
horizontal?: Horizontal;
161-
};
162-
163-
export const PopoverContent: FC<PopoverContentProps> = ({
164-
horizontal = "left",
165-
onPointerEnter,
166-
onPointerLeave,
167-
...popoverProps
168-
}) => {
169-
const popover = usePopover();
170-
const hoverMode = popover.mode === "hover";
171-
172-
return (
173-
<MuiPopover
174-
disablePortal
175-
css={{
176-
// When it is on hover mode, and the mode is moving from the trigger to
177-
// the popover, if there is any space, the popover will be closed. I
178-
// found this is a limitation on how MUI structured the component. It is
179-
// not a big issue for now but we can re-evaluate it in the future.
180-
marginTop: hoverMode ? undefined : 8,
181-
pointerEvents: hoverMode ? "none" : undefined,
182-
"& .MuiPaper-root": {
183-
minWidth: 320,
184-
fontSize: 14,
185-
pointerEvents: hoverMode ? "auto" : undefined,
186-
},
187-
}}
188-
{...horizontalProps(horizontal)}
189-
{...modeProps(popover, onPointerEnter, onPointerLeave)}
190-
{...popoverProps}
191-
id={popover.id}
192-
open={popover.open}
193-
onClose={() => popover.setOpen(false)}
194-
anchorEl={popover.triggerRef.current}
7+
import { cn } from "utils/cn";
8+
9+
export const Popover = PopoverPrimitive.Root;
10+
11+
export const PopoverTrigger = PopoverPrimitive.Trigger;
12+
13+
export const PopoverContent = forwardRef<
14+
ElementRef<typeof PopoverPrimitive.Content>,
15+
ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
16+
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
17+
<PopoverPrimitive.Portal>
18+
<PopoverPrimitive.Content
19+
ref={ref}
20+
align={align}
21+
sideOffset={sideOffset}
22+
className={cn(
23+
`z-50 w-72 rounded-md border border-solid bg-surface-primary p-4
24+
text-content-primary shadow-md outline-none
25+
data-[state=open]:animate-in data-[state=closed]:animate-out
26+
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
27+
data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
28+
data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2
29+
data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2`,
30+
className,
31+
)}
32+
{...props}
19533
/>
196-
);
197-
};
198-
199-
const modeProps = (
200-
popover: PopoverContextValue,
201-
externalOnPointerEnter: PointerEventHandler<HTMLDivElement> | undefined,
202-
externalOnPointerLeave: PointerEventHandler<HTMLDivElement> | undefined,
203-
) => {
204-
if (popover.mode === "hover") {
205-
return {
206-
onPointerEnter: (event: PointerEvent<HTMLDivElement>) => {
207-
popover.setOpen(true);
208-
externalOnPointerEnter?.(event);
209-
},
210-
onPointerLeave: (event: PointerEvent<HTMLDivElement>) => {
211-
popover.setOpen(false);
212-
externalOnPointerLeave?.(event);
213-
},
214-
};
215-
}
216-
217-
return {};
218-
};
219-
220-
const horizontalProps = (horizontal: Horizontal) => {
221-
if (horizontal === "right") {
222-
return {
223-
anchorOrigin: {
224-
vertical: "bottom",
225-
horizontal: "right",
226-
},
227-
transformOrigin: {
228-
vertical: "top",
229-
horizontal: "right",
230-
},
231-
} as const;
232-
}
233-
234-
return {
235-
anchorOrigin: {
236-
vertical: "bottom",
237-
horizontal: "left",
238-
},
239-
} as const;
240-
};
34+
</PopoverPrimitive.Portal>
35+
));

site/src/components/SelectMenu/SelectMenu.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import Button, { type ButtonProps } from "@mui/material/Button";
33
import MenuItem, { type MenuItemProps } from "@mui/material/MenuItem";
44
import MenuList, { type MenuListProps } from "@mui/material/MenuList";
55
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
6-
import {
7-
Popover,
8-
PopoverContent,
9-
PopoverTrigger,
10-
} from "components/Popover/Popover";
116
import {
127
SearchField,
138
type SearchFieldProps,
149
} from "components/SearchField/SearchField";
10+
import {
11+
Popover,
12+
PopoverContent,
13+
PopoverTrigger,
14+
} from "components/deprecated/Popover/Popover";
1515
import {
1616
Children,
1717
type FC,

0 commit comments

Comments
 (0)