Skip to content

Commit

Permalink
style(dialog): animate dialog height change through prop (#1363)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredvu authored Dec 12, 2024
1 parent f06a615 commit ad70e35
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 129 deletions.
216 changes: 87 additions & 129 deletions src/components/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import {
Title,
Trigger,
} from '@radix-ui/react-dialog';
import styled, { css, keyframes } from 'styled-components';
import styled, { css } from 'styled-components';
import tw from 'twin.macro';

import { useDialogArea } from '@/hooks/useDialogArea';
import { useResizeObserver } from '@/hooks/useResizeObserver';

import breakpoints from '@/styles/breakpoints';
import { layoutMixins } from '@/styles/layoutMixins';
Expand Down Expand Up @@ -100,6 +101,12 @@ export const Dialog = ({
className,
}: DialogProps) => {
const closeButtonRef = useRef<HTMLButtonElement>(null);
const ref = useRef<HTMLDivElement>(null);

const { height = 0 } = useResizeObserver({
ref,
box: 'border-box',
});

return (
<Root modal={withOverlay} open={isOpen} onOpenChange={setIsOpen}>
Expand All @@ -117,66 +124,69 @@ export const Dialog = ({
e.preventDefault();
}
}}
$height={height}
$stacked={stacked}
$withAnimation={withAnimation}
>
{slotHeaderAbove}
{stacked ? (
<$StackedHeaderTopRow $withBorder={hasHeaderBorder} $withBlur={hasHeaderBlur}>
{onBack && <$BackButton onClick={onBack} />}

{slotIcon}

{!preventClose && withClose && (
<$Close ref={closeButtonRef} $absolute={stacked}>
<Icon iconName={IconName.Close} />
</$Close>
)}

{title && <$Title>{title}</$Title>}

{description && <$Description>{description}</$Description>}

{slotHeaderInner}
</$StackedHeaderTopRow>
) : slotHeader ? (
<div>
{!preventClose && withClose && (
<$Close ref={closeButtonRef} $absolute>
<Icon iconName={IconName.Close} />
</$Close>
)}
{slotHeader}
</div>
) : (
<$Header $withBorder={hasHeaderBorder} $withBlur={hasHeaderBlur}>
<div tw="row gap-[--dialog-title-gap]">
{onBack && <BackButton onClick={onBack} />}

{slotIcon && (
<div tw="row h-[1em] w-[1em] text-[length:--dialog-icon-size] leading-none">
{slotIcon}
</div>
<$InnerContainer ref={ref} placement={placement}>
{slotHeaderAbove}
{stacked ? (
<$StackedHeaderTopRow $withBorder={hasHeaderBorder} $withBlur={hasHeaderBlur}>
{onBack && <$BackButton onClick={onBack} />}

{slotIcon}

{!preventClose && withClose && (
<$Close ref={closeButtonRef} $absolute={stacked}>
<Icon iconName={IconName.Close} />
</$Close>
)}

{title && <$Title>{title}</$Title>}

{description && <$Description>{description}</$Description>}

{slotHeaderInner}
</$StackedHeaderTopRow>
) : slotHeader ? (
<div>
{!preventClose && withClose && (
<$Close ref={closeButtonRef}>
<$Close ref={closeButtonRef} $absolute>
<Icon iconName={IconName.Close} />
</$Close>
)}
{slotHeader}
</div>
) : (
<$Header $withBorder={hasHeaderBorder} $withBlur={hasHeaderBlur}>
<div tw="row gap-[--dialog-title-gap]">
{onBack && <BackButton onClick={onBack} />}

{slotIcon && (
<div tw="row h-[1em] w-[1em] text-[length:--dialog-icon-size] leading-none">
{slotIcon}
</div>
)}

{title && <$Title>{title}</$Title>}

{description && <$Description>{description}</$Description>}
{!preventClose && withClose && (
<$Close ref={closeButtonRef}>
<Icon iconName={IconName.Close} />
</$Close>
)}
</div>

{slotHeaderInner}
</$Header>
)}
{description && <$Description>{description}</$Description>}

<$Content>{children}</$Content>
{slotHeaderInner}
</$Header>
)}

{slotFooter && <$Footer $withBorder={hasFooterBorder}>{slotFooter}</$Footer>}
<$Content>{children}</$Content>

{slotFooter && <$Footer $withBorder={hasFooterBorder}>{slotFooter}</$Footer>}
</$InnerContainer>
</$Container>
</DialogPortal>
</Root>
Expand All @@ -196,6 +206,7 @@ const $Overlay = styled(Overlay)`

const $Container = styled(Content)<{
placement: DialogPlacement;
$height?: number;
$stacked?: boolean;
$withAnimation?: boolean;
}>`
Expand Down Expand Up @@ -255,24 +266,28 @@ const $Container = styled(Content)<{
outline: none;
${({ placement, $withAnimation }) =>
${({ placement, $height, $withAnimation }) =>
({
[DialogPlacement.Default]: css`
inset: var(--dialog-inset);
margin: auto;
max-width: var(--dialog-width);
height: fit-content;
max-height: var(--dialog-height);
${$withAnimation
? css`
height: ${$height ? `${$height}px` : 'fit-content'};
transition: height 0.25s ease-in-out;
`
: css`
height: fit-content;
`}
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);
Expand All @@ -284,94 +299,18 @@ const $Container = styled(Content)<{
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); */
}
${$withAnimation &&
css`
@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);
height: 100%;
@media ${breakpoints.notMobile} {
max-width: var(--dialog-width);
margin-left: auto;
}
${$withAnimation &&
css`
@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`
${$withAnimation &&
css`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
from {
scale: 0.99;
opacity: 0;
}
`} 0.15s var(--ease-out-expo);
}
&[data-state='closed'] {
animation: ${keyframes`
to {
scale: 0.99;
opacity: 0;
}
`} 0.15s var(--ease-out-expo);
}
}
`}
`,
[DialogPlacement.Inline]: css``,
[DialogPlacement.FullScreen]: css`
--dialog-width: 100vw;
--dialog-height: 100vh;
Expand All @@ -388,6 +327,25 @@ const $Container = styled(Content)<{
`}
`;

const $InnerContainer = styled.div<{ placement: DialogPlacement }>`
${({ placement }) =>
({
[DialogPlacement.Default]: css``,
[DialogPlacement.Sidebar]: css`
${layoutMixins.flexColumn}
height: 100%;
`,
[DialogPlacement.Inline]: css`
${layoutMixins.flexColumn}
height: 100%;
`,
[DialogPlacement.FullScreen]: css`
${layoutMixins.flexColumn}
height: 100%;
`,
})[placement]}
`;

const $Header = styled.header<{ $withBorder: boolean; $withBlur: boolean }>`
${layoutMixins.stickyHeader}
Expand Down
19 changes: 19 additions & 0 deletions src/hooks/useIsMounted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useCallback, useEffect, useRef } from 'react';

/**
* @description Custom hook that determines if the component is currently mounted.
* @url https://usehooks-ts.com/react-hook/use-is-mounted
*/
export function useIsMounted(): () => boolean {
const isMounted = useRef(false);

useEffect(() => {
isMounted.current = true;

return () => {
isMounted.current = false;
};
}, []);

return useCallback(() => isMounted.current, []);
}
Loading

0 comments on commit ad70e35

Please sign in to comment.