Initial commit
This commit is contained in:
@@ -0,0 +1,460 @@
|
||||
import { useRef } from 'react';
|
||||
import styled, { type AnyStyledComponent, keyframes, css } from 'styled-components';
|
||||
|
||||
import {
|
||||
Root,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Title,
|
||||
Description,
|
||||
Close,
|
||||
Portal,
|
||||
} from '@radix-ui/react-dialog';
|
||||
|
||||
import { breakpoints } from '@/styles';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { BackButton } from '@/components/BackButton';
|
||||
|
||||
import { useDialogArea } from '@/hooks/useDialogArea';
|
||||
|
||||
export enum DialogPlacement {
|
||||
Default = 'Default',
|
||||
Sidebar = 'Sidebar',
|
||||
Inline = 'Inline',
|
||||
FullScreen = 'FullScreen',
|
||||
}
|
||||
|
||||
type ElementProps = {
|
||||
isOpen?: boolean;
|
||||
setIsOpen?: (open: boolean) => void;
|
||||
slotIcon?: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
onBack?: () => void;
|
||||
preventClose?: boolean;
|
||||
slotTrigger?: React.ReactNode;
|
||||
slotHeaderInner?: React.ReactNode;
|
||||
slotFooter?: React.ReactNode;
|
||||
};
|
||||
|
||||
type StyleProps = {
|
||||
placement?: DialogPlacement;
|
||||
portalContainer?: HTMLElement;
|
||||
hasHeaderBorder?: boolean;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const DialogPortal = ({
|
||||
withPortal,
|
||||
container,
|
||||
children,
|
||||
}: {
|
||||
withPortal: boolean;
|
||||
container?: HTMLElement;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { dialogArea } = useDialogArea();
|
||||
|
||||
return withPortal ? (
|
||||
<Portal container={container ?? dialogArea}>{children}</Portal>
|
||||
) : (
|
||||
<>{children}</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Dialog = ({
|
||||
isOpen = false,
|
||||
setIsOpen,
|
||||
slotIcon,
|
||||
title,
|
||||
description,
|
||||
onBack,
|
||||
preventClose,
|
||||
slotTrigger,
|
||||
slotHeaderInner,
|
||||
slotFooter,
|
||||
placement = DialogPlacement.Default,
|
||||
portalContainer,
|
||||
hasHeaderBorder = false,
|
||||
children,
|
||||
className,
|
||||
}: ElementProps & StyleProps) => {
|
||||
const closeButtonRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const showOverlay = ![DialogPlacement.Inline, DialogPlacement.FullScreen].includes(placement);
|
||||
|
||||
return (
|
||||
<Root modal={showOverlay} open={isOpen} onOpenChange={setIsOpen}>
|
||||
{slotTrigger && <Trigger asChild>{slotTrigger}</Trigger>}
|
||||
<DialogPortal withPortal={placement !== DialogPlacement.Inline} container={portalContainer}>
|
||||
{showOverlay && <Styled.Overlay />}
|
||||
<Styled.Container
|
||||
placement={placement}
|
||||
className={className}
|
||||
onEscapeKeyDown={() => {
|
||||
closeButtonRef.current?.focus();
|
||||
}}
|
||||
onInteractOutside={(e: Event) => {
|
||||
if (!showOverlay || preventClose) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Styled.Header $withBorder={hasHeaderBorder}>
|
||||
<Styled.HeaderTopRow>
|
||||
{onBack && <BackButton onClick={onBack} />}
|
||||
|
||||
{slotIcon && <Styled.Icon>{slotIcon}</Styled.Icon>}
|
||||
|
||||
{title && <Styled.Title>{title}</Styled.Title>}
|
||||
|
||||
{!preventClose && (
|
||||
<Styled.Close ref={closeButtonRef}>
|
||||
<Icon iconName={IconName.Close} />
|
||||
</Styled.Close>
|
||||
)}
|
||||
</Styled.HeaderTopRow>
|
||||
|
||||
{description && <Styled.Description>{description}</Styled.Description>}
|
||||
|
||||
{slotHeaderInner}
|
||||
</Styled.Header>
|
||||
|
||||
<Styled.Content>{children}</Styled.Content>
|
||||
|
||||
{slotFooter && <Styled.Footer>{slotFooter}</Styled.Footer>}
|
||||
</Styled.Container>
|
||||
</DialogPortal>
|
||||
</Root>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Overlay = styled(Overlay)`
|
||||
z-index: 1;
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&[data-state='open'] {
|
||||
animation: ${keyframes`
|
||||
to {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
`} 0.15s var(--ease-out-expo) forwards;
|
||||
}
|
||||
|
||||
&[data-state='closed'] {
|
||||
animation: ${keyframes`
|
||||
from {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
`} 0.15s;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
|
||||
/* Params */
|
||||
--dialog-inset: 1rem;
|
||||
--dialog-width: 30rem;
|
||||
--dialog-backgroundColor: var(--color-layer-3);
|
||||
--dialog-radius: 1rem;
|
||||
|
||||
--dialog-paddingX: 1.5rem;
|
||||
|
||||
--dialog-header-z: 1;
|
||||
--dialog-header-height: auto; /* set to fixed value to enable inner sticky areas */
|
||||
--dialog-header-paddingTop: 1.5rem;
|
||||
--dialog-header-paddingBottom: 1rem;
|
||||
--dialog-header-paddingLeft: var(--dialog-paddingX);
|
||||
--dialog-header-paddingRight: var(--dialog-paddingX);
|
||||
|
||||
--dialog-content-paddingTop: 0rem;
|
||||
--dialog-content-paddingBottom: 1.5rem;
|
||||
--dialog-content-paddingLeft: var(--dialog-paddingX);
|
||||
--dialog-content-paddingRight: var(--dialog-paddingX);
|
||||
|
||||
--dialog-footer-paddingTop: 0rem;
|
||||
--dialog-footer-paddingBottom: 1rem;
|
||||
--dialog-footer-paddingLeft: var(--dialog-paddingX);
|
||||
--dialog-footer-paddingRight: var(--dialog-paddingX);
|
||||
|
||||
--dialog-title-gap: 0.5rem;
|
||||
--dialog-icon-size: 1.75em;
|
||||
|
||||
/* Calculated */
|
||||
--dialog-height: calc(100% - 2 * var(--dialog-inset));
|
||||
|
||||
/* Rules */
|
||||
${layoutMixins.scrollArea}
|
||||
--scrollArea-height: var(--dialog-height);
|
||||
|
||||
${layoutMixins.withOuterBorder}
|
||||
--border-width: var(--default-border-width);
|
||||
--border-color: var(--color-border);
|
||||
|
||||
isolation: isolate;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
${layoutMixins.stickyArea0}
|
||||
--stickyArea0-topHeight: var(--dialog-header-height);
|
||||
--stickyArea0-background: var(--dialog-backgroundColor);
|
||||
|
||||
${layoutMixins.flexColumn}
|
||||
|
||||
outline: none;
|
||||
|
||||
${({ placement }) =>
|
||||
({
|
||||
[DialogPlacement.Default]: css`
|
||||
inset: var(--dialog-inset);
|
||||
margin: auto;
|
||||
|
||||
max-width: var(--dialog-width);
|
||||
height: fit-content;
|
||||
max-height: var(--dialog-height);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
border-radius: var(--dialog-radius);
|
||||
/* clip-path: inset(
|
||||
calc(-1 * var(--border-width)) round calc(var(--dialog-radius) + var(--border-width))
|
||||
);
|
||||
overflow-clip-margin: var(--border-width); */
|
||||
|
||||
@media ${breakpoints.mobile} {
|
||||
top: calc(var(--dialog-inset) * 2);
|
||||
bottom: 0;
|
||||
--dialog-width: initial;
|
||||
width: var(--dialog-width);
|
||||
|
||||
margin-bottom: 0;
|
||||
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
/* Hack (uneven border-radius causes overflow issues) */
|
||||
/* top: auto;
|
||||
bottom: calc(-1 * var(--dialog-radius));
|
||||
padding-bottom: var(--dialog-radius); */
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&[data-state='open'] {
|
||||
animation: ${keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
0.01% {
|
||||
max-height: 0;
|
||||
}
|
||||
`} 0.15s var(--ease-out-expo);
|
||||
}
|
||||
|
||||
&[data-state='closed'] {
|
||||
animation: ${keyframes`
|
||||
to {
|
||||
opacity: 0;
|
||||
scale: 0.9;
|
||||
max-height: 0;
|
||||
}
|
||||
`} 0.15s;
|
||||
}
|
||||
}
|
||||
`,
|
||||
[DialogPlacement.Sidebar]: css`
|
||||
--dialog-width: var(--sidebar-width);
|
||||
|
||||
@media ${breakpoints.notMobile} {
|
||||
max-width: var(--dialog-width);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&[data-state='open'] {
|
||||
animation: ${keyframes`
|
||||
from {
|
||||
translate: 100% 0;
|
||||
opacity: 0;
|
||||
}
|
||||
`} 0.15s var(--ease-out-expo);
|
||||
}
|
||||
|
||||
&[data-state='closed'] {
|
||||
animation: ${keyframes`
|
||||
to {
|
||||
translate: 100% 0;
|
||||
opacity: 0;
|
||||
}
|
||||
`} 0.15s var(--ease-out-expo);
|
||||
}
|
||||
}
|
||||
`,
|
||||
[DialogPlacement.Inline]: css`
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&[data-state='open'] {
|
||||
animation: ${keyframes`
|
||||
from {
|
||||
scale: 0.99;
|
||||
opacity: 0;
|
||||
/* filter: blur(2px); */
|
||||
/* backdrop-filter: none; */
|
||||
}
|
||||
`} 0.15s var(--ease-out-expo);
|
||||
}
|
||||
|
||||
&[data-state='closed'] {
|
||||
animation: ${keyframes`
|
||||
to {
|
||||
scale: 0.99;
|
||||
opacity: 0;
|
||||
/* filter: blur(2px); */
|
||||
/* backdrop-filter: none; */
|
||||
}
|
||||
`} 0.15s var(--ease-out-expo);
|
||||
}
|
||||
}
|
||||
`,
|
||||
[DialogPlacement.FullScreen]: css`
|
||||
--dialog-width: 100vw;
|
||||
--dialog-height: 100vh;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
`,
|
||||
}[placement])}
|
||||
`;
|
||||
|
||||
Styled.Header = styled.header<{ $withBorder: boolean }>`
|
||||
${layoutMixins.stickyHeader}
|
||||
|
||||
z-index: var(--dialog-header-z);
|
||||
|
||||
display: block;
|
||||
padding: var(--dialog-header-paddingTop) var(--dialog-header-paddingLeft)
|
||||
var(--dialog-header-paddingBottom) var(--dialog-header-paddingRight);
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
|
||||
${({ $withBorder }) =>
|
||||
$withBorder &&
|
||||
css`
|
||||
${layoutMixins.withOuterBorder};
|
||||
background: var(--dialog-backgroundColor);
|
||||
`};
|
||||
`;
|
||||
|
||||
Styled.HeaderTopRow = styled.div`
|
||||
${layoutMixins.row}
|
||||
gap: var(--dialog-title-gap);
|
||||
`;
|
||||
|
||||
Styled.HeaderTopRow = styled.div`
|
||||
${layoutMixins.row}
|
||||
gap: var(--dialog-title-gap);
|
||||
`;
|
||||
|
||||
Styled.Content = styled.div`
|
||||
flex: 1;
|
||||
|
||||
${layoutMixins.column}
|
||||
|
||||
${layoutMixins.stickyArea1}
|
||||
--stickyArea1-background: var(--dialog-backgroundColor);
|
||||
--stickyArea1-paddingTop: var(--dialog-content-paddingTop);
|
||||
--stickyArea1-paddingBottom: var(--dialog-content-paddingBottom);
|
||||
--stickyArea1-paddingLeft: var(--dialog-content-paddingLeft);
|
||||
--stickyArea1-paddingRight: var(--dialog-content-paddingRight);
|
||||
|
||||
padding: var(--dialog-content-paddingTop) var(--dialog-content-paddingRight)
|
||||
var(--dialog-content-paddingBottom) var(--dialog-content-paddingLeft);
|
||||
|
||||
isolation: isolate;
|
||||
`;
|
||||
|
||||
Styled.Icon = styled.div`
|
||||
${layoutMixins.row}
|
||||
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
font-size: var(--dialog-icon-size); /* 1 line-height */
|
||||
line-height: 1;
|
||||
`;
|
||||
|
||||
Styled.Close = styled(Close)`
|
||||
width: 0.7813rem;
|
||||
height: 0.7813rem;
|
||||
|
||||
box-sizing: content-box;
|
||||
padding: 0.5rem;
|
||||
margin: auto 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
border-radius: 0.25rem;
|
||||
|
||||
color: var(--color-text-0);
|
||||
|
||||
> svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
@media ${breakpoints.tablet} {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Title = styled(Title)`
|
||||
flex: 1;
|
||||
|
||||
font: var(--font-large-medium);
|
||||
color: var(--color-text-2);
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
Styled.Description = styled(Description)`
|
||||
margin-top: 0.5rem;
|
||||
color: var(--color-text-0);
|
||||
font: var(--font-base-book);
|
||||
`;
|
||||
|
||||
Styled.Footer = styled.footer`
|
||||
display: grid;
|
||||
${layoutMixins.stickyFooter}
|
||||
${layoutMixins.withStickyFooterBackdrop}
|
||||
--stickyFooterBackdrop-outsetX: var(--dialog-paddingX);
|
||||
|
||||
padding: var(--dialog-footer-paddingTop) var(--dialog-footer-paddingLeft)
|
||||
var(--dialog-footer-paddingBottom) var(--dialog-footer-paddingRight);
|
||||
`;
|
||||
Reference in New Issue
Block a user