Skip to content
1 change: 0 additions & 1 deletion src/components/Button/button.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ $root: button;
height: var(--button-height, 34px);
border-radius: var(--button-border-radius, 10px);
padding: var(--button-padding, 0 16px);
will-change: background, color, transform;
transition:
color var(--button-speed-color, var(--speed-color)),
background var(--button-speed-bg, var(--speed-color)),
Expand Down
128 changes: 128 additions & 0 deletions src/components/Popover/Popover.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React from "react";
import {Meta, StoryObj} from "@storybook/react";

import {
Popover as PopoverComponent,
PopoverAnchor,
PopoverContent,
PopoverContentProps,
PopoverProps,
PopoverTrigger,
PopoverTriggerProps,
} from "./index";

import {TextArea, TextField} from "../index";

type Props = PopoverProps &
PopoverContentProps &
PopoverTriggerProps & {
withAnchor: boolean;
triggerWidth: number;
};

const meta: Meta<Props> = {
title: "Components/Popover",
component: PopoverComponent,
tags: ["autodocs"],
argTypes: {
// Root
modal: {table: {category: "Root"}},

// Trigger
center: {table: {category: "Trigger"}},

// Content
side: {
options: ["top", "right", "bottom", "left"],
control: {type: "select"},
table: {category: "Content"},
},
align: {
options: ["start", "center", "end"],
control: {type: "select"},
table: {category: "Content"},
},
sticky: {
options: ["partial", "always"],
control: {type: "select"},
table: {category: "Content"},
},
alignOffset: {table: {category: "Content"}},
sideOffset: {table: {category: "Content"}},
minWidth: {table: {category: "Content"}, control: {type: "number"}},
maxWidth: {table: {category: "Content"}, control: {type: "number"}},
fullWidth: {table: {category: "Content"}},
overlay: {table: {category: "Content"}},
avoidCollisions: {table: {category: "Content"}},
collisionPadding: {table: {category: "Content"}},
hideWhenDetached: {table: {category: "Content"}},
arrow: {table: {category: "Content"}},
arrowWidth: {table: {category: "Content"}},
arrowHeight: {table: {category: "Content"}},

// Storybook view
withAnchor: {table: {category: "Storybook_view"}},
triggerWidth: {table: {category: "Storybook_view"}, control: {type: "number"}},
},
};

export default meta;

export const Popover: StoryObj<Props> = {
args: {
modal: false,

center: false,

side: "bottom",
sideOffset: 10,
align: "center",
alignOffset: 0,
minWidth: undefined,
maxWidth: undefined,
fullWidth: false,
overlay: true,
avoidCollisions: true,
collisionPadding: 0,
sticky: "partial",
hideWhenDetached: false,
arrow: true,
arrowWidth: 10,
arrowHeight: 5,

withAnchor: false,
triggerWidth: undefined,
},
render: args => {
const {modal, center, withAnchor, triggerWidth, ...other} = args;

return (
<PopoverComponent modal={modal}>
<PopoverTrigger center={center} style={{width: triggerWidth}}>
<span>Open Popover</span>
{withAnchor && <PopoverAnchor>➕</PopoverAnchor>}
</PopoverTrigger>

<PopoverContent {...other}>
<div style={{display: "flex", flexDirection: "column", gap: 15}}>
{["Name", "Surname", "Age"].map(item => (
<div key={item} style={{display: "flex", alignItems: "center", gap: 5}}>
<label style={{fontSize: "16px", fontWeight: 600, flex: 1}} htmlFor={item}>
{item}
</label>
<div style={{flex: 1}}>
<TextField
id={item}
type={item === "Age" ? "number" : "text"}
placeholder={`Enter ${item}`}
/>
</div>
</div>
))}
<TextArea placeholder="Enter your message" />
</div>
</PopoverContent>
</PopoverComponent>
);
},
};
15 changes: 15 additions & 0 deletions src/components/Popover/Popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, {FC, memo} from "react";

import {PopoverProps, Root} from "@radix-ui/react-popover";

import {useComponentProps} from "../../providers";

export {type PopoverProps};

const Popover: FC<PopoverProps> = props => {
const mergedProps = {...useComponentProps("popover"), ...props};

return <Root {...mergedProps} />;
};

export default memo(Popover);
23 changes: 23 additions & 0 deletions src/components/Popover/PopoverAnchor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, {forwardRef, ForwardRefRenderFunction, memo} from "react";

import classnames from "classnames";

import {Anchor, PopoverAnchorProps} from "@radix-ui/react-popover";

import {useComponentProps} from "../../providers";

import styles from "./popover.module.scss";

export type {PopoverAnchorProps};

const PopoverAnchor: ForwardRefRenderFunction<HTMLDivElement, PopoverAnchorProps> = (props, ref) => {
const {className, children, ...other} = {...useComponentProps("popoverAnchor"), ...props};

return (
<Anchor ref={ref} className={classnames(styles["popover__anchor"], className)} {...other}>
{children}
</Anchor>
);
};

export default memo(forwardRef(PopoverAnchor));
23 changes: 23 additions & 0 deletions src/components/Popover/PopoverClose.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, {forwardRef, ForwardRefRenderFunction, memo} from "react";

import classnames from "classnames";

import {Close, PopoverCloseProps} from "@radix-ui/react-popover";

import {useComponentProps} from "../../providers";

import styles from "./popover.module.scss";

export type {PopoverCloseProps};

const PopoverClose: ForwardRefRenderFunction<HTMLButtonElement, PopoverCloseProps> = (props, ref) => {
const {className, children, ...other} = {...useComponentProps("popoverClose"), ...props};

return (
<Close ref={ref} className={classnames(styles["popover__close"], className)} {...other}>
{children}
</Close>
);
};

export default memo(forwardRef(PopoverClose));
73 changes: 73 additions & 0 deletions src/components/Popover/PopoverContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, {forwardRef, ForwardRefRenderFunction, memo} from "react";

import classnames from "classnames";

import {
Arrow,
Content,
PopoverContentProps as PopoverContentRadixProps,
PopoverPortalProps,
Portal,
} from "@radix-ui/react-popover";

import {useComponentProps} from "../../providers";

import styles from "./popover.module.scss";

export interface PopoverContentProps extends PopoverContentRadixProps, PopoverPortalProps {
maxWidth?: number;
minWidth?: number;
fullWidth?: boolean;
arrow?: boolean;
arrowWidth?: number;
arrowHeight?: number;
overlay?: boolean;
overlayClassname?: string;
}

const PopoverContent: ForwardRefRenderFunction<HTMLDivElement, PopoverContentProps> = (props, ref) => {
const {
maxWidth,
minWidth,
arrow,
arrowWidth,
arrowHeight,
fullWidth,
overlay,
overlayClassname,
container,
className,
style,
children,
...other
} = {...useComponentProps("popoverContent"), ...props};

return (
<Portal container={container}>
<>
{overlay && <div className={classnames(styles["popover__overlay"], overlayClassname)} />}
<Content
ref={ref}
className={classnames(
styles["popover__content"],
{
[styles["popover__content--full-width"]]: fullWidth,
},
className
)}
{...other}
style={{
minWidth: minWidth ? minWidth : undefined,
maxWidth: maxWidth ? maxWidth : undefined,
...style,
}}
>
{children}
{arrow && <Arrow className={styles["popover__arrow"]} width={arrowWidth} height={arrowHeight} />}
</Content>
</>
</Portal>
);
};

export default memo(forwardRef(PopoverContent));
35 changes: 35 additions & 0 deletions src/components/Popover/PopoverTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, {forwardRef, ForwardRefRenderFunction, memo} from "react";

import classnames from "classnames";

