mirror of
https://github.com/snowball-tools/snowballtools-base.git
synced 2024-11-17 16:29:19 +00:00
⚡️ feat: create modal comopnent
This commit is contained in:
parent
32305cedbc
commit
16e7b22507
67
packages/frontend/src/components/shared/Modal/Modal.theme.ts
Normal file
67
packages/frontend/src/components/shared/Modal/Modal.theme.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import type { VariantProps } from 'tailwind-variants';
|
||||||
|
import { tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const modalTheme = tv({
|
||||||
|
slots: {
|
||||||
|
overlay: [
|
||||||
|
'z-modal',
|
||||||
|
'fixed',
|
||||||
|
'inset-0',
|
||||||
|
'bg-bg-base/90',
|
||||||
|
'backdrop-blur-sm',
|
||||||
|
'overflow-y-auto',
|
||||||
|
'flex',
|
||||||
|
'justify-center',
|
||||||
|
'items-center',
|
||||||
|
'p-6',
|
||||||
|
'sm:p-10',
|
||||||
|
'data-[state=closed]:animate-[dialog-overlay-hide_200ms]',
|
||||||
|
'data-[state=open]:animate-[dialog-overlay-show_200ms]',
|
||||||
|
'data-[state=closed]:hidden', // Fix overlay not close when modal is closed
|
||||||
|
],
|
||||||
|
close: [
|
||||||
|
'absolute',
|
||||||
|
'right-6',
|
||||||
|
'top-5',
|
||||||
|
'sm:right-10',
|
||||||
|
'sm:top-[38px]',
|
||||||
|
'z-[1]',
|
||||||
|
],
|
||||||
|
header: ['flex', 'flex-col', 'gap-2', 'items-start', 'px-6', 'sm:px-10'],
|
||||||
|
headerTitle: ['text-lg', 'sm:text-xl', 'text-text-em-high'],
|
||||||
|
headerDescription: ['text-sm', 'text-text-em-low'],
|
||||||
|
footer: ['flex', 'justify-end', 'gap-3', 'sm:gap-4', 'px-6', 'sm:px-10'],
|
||||||
|
content: [
|
||||||
|
'h-fit',
|
||||||
|
'sm:min-h-0',
|
||||||
|
'sm:m-auto',
|
||||||
|
'relative',
|
||||||
|
'flex',
|
||||||
|
'flex-col',
|
||||||
|
'gap-y-8',
|
||||||
|
'py-6',
|
||||||
|
'sm:py-10',
|
||||||
|
'overflow-hidden',
|
||||||
|
'w-full',
|
||||||
|
'max-w-[560px]',
|
||||||
|
'rounded-xl',
|
||||||
|
'bg-surface-base',
|
||||||
|
'border',
|
||||||
|
'border-border-base',
|
||||||
|
'text-text-em-high',
|
||||||
|
],
|
||||||
|
body: ['flex-1', 'px-6', 'sm:px-10'],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
fullPage: {
|
||||||
|
true: {
|
||||||
|
content: ['h-full'],
|
||||||
|
overlay: ['!p-0'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
fullPage: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export type ModalVariants = VariantProps<typeof modalTheme>;
|
46
packages/frontend/src/components/shared/Modal/Modal.tsx
Normal file
46
packages/frontend/src/components/shared/Modal/Modal.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { DialogProps } from '@radix-ui/react-dialog';
|
||||||
|
import { Root, Trigger } from '@radix-ui/react-dialog';
|
||||||
|
import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import type { ModalVariants } from './Modal.theme';
|
||||||
|
import { ModalBody } from './ModalBody';
|
||||||
|
import { ModalContent } from './ModalContent';
|
||||||
|
import { ModalFooter } from './ModalFooter';
|
||||||
|
import { ModalHeader } from './ModalHeader';
|
||||||
|
import ModalProvider from './ModalProvider';
|
||||||
|
|
||||||
|
export interface ModalProps
|
||||||
|
extends ComponentPropsWithoutRef<'div'>,
|
||||||
|
ModalVariants,
|
||||||
|
DialogProps {
|
||||||
|
hasCloseButton?: boolean;
|
||||||
|
hasOverlay?: boolean;
|
||||||
|
preventClickOutsideToClose?: boolean;
|
||||||
|
}
|
||||||
|
export const Modal = ({
|
||||||
|
children,
|
||||||
|
hasCloseButton = true,
|
||||||
|
hasOverlay = true,
|
||||||
|
preventClickOutsideToClose = false,
|
||||||
|
fullPage = false,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<ModalProps>) => {
|
||||||
|
return (
|
||||||
|
<ModalProvider
|
||||||
|
fullPage={fullPage}
|
||||||
|
hasCloseButton={hasCloseButton}
|
||||||
|
hasOverlay={hasOverlay}
|
||||||
|
preventClickOutsideToClose={preventClickOutsideToClose}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Root {...props}>{children}</Root>
|
||||||
|
</ModalProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Modal.Trigger = Trigger;
|
||||||
|
Modal.Content = ModalContent;
|
||||||
|
Modal.Header = ModalHeader;
|
||||||
|
Modal.Footer = ModalFooter;
|
||||||
|
Modal.Body = ModalBody;
|
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react';
|
||||||
|
import { modalTheme } from 'components/shared/Modal/Modal.theme';
|
||||||
|
|
||||||
|
export interface ModalBodyProps extends ComponentPropsWithoutRef<'div'> {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModalBody = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<ModalBodyProps>) => {
|
||||||
|
const { body } = modalTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={body({
|
||||||
|
className,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './ModalBody';
|
@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||||
|
import { Close, Content, Overlay, Portal } from '@radix-ui/react-dialog';
|
||||||
|
import { Ref, forwardRef, type PropsWithChildren } from 'react';
|
||||||
|
import { useModal } from 'components/shared/Modal/ModalProvider';
|
||||||
|
import { modalTheme } from 'components/shared/Modal/Modal.theme';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import { CrossIcon } from 'components/shared/CustomIcon';
|
||||||
|
|
||||||
|
type PointerDownOutsideEvent = CustomEvent<{
|
||||||
|
originalEvent: PointerEvent;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export interface ModalContentProps extends DialogContentProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalContent = forwardRef(
|
||||||
|
(
|
||||||
|
{ children, className, ...props }: PropsWithChildren<ModalContentProps>,
|
||||||
|
forwardedRef,
|
||||||
|
) => {
|
||||||
|
const { hasCloseButton, preventClickOutsideToClose, fullPage } = useModal();
|
||||||
|
|
||||||
|
const { content, close, overlay } = modalTheme({ fullPage });
|
||||||
|
|
||||||
|
const preventClickOutsideToCloseProps = preventClickOutsideToClose && {
|
||||||
|
onPointerDownOutside: (e: PointerDownOutsideEvent) => e.preventDefault(),
|
||||||
|
onEscapeKeyDown: (e: KeyboardEvent) => e.preventDefault(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Overlay className={overlay({ fullPage })}>
|
||||||
|
<Content
|
||||||
|
className={content({ className, fullPage })}
|
||||||
|
{...preventClickOutsideToCloseProps}
|
||||||
|
{...props}
|
||||||
|
ref={forwardedRef as Ref<HTMLDivElement>}
|
||||||
|
>
|
||||||
|
{hasCloseButton && (
|
||||||
|
<Close asChild>
|
||||||
|
<Button
|
||||||
|
aria-label="Close"
|
||||||
|
className={close()}
|
||||||
|
size="sm"
|
||||||
|
iconOnly
|
||||||
|
variant="tertiary"
|
||||||
|
>
|
||||||
|
<CrossIcon />
|
||||||
|
</Button>
|
||||||
|
</Close>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</Content>
|
||||||
|
</Overlay>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ModalContent.displayName = 'ModalContent';
|
||||||
|
|
||||||
|
export { ModalContent };
|
@ -0,0 +1 @@
|
|||||||
|
export * from './ModalContent';
|
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react';
|
||||||
|
import { modalTheme } from 'components/shared/Modal/Modal.theme';
|
||||||
|
|
||||||
|
type ModalFooterProps = ComponentPropsWithoutRef<'div'> & {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModalFooter = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<ModalFooterProps>) => {
|
||||||
|
const { footer } = modalTheme({
|
||||||
|
className,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className={footer({ className })} {...props}>
|
||||||
|
{children}
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './ModalFooter';
|
@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { DialogDescriptionProps } from '@radix-ui/react-dialog';
|
||||||
|
import { Description, Title } from '@radix-ui/react-dialog';
|
||||||
|
import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react';
|
||||||
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
import { modalTheme } from 'components/shared/Modal/Modal.theme';
|
||||||
|
|
||||||
|
type ModalHeaderProps = ComponentPropsWithoutRef<'div'> & {
|
||||||
|
className?: string;
|
||||||
|
description?: string | React.ReactNode;
|
||||||
|
descriptionProps?: DialogDescriptionProps;
|
||||||
|
headingProps?: ComponentPropsWithoutRef<'h2'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModalHeader = ({
|
||||||
|
children,
|
||||||
|
description,
|
||||||
|
className,
|
||||||
|
descriptionProps,
|
||||||
|
headingProps,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<ModalHeaderProps>) => {
|
||||||
|
const { header, headerDescription, headerTitle } = modalTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={header({
|
||||||
|
className,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Title asChild>
|
||||||
|
<Heading
|
||||||
|
{...headingProps}
|
||||||
|
className={headerTitle({ className: headingProps?.className })}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Heading>
|
||||||
|
</Title>
|
||||||
|
{description && (
|
||||||
|
<Description
|
||||||
|
{...descriptionProps}
|
||||||
|
className={headerDescription({
|
||||||
|
className: descriptionProps?.className,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Description>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './ModalHeader';
|
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
import type { ModalProps } from './Modal';
|
||||||
|
import type { ModalVariants } from './Modal.theme';
|
||||||
|
|
||||||
|
export interface ModalProviderProps
|
||||||
|
extends Partial<ModalVariants>,
|
||||||
|
ModalProps {}
|
||||||
|
|
||||||
|
type ModalProviderContext = ReturnType<typeof useModalValues>;
|
||||||
|
|
||||||
|
const ModalContext = createContext<Partial<ModalProviderContext> | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
// For inferring return type
|
||||||
|
const useModalValues = (props: ModalProviderProps) => {
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModalProvider = ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<ModalProviderProps>): JSX.Element => {
|
||||||
|
const values = useModalValues(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContext.Provider value={values}>{children}</ModalContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useModal = () => {
|
||||||
|
const context = useContext(ModalContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useModal was used outside of its Provider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalProvider;
|
1
packages/frontend/src/components/shared/Modal/index.ts
Normal file
1
packages/frontend/src/components/shared/Modal/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Modal';
|
Loading…
Reference in New Issue
Block a user