chore(site): refactor popover to make it easier to extend (#13611)

This commit is contained in:
Bruno Quaresma
2024-06-21 11:15:37 -03:00
committed by GitHub
parent 714f2ef83c
commit cbdaa63b68
18 changed files with 216 additions and 235 deletions

View File

@ -160,7 +160,7 @@ export const HelpTooltipAction: FC<HelpTooltipActionProps> = ({
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
onClick(); onClick();
popover.setIsOpen(false); popover.setOpen(false);
}} }}
> >
<Icon css={styles.actionIcon} /> <Icon css={styles.actionIcon} />

View File

@ -3,7 +3,7 @@ import Button from "@mui/material/Button";
import InputAdornment from "@mui/material/InputAdornment"; import InputAdornment from "@mui/material/InputAdornment";
import TextField, { type TextFieldProps } from "@mui/material/TextField"; import TextField, { type TextFieldProps } from "@mui/material/TextField";
import { visuallyHidden } from "@mui/utils"; import { visuallyHidden } from "@mui/utils";
import { type FC, lazy, Suspense } from "react"; import { type FC, lazy, Suspense, useState } from "react";
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { Loader } from "components/Loader/Loader"; import { Loader } from "components/Loader/Loader";
@ -37,6 +37,7 @@ export const IconField: FC<IconFieldProps> = ({
const theme = useTheme(); const theme = useTheme();
const hasIcon = textFieldProps.value && textFieldProps.value !== ""; const hasIcon = textFieldProps.value && textFieldProps.value !== "";
const [open, setOpen] = useState(false);
return ( return (
<Stack spacing={1}> <Stack spacing={1}>
@ -86,31 +87,26 @@ export const IconField: FC<IconFieldProps> = ({
} }
`} `}
/> />
<Popover> <Popover open={open} onOpenChange={setOpen}>
{(popover) => ( <PopoverTrigger>
<> <Button fullWidth endIcon={<DropdownArrow />}>
<PopoverTrigger> Select emoji
<Button fullWidth endIcon={<DropdownArrow />}> </Button>
Select emoji </PopoverTrigger>
</Button> <PopoverContent
</PopoverTrigger> id="emoji"
<PopoverContent css={{ marginTop: 0, ".MuiPaper-root": { width: "auto" } }}
id="emoji" >
css={{ marginTop: 0, ".MuiPaper-root": { width: "auto" } }} <Suspense fallback={<Loader />}>
> <EmojiPicker
<Suspense fallback={<Loader />}> onEmojiSelect={(emoji) => {
<EmojiPicker const value = emoji.src ?? urlFromUnifiedCode(emoji.unified);
onEmojiSelect={(emoji) => { onPickEmoji(value);
const value = setOpen(false);
emoji.src ?? urlFromUnifiedCode(emoji.unified); }}
onPickEmoji(value); />
popover.setIsOpen(false); </Suspense>
}} </PopoverContent>
/>
</Suspense>
</PopoverContent>
</>
)}
</Popover> </Popover>
{/* {/*

View File

@ -12,7 +12,6 @@ import {
type ReactNode, type ReactNode,
type RefObject, type RefObject,
useContext, useContext,
useEffect,
useId, useId,
useRef, useRef,
useState, useState,
@ -25,14 +24,12 @@ type TriggerRef = RefObject<HTMLElement>;
type TriggerElement = ReactElement<{ type TriggerElement = ReactElement<{
ref: TriggerRef; ref: TriggerRef;
onClick?: () => void; onClick?: () => void;
"aria-haspopup"?: boolean;
"aria-owns"?: string | undefined;
}>; }>;
type PopoverContextValue = { type PopoverContextValue = {
id: string; id: string;
isOpen: boolean; open: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setOpen: (open: boolean) => void;
triggerRef: TriggerRef; triggerRef: TriggerRef;
mode: TriggerMode; mode: TriggerMode;
}; };
@ -41,32 +38,41 @@ const PopoverContext = createContext<PopoverContextValue | undefined>(
undefined, undefined,
); );
export interface PopoverProps { type BasePopoverProps = {
children: ReactNode | ((popover: PopoverContextValue) => ReactNode); // Allows inline usage children: ReactNode;
mode?: TriggerMode; mode?: TriggerMode;
isDefaultOpen?: boolean; };
}
export const Popover: FC<PopoverProps> = ({ // By separating controlled and uncontrolled props, we achieve more accurate
children, // type inference.
mode, type UncontrolledPopoverProps = BasePopoverProps & {
isDefaultOpen, open?: undefined;
}) => { onOpenChange?: undefined;
};
type ControlledPopoverProps = BasePopoverProps & {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export type PopoverProps = UncontrolledPopoverProps | ControlledPopoverProps;
export const Popover: FC<PopoverProps> = (props) => {
const hookId = useId(); const hookId = useId();
const [isOpen, setIsOpen] = useState(isDefaultOpen ?? false); const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
const triggerRef = useRef<HTMLElement>(null); const triggerRef: TriggerRef = useRef(null);
const value: PopoverContextValue = { const value: PopoverContextValue = {
isOpen,
setIsOpen,
triggerRef, triggerRef,
id: `${hookId}-popover`, id: `${hookId}-popover`,
mode: mode ?? "click", mode: props.mode ?? "click",
open: props.open ?? uncontrolledOpen,
setOpen: props.onOpenChange ?? setUncontrolledOpen,
}; };
return ( return (
<PopoverContext.Provider value={value}> <PopoverContext.Provider value={value}>
{typeof children === "function" ? children(value) : children} {props.children}
</PopoverContext.Provider> </PopoverContext.Provider>
); );
}; };
@ -82,23 +88,25 @@ export const usePopover = () => {
}; };
export const PopoverTrigger = ( export const PopoverTrigger = (
props: HTMLAttributes<HTMLElement> & { children: TriggerElement }, props: HTMLAttributes<HTMLElement> & {
children: TriggerElement;
},
) => { ) => {
const popover = usePopover(); const popover = usePopover();
const { children, ...elementProps } = props; const { children, ...elementProps } = props;
const clickProps = { const clickProps = {
onClick: () => { onClick: () => {
popover.setIsOpen((isOpen) => !isOpen); popover.setOpen(true);
}, },
}; };
const hoverProps = { const hoverProps = {
onPointerEnter: () => { onPointerEnter: () => {
popover.setIsOpen(true); popover.setOpen(true);
}, },
onPointerLeave: () => { onPointerLeave: () => {
popover.setIsOpen(false); popover.setOpen(false);
}, },
}; };
@ -106,7 +114,8 @@ export const PopoverTrigger = (
...elementProps, ...elementProps,
...(popover.mode === "click" ? clickProps : hoverProps), ...(popover.mode === "click" ? clickProps : hoverProps),
"aria-haspopup": true, "aria-haspopup": true,
"aria-owns": popover.isOpen ? popover.id : undefined, "aria-owns": popover.id,
"aria-expanded": popover.open,
ref: popover.triggerRef, ref: popover.triggerRef,
}); });
}; };
@ -125,22 +134,8 @@ export const PopoverContent: FC<PopoverContentProps> = ({
...popoverProps ...popoverProps
}) => { }) => {
const popover = usePopover(); const popover = usePopover();
const [isReady, setIsReady] = useState(false);
const hoverMode = popover.mode === "hover"; const hoverMode = popover.mode === "hover";
// This is a hack to make sure the popover is not rendered until the trigger
// is ready. This is a limitation on MUI that does not support defaultIsOpen
// on Popover but we need it to storybook the component.
useEffect(() => {
if (!isReady && popover.triggerRef.current !== null) {
setIsReady(true);
}
}, [isReady, popover.triggerRef]);
if (!popover.triggerRef.current) {
return null;
}
return ( return (
<MuiPopover <MuiPopover
disablePortal disablePortal
@ -161,8 +156,8 @@ export const PopoverContent: FC<PopoverContentProps> = ({
{...modeProps(popover)} {...modeProps(popover)}
{...popoverProps} {...popoverProps}
id={popover.id} id={popover.id}
open={popover.isOpen} open={popover.open}
onClose={() => popover.setIsOpen(false)} onClose={() => popover.setOpen(false)}
anchorEl={popover.triggerRef.current} anchorEl={popover.triggerRef.current}
/> />
); );
@ -172,10 +167,10 @@ const modeProps = (popover: PopoverContextValue) => {
if (popover.mode === "hover") { if (popover.mode === "hover") {
return { return {
onPointerEnter: () => { onPointerEnter: () => {
popover.setIsOpen(true); popover.setOpen(true);
}, },
onPointerLeave: () => { onPointerLeave: () => {
popover.setIsOpen(false); popover.setOpen(false);
}, },
}; };
} }

View File

@ -87,7 +87,7 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
}) => { }) => {
const popover = usePopover(); const popover = usePopover();
const onPopoverClose = () => popover.setIsOpen(false); const onPopoverClose = () => popover.setOpen(false);
return ( return (
<nav> <nav>

View File

@ -11,6 +11,7 @@ import {
MockUser, MockUser,
MockWorkspaceProxies, MockWorkspaceProxies,
} from "testHelpers/entities"; } from "testHelpers/entities";
import { withDesktopViewport } from "testHelpers/storybook";
import { ProxyMenu } from "./ProxyMenu"; import { ProxyMenu } from "./ProxyMenu";
const defaultProxyContextValue = { const defaultProxyContextValue = {
@ -36,11 +37,7 @@ const meta: Meta<typeof ProxyMenu> = {
<Story /> <Story />
</AuthProvider> </AuthProvider>
), ),
(Story) => ( withDesktopViewport,
<div css={{ width: 1200, height: 800 }}>
<Story />
</div>
),
], ],
parameters: { parameters: {
queries: [ queries: [

View File

@ -1,6 +1,6 @@
import { css, type Interpolation, type Theme, useTheme } from "@emotion/react"; import { css, type Interpolation, type Theme, useTheme } from "@emotion/react";
import Badge from "@mui/material/Badge"; import Badge from "@mui/material/Badge";
import type { FC } from "react"; import { useState, type FC } from "react";
import type * as TypesGen from "api/typesGenerated"; import type * as TypesGen from "api/typesGenerated";
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
import { import {
@ -26,48 +26,45 @@ export const UserDropdown: FC<UserDropdownProps> = ({
onSignOut, onSignOut,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const [open, setOpen] = useState(false);
return ( return (
<Popover> <Popover open={open} onOpenChange={setOpen}>
{(popover) => ( <PopoverTrigger>
<> <button css={styles.button} data-testid="user-dropdown-trigger">
<PopoverTrigger> <div css={styles.badgeContainer}>
<button css={styles.button} data-testid="user-dropdown-trigger"> <Badge overlap="circular">
<div css={styles.badgeContainer}> <UserAvatar
<Badge overlap="circular"> css={styles.avatar}
<UserAvatar username={user.username}
css={styles.avatar} avatarURL={user.avatar_url}
username={user.username} />
avatarURL={user.avatar_url} </Badge>
/> <DropdownArrow
</Badge> color={theme.experimental.l2.fill.solid}
<DropdownArrow close={open}
color={theme.experimental.l2.fill.solid}
close={popover.isOpen}
/>
</div>
</button>
</PopoverTrigger>
<PopoverContent
horizontal="right"
css={{
".MuiPaper-root": {
minWidth: "auto",
width: 260,
boxShadow: theme.shadows[6],
},
}}
>
<UserDropdownContent
user={user}
buildInfo={buildInfo}
supportLinks={supportLinks}
onSignOut={onSignOut}
/> />
</PopoverContent> </div>
</> </button>
)} </PopoverTrigger>
<PopoverContent
horizontal="right"
css={{
".MuiPaper-root": {
minWidth: "auto",
width: 260,
boxShadow: theme.shadows[6],
},
}}
>
<UserDropdownContent
user={user}
buildInfo={buildInfo}
supportLinks={supportLinks}
onSignOut={onSignOut}
/>
</PopoverContent>
</Popover> </Popover>
); );
}; };

View File

@ -43,7 +43,7 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
const popover = usePopover(); const popover = usePopover();
const onPopoverClose = () => { const onPopoverClose = () => {
popover.setIsOpen(false); popover.setOpen(false);
}; };
const renderMenuIcon = (icon: string): JSX.Element => { const renderMenuIcon = (icon: string): JSX.Element => {

View File

@ -1,5 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within } from "@storybook/test";
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
import { withDesktopViewport } from "testHelpers/storybook";
import { SSHButton } from "./SSHButton"; import { SSHButton } from "./SSHButton";
const meta: Meta<typeof SSHButton> = { const meta: Meta<typeof SSHButton> = {
@ -22,7 +24,12 @@ export const Opened: Story = {
args: { args: {
workspaceName: MockWorkspace.name, workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name, agentName: MockWorkspaceAgent.name,
isDefaultOpen: true,
sshPrefix: "coder.", sshPrefix: "coder.",
}, },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button");
await userEvent.click(button);
},
decorators: [withDesktopViewport],
}; };

View File

@ -20,20 +20,18 @@ import { docs } from "utils/docs";
export interface SSHButtonProps { export interface SSHButtonProps {
workspaceName: string; workspaceName: string;
agentName: string; agentName: string;
isDefaultOpen?: boolean;
sshPrefix?: string; sshPrefix?: string;
} }
export const SSHButton: FC<SSHButtonProps> = ({ export const SSHButton: FC<SSHButtonProps> = ({
workspaceName, workspaceName,
agentName, agentName,
isDefaultOpen = false,
sshPrefix, sshPrefix,
}) => { }) => {
const paper = useClassName(classNames.paper, []); const paper = useClassName(classNames.paper, []);
return ( return (
<Popover isDefaultOpen={isDefaultOpen}> <Popover>
<PopoverTrigger> <PopoverTrigger>
<Button <Button
size="small" size="small"

View File

@ -56,7 +56,7 @@ export const WorkspaceOutdatedTooltipContent: FC<TooltipProps> = ({
const popover = usePopover(); const popover = usePopover();
const { data: activeVersion } = useQuery({ const { data: activeVersion } = useQuery({
...templateVersion(latestVersionId), ...templateVersion(latestVersionId),
enabled: popover.isOpen, enabled: popover.open,
}); });
const theme = useTheme(); const theme = useTheme();

View File

@ -49,92 +49,89 @@ export const DateRange: FC<DateRangeProps> = ({ value, onChange }) => {
key: "selection", key: "selection",
}, },
]); ]);
const [open, setOpen] = useState(false);
return ( return (
<Popover> <Popover open={open} onOpenChange={setOpen}>
{(popover) => ( <PopoverTrigger>
<> <Button>
<PopoverTrigger> <span>{format(value.startDate, "MMM d, Y")}</span>
<Button> <ArrowRightAltOutlined
<span>{format(value.startDate, "MMM d, Y")}</span> css={{ width: 16, height: 16, marginLeft: 8, marginRight: 8 }}
<ArrowRightAltOutlined />
css={{ width: 16, height: 16, marginLeft: 8, marginRight: 8 }} <span>{format(value.endDate, "MMM d, Y")}</span>
/> </Button>
<span>{format(value.endDate, "MMM d, Y")}</span> </PopoverTrigger>
</Button> <PopoverContent>
</PopoverTrigger> <DateRangePicker
<PopoverContent> css={styles.wrapper}
<DateRangePicker onChange={(item) => {
css={styles.wrapper} const range = item.selection;
onChange={(item) => { setRanges([range]);
const range = item.selection;
setRanges([range]);
// When it is the first selection, we don't want to close the popover // When it is the first selection, we don't want to close the popover
// We have to do that ourselves because the library doesn't provide a way to do it // We have to do that ourselves because the library doesn't provide a way to do it
if (selectionStatusRef.current === "idle") { if (selectionStatusRef.current === "idle") {
selectionStatusRef.current = "selecting"; selectionStatusRef.current = "selecting";
return; return;
} }
selectionStatusRef.current = "idle"; selectionStatusRef.current = "idle";
const startDate = range.startDate as Date; const startDate = range.startDate as Date;
const endDate = range.endDate as Date; const endDate = range.endDate as Date;
const now = new Date(); const now = new Date();
onChange({ onChange({
startDate: startOfDay(startDate), startDate: startOfDay(startDate),
endDate: isToday(endDate) endDate: isToday(endDate)
? startOfHour(addHours(now, 1)) ? startOfHour(addHours(now, 1))
: startOfDay(addDays(endDate, 1)), : startOfDay(addDays(endDate, 1)),
}); });
popover.setIsOpen(false); setOpen(false);
}} }}
moveRangeOnFirstSelection={false} moveRangeOnFirstSelection={false}
months={2} months={2}
ranges={ranges} ranges={ranges}
maxDate={new Date()} maxDate={new Date()}
direction="horizontal" direction="horizontal"
staticRanges={createStaticRanges([ staticRanges={createStaticRanges([
{ {
label: "Today", label: "Today",
range: () => ({ range: () => ({
startDate: new Date(), startDate: new Date(),
endDate: new Date(), endDate: new Date(),
}), }),
}, },
{ {
label: "Yesterday", label: "Yesterday",
range: () => ({ range: () => ({
startDate: subDays(new Date(), 1), startDate: subDays(new Date(), 1),
endDate: subDays(new Date(), 1), endDate: subDays(new Date(), 1),
}), }),
}, },
{ {
label: "Last 7 days", label: "Last 7 days",
range: () => ({ range: () => ({
startDate: subDays(new Date(), 6), startDate: subDays(new Date(), 6),
endDate: new Date(), endDate: new Date(),
}), }),
}, },
{ {
label: "Last 14 days", label: "Last 14 days",
range: () => ({ range: () => ({
startDate: subDays(new Date(), 13), startDate: subDays(new Date(), 13),
endDate: new Date(), endDate: new Date(),
}), }),
}, },
{ {
label: "Last 30 days", label: "Last 30 days",
range: () => ({ range: () => ({
startDate: subDays(new Date(), 29), startDate: subDays(new Date(), 29),
endDate: new Date(), endDate: new Date(),
}), }),
}, },
])} ])}
/> />
</PopoverContent> </PopoverContent>
</>
)}
</Popover> </Popover>
); );
}; };

View File

@ -1,43 +1,43 @@
import type { Meta, StoryObj } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within } from "@storybook/test";
import { import {
MockOwnerRole, MockOwnerRole,
MockSiteRoles, MockSiteRoles,
MockUserAdminRole, MockUserAdminRole,
} from "testHelpers/entities"; } from "testHelpers/entities";
import { withDesktopViewport } from "testHelpers/storybook";
import { EditRolesButton } from "./EditRolesButton"; import { EditRolesButton } from "./EditRolesButton";
const meta: Meta<typeof EditRolesButton> = { const meta: Meta<typeof EditRolesButton> = {
title: "pages/UsersPage/EditRolesButton", title: "pages/UsersPage/EditRolesButton",
component: EditRolesButton, component: EditRolesButton,
args: { args: {
isDefaultOpen: true, selectedRoleNames: new Set([MockUserAdminRole.name, MockOwnerRole.name]),
roles: MockSiteRoles,
}, },
decorators: [withDesktopViewport],
}; };
export default meta; export default meta;
type Story = StoryObj<typeof EditRolesButton>; type Story = StoryObj<typeof EditRolesButton>;
const selectedRoleNames = new Set([MockUserAdminRole.name, MockOwnerRole.name]); export const Closed: Story = {};
export const Open: Story = { export const Open: Story = {
args: { play: async ({ canvasElement }) => {
selectedRoleNames, const canvas = within(canvasElement);
roles: MockSiteRoles, await userEvent.click(canvas.getByRole("button"));
},
parameters: {
chromatic: { delay: 300 },
}, },
}; };
export const Loading: Story = { export const Loading: Story = {
args: { args: {
isLoading: true, isLoading: true,
selectedRoleNames,
roles: MockSiteRoles,
userLoginType: "password", userLoginType: "password",
oidcRoleSync: false, oidcRoleSync: false,
}, },
parameters: { play: async ({ canvasElement }) => {
chromatic: { delay: 300 }, const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button"));
}, },
}; };

View File

@ -72,7 +72,6 @@ export interface EditRolesButtonProps {
roles: readonly SlimRole[]; roles: readonly SlimRole[];
selectedRoleNames: Set<string>; selectedRoleNames: Set<string>;
onChange: (roles: SlimRole["name"][]) => void; onChange: (roles: SlimRole["name"][]) => void;
isDefaultOpen?: boolean;
oidcRoleSync: boolean; oidcRoleSync: boolean;
userLoginType: string; userLoginType: string;
} }
@ -82,7 +81,6 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
selectedRoleNames, selectedRoleNames,
onChange, onChange,
isLoading, isLoading,
isDefaultOpen = false,
userLoginType, userLoginType,
oidcRoleSync, oidcRoleSync,
}) => { }) => {
@ -116,7 +114,7 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
} }
return ( return (
<Popover isDefaultOpen={isDefaultOpen}> <Popover>
<PopoverTrigger> <PopoverTrigger>
<IconButton <IconButton
size="small" size="small"

View File

@ -117,7 +117,7 @@ const BuildParametersPopoverContent: FC<BuildParametersPopoverContentProps> = ({
<Form <Form
onSubmit={(buildParameters) => { onSubmit={(buildParameters) => {
onSubmit(buildParameters); onSubmit(buildParameters);
popover.setIsOpen(false); popover.setOpen(false);
}} }}
ephemeralParameters={ephemeralParameters} ephemeralParameters={ephemeralParameters}
buildParameters={buildParameters.map( buildParameters={buildParameters.map(

View File

@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent, fn, waitFor, within } from "@storybook/test"; import { expect, userEvent, fn, waitFor, within } from "@storybook/test";
import { agentLogsKey, buildLogsKey } from "api/queries/workspaces"; import { agentLogsKey, buildLogsKey } from "api/queries/workspaces";
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
import { withDesktopViewport } from "testHelpers/storybook";
import { DownloadLogsDialog } from "./DownloadLogsDialog"; import { DownloadLogsDialog } from "./DownloadLogsDialog";
const meta: Meta<typeof DownloadLogsDialog> = { const meta: Meta<typeof DownloadLogsDialog> = {
@ -24,13 +25,7 @@ const meta: Meta<typeof DownloadLogsDialog> = {
}, },
], ],
}, },
decorators: [ decorators: [withDesktopViewport],
(Story) => (
<div css={{ width: 1200, height: 800 }}>
<Story />
</div>
),
],
}; };
export default meta; export default meta;

View File

@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within, expect } from "@storybook/test"; import { userEvent, within, expect } from "@storybook/test";
import { buildLogsKey, agentLogsKey } from "api/queries/workspaces"; import { buildLogsKey, agentLogsKey } from "api/queries/workspaces";
import * as Mocks from "testHelpers/entities"; import * as Mocks from "testHelpers/entities";
import { withDesktopViewport } from "testHelpers/storybook";
import { WorkspaceActions } from "./WorkspaceActions"; import { WorkspaceActions } from "./WorkspaceActions";
const meta: Meta<typeof WorkspaceActions> = { const meta: Meta<typeof WorkspaceActions> = {
@ -10,13 +11,7 @@ const meta: Meta<typeof WorkspaceActions> = {
args: { args: {
isUpdating: false, isUpdating: false,
}, },
decorators: [ decorators: [withDesktopViewport],
(Story) => (
<div css={{ width: 1200, height: 800 }}>
<Story />
</div>
),
],
}; };
export default meta; export default meta;

View File

@ -70,7 +70,7 @@ const NotificationPill: FC<NotificationsProps> = ({
icon={icon} icon={icon}
css={(theme) => ({ css={(theme) => ({
"& svg": { color: theme.roles[severity].outline }, "& svg": { color: theme.roles[severity].outline },
borderColor: popover.isOpen ? theme.roles[severity].outline : undefined, borderColor: popover.open ? theme.roles[severity].outline : undefined,
})} })}
> >
{items.length} {items.length}

View File

@ -78,3 +78,9 @@ export const withWebSocket = (Story: FC, { parameters }: StoryContext) => {
return <Story />; return <Story />;
}; };
export const withDesktopViewport = (Story: FC) => (
<div style={{ width: 1200, height: 800 }}>
<Story />
</div>
);