Better dialog setting logic
This commit is contained in:
parent
7a6440988b
commit
e49e6ee802
4 changed files with 87 additions and 43 deletions
|
@ -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'}>
|
||||||
|
|
|
@ -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(
|
||||||
}
|
|
||||||
|
|
||||||
const element = (
|
|
||||||
<div className={'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'}>{children}</div>
|
<div className={'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'}>{children}</div>
|
||||||
);
|
);
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
return createPortal(element, buttons.current);
|
return null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue