/* eslint-disable testing-library/no-node-access */
import * as React from "react";
import { userEvent, within } from "@storybook/testing-library";
import { expect } from "@storybook/jest";
import * as Tokens from "@washingtonpost/wpds-theme/src/tokens";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { Popover } from "./";
import { styled, theme, globalCss } from "@washingtonpost/wpds-theme";
import { ChevronRight } from "@washingtonpost/wpds-assets";
import type { ComponentMeta, ComponentStory } from "@storybook/react";
export default {
title: "Popover",
component: Popover.Content,
subcomponents: {
Root: Popover.Root,
Portal: Popover.Portal,
Trigger: Popover.Trigger,
Anchor: Popover.Anchor,
},
} as ComponentMeta<typeof Popover.Content>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Template: ComponentStory<any> = (args) => {
const triggerRef = React.useRef(null);
const [parentElement, setParentElement] = React.useState();
React.useEffect(() => {
triggerRef?.current &&
setParentElement(triggerRef.current["parentElement"]);
}, [setParentElement]);
return (
<Popover.Root>
<Popover.Trigger ref={triggerRef}>Trigger</Popover.Trigger>
<Popover.Portal container={parentElement}>
<Popover.Content {...args}></Popover.Content>
</Popover.Portal>
</Popover.Root>
);
};
export const Default = Template.bind({});
Default.args = {
children:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Diam non eget consequat pretium.",
density: "default",
sideOffset: "050",
side: "bottom",
align: "center",
};
Default.argTypes = {
density: {
control: "select",
options: ["default", "compact"],
},
sideOffset: {
options: Object.keys(Tokens.sizes),
mapping: theme.sizes,
control: "select",
},
};
Default.parameters = {
chromatic: { disableSnapshot: true },
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const AnchorTemplate: ComponentStory<any> = (args) => {
const triggerRef = React.useRef(null);
const [parentElement, setParentElement] = React.useState();
React.useEffect(() => {
triggerRef?.current &&
setParentElement(triggerRef.current["parentElement"]);
}, [setParentElement]);
return (
<Popover.Root>
<Popover.Trigger ref={triggerRef}>Trigger</Popover.Trigger>
<Popover.Portal container={parentElement}>
<Popover.Content {...args}></Popover.Content>
</Popover.Portal>
<Popover.Anchor asChild>
<div
style={{
position: "absolute",
bottom: 0,
color: theme.colors.primary.value,
}}
>
Anchor
</div>
</Popover.Anchor>
</Popover.Root>
);
};
export const Anchor = AnchorTemplate.bind({});
Anchor.args = {
children:
"Anchors allow for Popups to be triggered and positioned independently",
};
Anchor.parameters = { chromatic: { disableSnapshot: true } };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CloseButtonTemplate: ComponentStory<any> = (args) => {
const triggerRef = React.useRef(null);
const [parentElement, setParentElement] = React.useState();
React.useEffect(() => {
triggerRef?.current &&
setParentElement(triggerRef.current["parentElement"]);
}, [setParentElement]);
return (
<Popover.Root>
<Popover.Trigger ref={triggerRef}>Trigger</Popover.Trigger>
<Popover.Portal container={parentElement}>
<Popover.Content
{...args}
onInteractOutside={(e) => {
e.preventDefault();
}}
>
<div style={{ marginInlineEnd: theme.sizes["175"].value }}>
{args.children}
</div>
<Popover.Close />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
};
export const CloseButton = CloseButtonTemplate.bind({});
CloseButton.args = {
children: "An optional close button may be included",
};
CloseButton.parameters = { chromatic: { disableSnapshot: true } };
const SideMenu = styled("menu", {
backgroundColor: theme.colors.gray40,
listStyle: "none",
marginBlock: 0,
paddingInlineStart: 0,
width: "250px",
minHeight: "100vh",
});
const MenuLink = styled("a", {
color: theme.colors.onPrimary,
display: "flex",
alignItems: "center",
fontSize: theme.fontSizes["087"],
fontWeight: theme.fontWeights.bold,
textDecoration: "none",
paddingBlock: theme.space["100"],
paddingInline: theme.space["125"],
"&:hover": {
backgroundColor: theme.colors.gray0,
},
"&:focus-visible": { outline: "none" },
"& :first-child": {
flex: 1,
},
});
const SubMenu = styled("menu", {
listStyle: "none",
marginBlock: 0,
paddingInlineStart: 0,
});
const SubMenuLink = styled("a", {
color: theme.colors.primary,
display: "block",
fontSize: theme.fontSizes["087"],
fontWeight: theme.fontWeights.bold,
paddingBlock: theme.space["100"],
paddingInline: theme.space["125"],
textDecoration: "none",
"&:hover": {
backgroundColor: theme.colors.gray500,
},
});
const Arrow = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="7"
fill="none"
viewBox="0 0 12 7"
>
<path fill={theme.colors.gray300.value} d="M0 1h12L6 7 0 1z"></path>
<path
fill={theme.colors.onPrimary.value}
d="M1 1h10L6 6 1 1zM0 0h12v1H0V0z"
></path>
</svg>
);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const MenuTemplate: ComponentStory<any> = () => {
const [menuOpen, setMenuOpen] = React.useState(false);
return (
<div style={{ width: "calc(100% + 4rem)", marginBlock: "-2rem" }}>
<SideMenu>
<li>
<MenuLink href="">Home Page</MenuLink>
</li>
<li
onMouseEnter={() => setMenuOpen(true)}
onMouseLeave={() => setMenuOpen(false)}
>
<Popover.Root
open={menuOpen}
onOpenChange={(open) => setMenuOpen(open)}
>
<Popover.Trigger asChild>
<MenuLink href="https://www.washingtonpost.com/politics/?itid=nb_front_politics">
<span>Politics</span>{" "}
<ChevronRight width={theme.sizes["125"].value} />
</MenuLink>
</Popover.Trigger>
<Popover.Content
side="right"
sideOffset={0}
css={{
"&:focus-visible": { outline: "none" },
"& span": { lineHeight: 0 },
}}
>
<SubMenu>
<li>
<SubMenuLink href="https://www.washingtonpost.com/elections/election-2024/">
Election 2024
</SubMenuLink>
</li>
<li>
<SubMenuLink href="https://www.washingtonpost.com/politics/courts-law/?itid=nb_politics_courts-and-law">
Courts and Law
</SubMenuLink>
</li>
<li>
<SubMenuLink href="https://www.washingtonpost.com/news/fact-checker/?itid=nb_politics_fact-checker">
Fact Checker
</SubMenuLink>
</li>
</SubMenu>
<PopoverPrimitive.Arrow asChild>
<Arrow />
</PopoverPrimitive.Arrow>
</Popover.Content>
</Popover.Root>
</li>
</SideMenu>
</div>
);
};
export const Menu = MenuTemplate.bind({});
Menu.parameters = { chromatic: { disableSnapshot: true } };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const FullPopover: ComponentStory<any> = ({ side, align }) => (
<Popover.Root open={true}>
<Popover.Trigger asChild>
<u
style={{
border: `1px solid ${theme.colors.gray300}`,
color: theme.colors.gray100.value,
paddingInline: theme.space["025"].value,
}}
tabIndex={0}
>
Trigger
</u>
</Popover.Trigger>
<Popover.Content
side={side}
align={align}
width={114}
css={{
animation: "unset",
}}
>
{side} {align}
</Popover.Content>
</Popover.Root>
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ChromaticTemplate: ComponentStory<any> = () => {
const side = ["bottom", "top", "right", "left"];
const align = ["start", "center", "end"];
return (
<div
style={{
display: "grid",
gridTemplateRows: "auto auto auto",
gridTemplateColumns: "auto auto",
gridTemplateAreas: "'bottom bottom' 'top top' 'right left'",
width: "100%",
}}
>
{side.map((s) => {
let direction = "row";
if (s === "right" || s === "left") {
direction = "column";
}
let alignItems = "flex-start";
if (s === "left") {
alignItems = "flex-end";
}
return (
<div
key={s}
style={
{
display: "flex",
flexDirection: direction,
gridArea: s,
justifyContent: "space-between",
alignItems: alignItems,
} as React.CSSProperties
}
>
{align.map((a) => {
let p = "0";
if (s === "bottom") {
p = "0 0 54px 0";
} else if (s === "top") {
p = "54px 0 0 0";
} else {
p = "20px 0 20px 0";
}
return (
<div key={s + a} style={{ padding: p }}>
<FullPopover side={s} align={a} />
</div>
);
})}
</div>
);
})}
</div>
);
};
export const Chromatic = ChromaticTemplate.bind({});
Chromatic.parameters = {
docs: { disable: true },
chromatic: { delay: 1000 },
};
const ecGlobalCss = globalCss({
":not(.wpds-dark) > .ec-grid": {
marginInlineStart: "-2rem",
},
".wpds-dark > .ec-grid": {
marginInlineEnd: "-2rem",
},
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const EdgeAndCornerCasesTemplate: ComponentStory<any> = () => {
ecGlobalCss();
return (
<div
className="ec-grid"
style={{
display: "grid",
gridTemplateRows: "auto 1fr auto",
gridTemplateColumns: "auto 1fr auto",
width: "calc(100% + 4rem)",
height: `100vh`,
marginBlockStart: "-2rem",
}}
>
<div>
<FullPopover side="left" align="end" />
</div>
<div style={{ display: "flex", justifyContent: "center" }}>
<FullPopover side="top" align="center" />
</div>
<div>
<FullPopover side="right" align="end" />
</div>
<div style={{ display: "flex", alignItems: "center" }}>
<FullPopover side="left" align="center" />
</div>
<div></div>
<div style={{ display: "flex", alignItems: "center" }}>
<FullPopover side="right" align="center" />
</div>
<div>
<FullPopover side="left" align="start" />
</div>
<div style={{ display: "flex", justifyContent: "center" }}>
<FullPopover side="bottom" align="center" />
</div>
<div>
<FullPopover side="right" align="start" />
</div>
</div>
);
};
export const EdgeAndCornerCases = EdgeAndCornerCasesTemplate.bind({});
EdgeAndCornerCases.parameters = {
docs: { disable: true },
chromatic: { delay: 3000 },
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const InteractionsTemplate: ComponentStory<any> = () => (
<Popover.Root>
<Popover.Trigger>Trigger</Popover.Trigger>
<Popover.Content>Popover Content</Popover.Content>
</Popover.Root>
);
export const Interactions = InteractionsTemplate.bind({});
Interactions.parameters = {
chromatic: { disableSnapshot: true },
};
// Function to emulate pausing between interactions
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Interactions.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const trigger = canvas.getByRole("button");
await userEvent.click(trigger);
await sleep(500);
const content = canvas.getByRole("dialog");
await expect(content).toBeVisible();
await userEvent.click(document.body);
await sleep(500);
await expect(content).not.toBeInTheDocument();
};
Source code of WPDS's UI Kit, by The Washington Post
Is powered by TypeScript Language Basics (builtin) for VSCode
Copyright © 2024 Ivan Uhalin