From e49e6ee80237e6d747064ffac6ba07397ba84b04 Mon Sep 17 00:00:00 2001 From: DaneEveritt Date: Sat, 2 Jul 2022 18:27:22 -0400 Subject: [PATCH] Better dialog setting logic --- .../components/elements/dialog/Dialog.tsx | 67 ++++++++++++++----- .../elements/dialog/DialogFooter.tsx | 18 +++-- .../components/elements/dialog/DialogIcon.tsx | 32 +++++---- .../components/elements/dialog/context.ts | 13 ++-- 4 files changed, 87 insertions(+), 43 deletions(-) diff --git a/resources/scripts/components/elements/dialog/Dialog.tsx b/resources/scripts/components/elements/dialog/Dialog.tsx index e6f464d52..4419b39a1 100644 --- a/resources/scripts/components/elements/dialog/Dialog.tsx +++ b/resources/scripts/components/elements/dialog/Dialog.tsx @@ -1,9 +1,9 @@ -import React, { useRef } from 'react'; +import React, { useEffect, useState } from 'react'; import { Dialog as HDialog } from '@headlessui/react'; import { Button } from '@/components/elements/button/index'; import { XIcon } from '@heroicons/react/solid'; -import DialogIcon from '@/components/elements/dialog/DialogIcon'; -import { AnimatePresence, motion } from 'framer-motion'; +import DialogIcon, { IconPosition } from '@/components/elements/dialog/DialogIcon'; +import { AnimatePresence, motion, useAnimation } from 'framer-motion'; import ConfirmationDialog from '@/components/elements/dialog/ConfirmationDialog'; import DialogContext from './context'; import DialogFooter from '@/components/elements/dialog/DialogFooter'; @@ -16,20 +16,56 @@ export interface DialogProps { export interface FullDialogProps extends DialogProps { hideCloseIcon?: boolean; + preventExternalClose?: boolean; title?: string; description?: string | undefined; children?: React.ReactNode; } -const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }: FullDialogProps) => { - const ref = useRef(null); - const icon = useRef(null); - const buttons = useRef(null); +const spring = { type: 'spring', damping: 15, stiffness: 300, duration: 0.15 }; +const variants = { + open: { opacity: 1, scale: 1, transition: spring }, + closed: { opacity: 0, scale: 0.85, transition: spring }, + bounce: { + scale: 0.95, + transition: { type: 'linear', duration: 0.075 }, + }, +}; + +const Dialog = ({ + open, + title, + description, + onClose, + hideCloseIcon, + preventExternalClose, + children, +}: FullDialogProps) => { + const controls = useAnimation(); + + const [icon, setIcon] = useState(); + const [footer, setFooter] = useState(); + const [iconPosition, setIconPosition] = useState('title'); + + const onDialogClose = (): void => { + if (!preventExternalClose) { + return onClose(); + } + + controls + .start('bounce') + .then(() => controls.start('open')) + .catch(console.error); + }; + + useEffect(() => { + controls.start(open ? 'open' : 'closed').catch(console.error); + }, [open]); return ( {open && ( - +
-
+ {iconPosition === 'container' && icon} +
-
+ {iconPosition !== 'container' && icon}
{title && ( {title} @@ -67,7 +102,7 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }: {children}
-
+ {footer} {/* Keep this below the other buttons so that it isn't the default focus if they're present. */} {!hideCloseIcon && (
diff --git a/resources/scripts/components/elements/dialog/DialogFooter.tsx b/resources/scripts/components/elements/dialog/DialogFooter.tsx index 17357b0f4..8389c9ffd 100644 --- a/resources/scripts/components/elements/dialog/DialogFooter.tsx +++ b/resources/scripts/components/elements/dialog/DialogFooter.tsx @@ -1,17 +1,15 @@ import React, { useContext } from 'react'; -import { createPortal } from 'react-dom'; import DialogContext from '@/components/elements/dialog/context'; +import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect'; export default ({ children }: { children: React.ReactNode }) => { - const { buttons } = useContext(DialogContext); + const { setFooter } = useContext(DialogContext); - if (!buttons.current) { - return null; - } + useDeepCompareEffect(() => { + setFooter( +
{children}
+ ); + }, [children]); - const element = ( -
{children}
- ); - - return createPortal(element, buttons.current); + return null; }; diff --git a/resources/scripts/components/elements/dialog/DialogIcon.tsx b/resources/scripts/components/elements/dialog/DialogIcon.tsx index 64ac18f7c..3d5445a5f 100644 --- a/resources/scripts/components/elements/dialog/DialogIcon.tsx +++ b/resources/scripts/components/elements/dialog/DialogIcon.tsx @@ -1,12 +1,14 @@ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { CheckIcon, ExclamationIcon, InformationCircleIcon, ShieldExclamationIcon } from '@heroicons/react/outline'; import classNames from 'classnames'; import DialogContext from '@/components/elements/dialog/context'; -import { createPortal } from 'react-dom'; import styles from './style.module.css'; +export type IconPosition = 'title' | 'container' | undefined; + interface Props { type: 'danger' | 'info' | 'success' | 'warning'; + position?: IconPosition; className?: string; } @@ -17,18 +19,22 @@ const icons = { info: InformationCircleIcon, }; -export default ({ type, className }: Props) => { - const { icon } = useContext(DialogContext); +export default ({ type, position, className }: Props) => { + const { setIcon, setIconPosition } = useContext(DialogContext); - if (!icon.current) { - return null; - } + useEffect(() => { + const Icon = icons[type]; - const element = ( -
- {React.createElement(icons[type], { className: 'w-6 h-6' })} -
- ); + setIcon( +
+ +
+ ); + }, [type, className]); - return createPortal(element, icon.current); + useEffect(() => { + setIconPosition(position); + }, [position]); + + return null; }; diff --git a/resources/scripts/components/elements/dialog/context.ts b/resources/scripts/components/elements/dialog/context.ts index 73f6020f4..65d2a2d34 100644 --- a/resources/scripts/components/elements/dialog/context.ts +++ b/resources/scripts/components/elements/dialog/context.ts @@ -1,13 +1,18 @@ import React from 'react'; +import { IconPosition } from './DialogIcon'; + +type Callback = ((value: T) => void) | React.Dispatch>; interface DialogContextType { - icon: React.RefObject; - buttons: React.RefObject; + setIcon: Callback; + setFooter: Callback; + setIconPosition: Callback; } const DialogContext = React.createContext({ - icon: React.createRef(), - buttons: React.createRef(), + setIcon: () => null, + setFooter: () => null, + setIconPosition: () => null, }); export default DialogContext;