️ feat: create modal comopnent

This commit is contained in:
Wahyu Kurniawan 2024-03-08 15:27:53 +07:00
parent 32305cedbc
commit 16e7b22507
No known key found for this signature in database
GPG Key ID: 040A1549143A8E33
12 changed files with 325 additions and 0 deletions

View 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>;

View 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;

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export * from './ModalBody';

View File

@ -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 };

View File

@ -0,0 +1 @@
export * from './ModalContent';

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export * from './ModalFooter';

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export * from './ModalHeader';

View File

@ -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;

View File

@ -0,0 +1 @@
export * from './Modal';