Fix/STAY-133 * fix: Add static summary buttons row on add ancillary flow * fix: refactor handling of modals * fix: refactor file structure for add ancillary flow * Merged in chore/replace-deprecated-body (pull request #3300) Replace deprecated <Body> with <Typography> * chore: replace deprecated body component * refactor: replace Body component with Typography across various components * merge Approved-by: Bianca Widstam Approved-by: Matilda Landström Approved-by: Bianca Widstam Approved-by: Matilda Landström
231 lines
6.1 KiB
TypeScript
231 lines
6.1 KiB
TypeScript
'use client'
|
|
|
|
import { cx } from 'class-variance-authority'
|
|
import { AnimatePresence, motion } from 'motion/react'
|
|
import { type PropsWithChildren, useEffect, useState } from 'react'
|
|
import {
|
|
Modal as AriaModal,
|
|
Dialog,
|
|
DialogTrigger,
|
|
ModalOverlay,
|
|
} from 'react-aria-components'
|
|
import { useIntl } from 'react-intl'
|
|
|
|
import { MaterialIcon } from '../Icons/MaterialIcon'
|
|
|
|
import {
|
|
type AnimationState,
|
|
AnimationStateEnum,
|
|
type InnerModalProps,
|
|
type ModalProps,
|
|
} from './modal'
|
|
import { fade, slideInOut } from './motionVariants'
|
|
import { modalContentVariants } from './variants'
|
|
|
|
import { IconButton } from '../IconButton'
|
|
import { Typography } from '../Typography'
|
|
import styles from './modal.module.css'
|
|
|
|
const MotionOverlay = motion.create(ModalOverlay)
|
|
const MotionModal = motion.create(AriaModal)
|
|
|
|
function InnerModal({
|
|
animation,
|
|
onAnimationComplete = () => undefined,
|
|
setAnimation,
|
|
onToggle,
|
|
isOpen,
|
|
children,
|
|
title,
|
|
subtitle,
|
|
withActions,
|
|
hideHeader,
|
|
className,
|
|
contentClassName,
|
|
}: PropsWithChildren<InnerModalProps>) {
|
|
const intl = useIntl()
|
|
|
|
const contentClassNames = modalContentVariants({
|
|
withActions,
|
|
className: contentClassName,
|
|
})
|
|
|
|
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
|
|
// TODO: Enabling this causes the modal to never unmount.
|
|
// Seems to be an issue with react-aria-components, see https://github.com/adobe/react-spectrum/issues/7563.
|
|
// Exit animations can probably be fixed by rewriting to a solution similar to
|
|
// https://react-spectrum.adobe.com/react-aria/examples/framer-modal-sheet.html.
|
|
// isExiting={animation === AnimationStateEnum.hidden}
|
|
onAnimationComplete={modalStateHandler}
|
|
variants={fade}
|
|
isOpen={isOpen}
|
|
onOpenChange={onOpenChange}
|
|
>
|
|
<MotionModal
|
|
className={cx(styles.modal, className)}
|
|
variants={slideInOut}
|
|
animate={animation}
|
|
initial="hidden"
|
|
>
|
|
<Dialog
|
|
className={styles.dialog}
|
|
aria-label={intl.formatMessage({
|
|
id: 'modal.dialog',
|
|
defaultMessage: 'Dialog',
|
|
})}
|
|
>
|
|
{({ close }) => (
|
|
<>
|
|
{!hideHeader && (
|
|
<header
|
|
className={cx(styles.header, {
|
|
[styles.verticalCenter]: !subtitle,
|
|
})}
|
|
>
|
|
<div>
|
|
{title && (
|
|
<Typography variant="Title/Subtitle/lg">
|
|
<h3>{title}</h3>
|
|
</Typography>
|
|
)}
|
|
{subtitle && (
|
|
<Typography variant="Body/Lead text">
|
|
<span>{subtitle}</span>
|
|
</Typography>
|
|
)}
|
|
</div>
|
|
|
|
<IconButton
|
|
onPress={close}
|
|
className={styles.close}
|
|
type="button"
|
|
aria-label={intl.formatMessage({
|
|
id: 'common.close',
|
|
defaultMessage: 'Close',
|
|
})}
|
|
theme="Black"
|
|
style="Muted"
|
|
>
|
|
<MaterialIcon
|
|
icon="close"
|
|
color="Icon/Feedback/Neutral"
|
|
size={24}
|
|
/>
|
|
</IconButton>
|
|
</header>
|
|
)}
|
|
|
|
<div className={contentClassNames}>{children}</div>
|
|
</>
|
|
)}
|
|
</Dialog>
|
|
</MotionModal>
|
|
</MotionOverlay>
|
|
)
|
|
}
|
|
|
|
export default function Modal({
|
|
onAnimationComplete = () => undefined,
|
|
trigger,
|
|
isOpen,
|
|
onToggle,
|
|
onOpenChange,
|
|
title,
|
|
subtitle,
|
|
children,
|
|
withActions = false,
|
|
hideHeader = false,
|
|
className = '',
|
|
contentClassName = '',
|
|
}: 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])
|
|
|
|
const shouldRender = isOpen || animation !== AnimationStateEnum.unmounted
|
|
|
|
if (!trigger) {
|
|
return (
|
|
<AnimatePresence>
|
|
{shouldRender && (
|
|
<InnerModal
|
|
onAnimationComplete={onAnimationComplete}
|
|
animation={animation}
|
|
setAnimation={setAnimation}
|
|
onToggle={onToggle}
|
|
isOpen={isOpen}
|
|
title={title}
|
|
subtitle={subtitle}
|
|
withActions={withActions}
|
|
hideHeader={hideHeader}
|
|
className={className}
|
|
contentClassName={contentClassName}
|
|
>
|
|
{children}
|
|
</InnerModal>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<DialogTrigger
|
|
onOpenChange={(isOpen) => {
|
|
setAnimation(
|
|
isOpen ? AnimationStateEnum.visible : AnimationStateEnum.hidden
|
|
)
|
|
onOpenChange?.(isOpen)
|
|
}}
|
|
>
|
|
{trigger}
|
|
<AnimatePresence>
|
|
{shouldRender && (
|
|
<InnerModal
|
|
onAnimationComplete={onAnimationComplete}
|
|
animation={animation}
|
|
setAnimation={setAnimation}
|
|
title={title}
|
|
subtitle={subtitle}
|
|
withActions={withActions}
|
|
className={className}
|
|
>
|
|
{children}
|
|
</InnerModal>
|
|
)}
|
|
</AnimatePresence>
|
|
</DialogTrigger>
|
|
)
|
|
}
|