Better dialog setting logic

This commit is contained in:
DaneEveritt 2022-07-02 18:27:22 -04:00
parent 7a6440988b
commit e49e6ee802
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
4 changed files with 87 additions and 43 deletions

View file

@ -1,9 +1,9 @@
import React, { useRef } from 'react'; import React, { useEffect, useState } from 'react';
import { Dialog as HDialog } from '@headlessui/react'; import { Dialog as HDialog } from '@headlessui/react';
import { Button } from '@/components/elements/button/index'; import { Button } from '@/components/elements/button/index';
import { XIcon } from '@heroicons/react/solid'; import { XIcon } from '@heroicons/react/solid';
import DialogIcon from '@/components/elements/dialog/DialogIcon'; import DialogIcon, { IconPosition } from '@/components/elements/dialog/DialogIcon';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion, useAnimation } from 'framer-motion';
import ConfirmationDialog from '@/components/elements/dialog/ConfirmationDialog'; import ConfirmationDialog from '@/components/elements/dialog/ConfirmationDialog';
import DialogContext from './context'; import DialogContext from './context';
import DialogFooter from '@/components/elements/dialog/DialogFooter'; import DialogFooter from '@/components/elements/dialog/DialogFooter';
@ -16,20 +16,56 @@ export interface DialogProps {
export interface FullDialogProps extends DialogProps { export interface FullDialogProps extends DialogProps {
hideCloseIcon?: boolean; hideCloseIcon?: boolean;
preventExternalClose?: boolean;
title?: string; title?: string;
description?: string | undefined; description?: string | undefined;
children?: React.ReactNode; children?: React.ReactNode;
} }
const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }: FullDialogProps) => { const spring = { type: 'spring', damping: 15, stiffness: 300, duration: 0.15 };
const ref = useRef<HTMLDivElement>(null); const variants = {
const icon = useRef<HTMLDivElement>(null); open: { opacity: 1, scale: 1, transition: spring },
const buttons = useRef<HTMLDivElement>(null); 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<React.ReactNode>();
const [footer, setFooter] = useState<React.ReactNode>();
const [iconPosition, setIconPosition] = useState<IconPosition>('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 ( return (
<AnimatePresence> <AnimatePresence>
{open && ( {open && (
<DialogContext.Provider value={{ icon, buttons }}> <DialogContext.Provider value={{ setIcon, setFooter, setIconPosition }}>
<HDialog <HDialog
static static
as={motion.div} as={motion.div}
@ -38,23 +74,22 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
open={open} open={open}
onClose={onClose} onClose={onDialogClose}
> >
<div className={'fixed inset-0 bg-gray-900/50 z-40'} /> <div className={'fixed inset-0 bg-gray-900/50 z-40'} />
<div className={'fixed inset-0 overflow-y-auto z-50'}> <div className={'fixed inset-0 overflow-y-auto z-50'}>
<div className={styles.container}> <div className={styles.container}>
<HDialog.Panel <HDialog.Panel
as={motion.div} as={motion.div}
initial={{ opacity: 0, scale: 0.85 }} animate={controls}
animate={{ opacity: 1, scale: 1 }} variants={variants}
exit={{ opacity: 0 }}
transition={{ type: 'spring', damping: 15, stiffness: 300, duration: 0.15 }}
className={styles.panel} className={styles.panel}
> >
<div className={'flex p-6 overflow-y-auto'}> <div className={'flex p-6 overflow-y-auto'}>
<div ref={ref} className={'flex-1 max-h-[70vh]'}> {iconPosition === 'container' && icon}
<div className={'flex-1 max-h-[70vh]'}>
<div className={'flex items-center'}> <div className={'flex items-center'}>
<div ref={icon} /> {iconPosition !== 'container' && icon}
<div> <div>
{title && ( {title && (
<HDialog.Title className={styles.title}>{title}</HDialog.Title> <HDialog.Title className={styles.title}>{title}</HDialog.Title>
@ -67,7 +102,7 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
{children} {children}
</div> </div>
</div> </div>
<div ref={buttons} /> {footer}
{/* Keep this below the other buttons so that it isn't the default focus if they're present. */} {/* Keep this below the other buttons so that it isn't the default focus if they're present. */}
{!hideCloseIcon && ( {!hideCloseIcon && (
<div className={'absolute right-0 top-0 m-4'}> <div className={'absolute right-0 top-0 m-4'}>

View file

@ -1,17 +1,15 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { createPortal } from 'react-dom';
import DialogContext from '@/components/elements/dialog/context'; import DialogContext from '@/components/elements/dialog/context';
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
export default ({ children }: { children: React.ReactNode }) => { export default ({ children }: { children: React.ReactNode }) => {
const { buttons } = useContext(DialogContext); const { setFooter } = useContext(DialogContext);
if (!buttons.current) { useDeepCompareEffect(() => {
return null; setFooter(
} <div className={'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'}>{children}</div>
);
}, [children]);
const element = ( return null;
<div className={'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'}>{children}</div>
);
return createPortal(element, buttons.current);
}; };

View file

@ -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 { CheckIcon, ExclamationIcon, InformationCircleIcon, ShieldExclamationIcon } from '@heroicons/react/outline';
import classNames from 'classnames'; import classNames from 'classnames';
import DialogContext from '@/components/elements/dialog/context'; import DialogContext from '@/components/elements/dialog/context';
import { createPortal } from 'react-dom';
import styles from './style.module.css'; import styles from './style.module.css';
export type IconPosition = 'title' | 'container' | undefined;
interface Props { interface Props {
type: 'danger' | 'info' | 'success' | 'warning'; type: 'danger' | 'info' | 'success' | 'warning';
position?: IconPosition;
className?: string; className?: string;
} }
@ -17,18 +19,22 @@ const icons = {
info: InformationCircleIcon, info: InformationCircleIcon,
}; };
export default ({ type, className }: Props) => { export default ({ type, position, className }: Props) => {
const { icon } = useContext(DialogContext); const { setIcon, setIconPosition } = useContext(DialogContext);
if (!icon.current) { useEffect(() => {
return null; const Icon = icons[type];
}
const element = ( setIcon(
<div className={classNames(styles.dialog_icon, styles[type], className)}> <div className={classNames(styles.dialog_icon, styles[type], className)}>
{React.createElement(icons[type], { className: 'w-6 h-6' })} <Icon className={'w-6 h-6'} />
</div> </div>
); );
}, [type, className]);
return createPortal(element, icon.current); useEffect(() => {
setIconPosition(position);
}, [position]);
return null;
}; };

View file

@ -1,13 +1,18 @@
import React from 'react'; import React from 'react';
import { IconPosition } from './DialogIcon';
type Callback<T> = ((value: T) => void) | React.Dispatch<React.SetStateAction<T>>;
interface DialogContextType { interface DialogContextType {
icon: React.RefObject<HTMLDivElement | undefined>; setIcon: Callback<React.ReactNode>;
buttons: React.RefObject<HTMLDivElement | undefined>; setFooter: Callback<React.ReactNode>;
setIconPosition: Callback<IconPosition>;
} }
const DialogContext = React.createContext<DialogContextType>({ const DialogContext = React.createContext<DialogContextType>({
icon: React.createRef(), setIcon: () => null,
buttons: React.createRef(), setFooter: () => null,
setIconPosition: () => null,
}); });
export default DialogContext; export default DialogContext;