170 lines
4.1 KiB
TypeScript
170 lines
4.1 KiB
TypeScript
"use client"
|
|
|
|
import { motion } from "framer-motion"
|
|
import { type PropsWithChildren, useEffect, useState } from "react"
|
|
import {
|
|
Dialog,
|
|
DialogTrigger,
|
|
Modal as AriaModal,
|
|
ModalOverlay,
|
|
} from "react-aria-components"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import { CloseLargeIcon } from "@/components/Icons"
|
|
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
|
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
|
|
import {
|
|
type AnimationState,
|
|
AnimationStateEnum,
|
|
type InnerModalProps,
|
|
type ModalProps,
|
|
} from "./modal"
|
|
import { fade, slideInOut } from "./motionVariants"
|
|
|
|
import styles from "./modal.module.css"
|
|
|
|
const MotionOverlay = motion(ModalOverlay)
|
|
const MotionModal = motion(AriaModal)
|
|
|
|
function InnerModal({
|
|
animation,
|
|
onAnimationComplete = () => undefined,
|
|
setAnimation,
|
|
onToggle,
|
|
isOpen,
|
|
children,
|
|
title,
|
|
subtitle,
|
|
}: PropsWithChildren<InnerModalProps>) {
|
|
const intl = useIntl()
|
|
|
|
function modalStateHandler(newAnimationState: AnimationState) {
|
|
setAnimation((currentAnimationState) =>
|
|
newAnimationState === AnimationStateEnum.hidden &&
|
|
currentAnimationState === AnimationStateEnum.hidden
|
|
? AnimationStateEnum.unmounted
|
|
: currentAnimationState
|
|
)
|
|
if (newAnimationState === AnimationStateEnum.visible) {
|
|
onAnimationComplete()
|
|
}
|
|
}
|
|
|
|
function onOpenChange(state: boolean) {
|
|
onToggle!(state)
|
|
}
|
|
|
|
return (
|
|
<MotionOverlay
|
|
animate={animation}
|
|
className={styles.overlay}
|
|
initial={"hidden"}
|
|
isDismissable
|
|
isExiting={animation === AnimationStateEnum.hidden}
|
|
onAnimationComplete={modalStateHandler}
|
|
variants={fade}
|
|
isOpen={isOpen}
|
|
onOpenChange={onOpenChange}
|
|
>
|
|
<MotionModal
|
|
className={styles.modal}
|
|
variants={slideInOut}
|
|
animate={animation}
|
|
initial={"hidden"}
|
|
>
|
|
<Dialog
|
|
className={styles.dialog}
|
|
aria-label={intl.formatMessage({ id: "Dialog" })}
|
|
>
|
|
{({ close }) => (
|
|
<>
|
|
<header className={styles.header}>
|
|
<div>
|
|
{title && (
|
|
<Subtitle type="one" color="uiTextHighContrast">
|
|
{title}
|
|
</Subtitle>
|
|
)}
|
|
{subtitle && (
|
|
<Preamble asChild>
|
|
<span>{subtitle}</span>
|
|
</Preamble>
|
|
)}
|
|
</div>
|
|
|
|
<button onClick={close} type="button" className={styles.close}>
|
|
<CloseLargeIcon color="uiTextMediumContrast" />
|
|
</button>
|
|
</header>
|
|
<section className={styles.content}>{children}</section>
|
|
</>
|
|
)}
|
|
</Dialog>
|
|
</MotionModal>
|
|
</MotionOverlay>
|
|
)
|
|
}
|
|
|
|
export default function Modal({
|
|
onAnimationComplete = () => undefined,
|
|
trigger,
|
|
isOpen,
|
|
onToggle,
|
|
title,
|
|
subtitle,
|
|
children,
|
|
}: PropsWithChildren<ModalProps>) {
|
|
const [animation, setAnimation] = useState<AnimationState>(
|
|
AnimationStateEnum.visible
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (typeof isOpen === "boolean") {
|
|
setAnimation(
|
|
isOpen ? AnimationStateEnum.visible : AnimationStateEnum.hidden
|
|
)
|
|
}
|
|
if (isOpen === undefined) {
|
|
setAnimation(AnimationStateEnum.unmounted)
|
|
}
|
|
}, [isOpen])
|
|
|
|
if (!trigger) {
|
|
return (
|
|
<InnerModal
|
|
onAnimationComplete={onAnimationComplete}
|
|
animation={animation}
|
|
setAnimation={setAnimation}
|
|
onToggle={onToggle}
|
|
isOpen={isOpen}
|
|
title={title}
|
|
subtitle={subtitle}
|
|
>
|
|
{children}
|
|
</InnerModal>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<DialogTrigger
|
|
onOpenChange={(isOpen) =>
|
|
setAnimation(
|
|
isOpen ? AnimationStateEnum.visible : AnimationStateEnum.hidden
|
|
)
|
|
}
|
|
>
|
|
{trigger}
|
|
<InnerModal
|
|
onAnimationComplete={onAnimationComplete}
|
|
animation={animation}
|
|
setAnimation={setAnimation}
|
|
title={title}
|
|
subtitle={subtitle}
|
|
>
|
|
{children}
|
|
</InnerModal>
|
|
</DialogTrigger>
|
|
)
|
|
}
|