import {PopoverTriggerProps as PopoverTriggerRadixProps, Trigger} from "@radix-ui/react-popover";

import {useComponentProps} from "../../providers";

import styles from "./popover.module.scss";

export interface PopoverTriggerProps extends PopoverTriggerRadixProps {
center?: boolean;
}

const PopoverTrigger: ForwardRefRenderFunction<HTMLButtonElement, PopoverTriggerProps> = (props, ref) => {
const {center, className, children, ...other} = {...useComponentProps("popoverTrigger"), ...props};

return (
<Trigger
ref={ref}
className={classnames(
styles["popover__trigger"],
{
[styles["popover__trigger--center"]]: center,
},
className
)}
{...other}
>
{children}
</Trigger>
);
};

export default memo(forwardRef(PopoverTrigger));
5 changes: 5 additions & 0 deletions src/components/Popover/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {default as Popover, type PopoverProps} from "./Popover";
export {default as PopoverClose, type PopoverCloseProps} from "./PopoverClose";
export {default as PopoverAnchor, type PopoverAnchorProps} from "./PopoverAnchor";
export {default as PopoverTrigger, type PopoverTriggerProps} from "./PopoverTrigger";
export {default as PopoverContent, type PopoverContentProps} from "./PopoverContent";
77 changes: 77 additions & 0 deletions src/components/Popover/popover.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
$root: popover;

.#{$root} {
&__overlay {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
background: var(--popover-overlay-bg, var(--overlay-bg, rgba(0, 0, 0, 0.5)));
}

&__anchor {
height: 100%;
}

&__trigger {
all: unset;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
box-sizing: border-box;
color: var(--popover-trigger-color);
background: var(--popover-trigger-bg-color, var(--bg-secondary-color));
box-shadow: var(--popover-trigger-shadow-offset) var(--popover-trigger-shadow-color);

border-width: var(--popover-trigger-border-width, 1px);
border-style: var(--popover-trigger-border-style, solid);
border-color: var(--popover-trigger-border-color, var(--border-color));
border-radius: var(--popover-trigger-border-radius, 10px);

font-size: var(--popover-trigger-font-size, var(--popover-font-size, 14px));
font-weight: var(--popover-trigger-font-weight, 500);
padding: var(--popover-trigger-padding, 8px 12px);
height: var(--popover-trigger-height);
gap: var(--popover-trigger-gap, 5px);
transition:
color var(--popover-speed-color, var(--speed-color)),
background var(--popover-speed-background, var(--speed-color)),
box-shadow var(--popover-speed-shadow, var(--speed-color)),
border-color var(--popover-speed-border-color, var(--speed-color));

&--center {
justify-content: center;
}

&:hover {
box-shadow: var(--popover-trigger-shadow-offset-hover) var(--popover-trigger-shadow-color);
border-width: var(--popover-trigger-border-width-hover, var(--popover-trigger-border-width, 1px));
border-color: var(--popover-trigger-border-color-hover, var(--popover-trigger-border-color, var(--border-color)));
}
}

&__content {
overflow: hidden;
padding: var(--popover-content-padding, 16px);
background: var(--popover-content-bg-color, var(--bg-primary-color));
border-radius: var(--popover-content-border-radius, 8px);
box-shadow: var(--popover-content-shadow-offset, 0 0 5px 0) var(--popover-content-shadow-color, rgba(0, 0, 0, 0.25));
height: var(--popover-content-height);
max-height: var(--popover-content-max-height, var(--radix-popover-content-available-height));
transition:
background var(--popover-speed-background, var(--speed-color)),
box-shadow var(--popover-speed-shadow, var(--speed-color));

&--full-width {
width: var(--popover-content-full-width, var(--radix-popover-content-available-width));
}
}

&__arrow {
& > polygon {
fill: var(--popover-arrow-bg-color, var(--popover-content-bg-color, var(--bg-primary-color)));
}
}
}
Loading
Loading