Skip to content

Commit e49e6ee

Browse files
committed
Better dialog setting logic
1 parent 7a64409 commit e49e6ee

File tree

4 files changed

+87
-43
lines changed

4 files changed

+87
-43
lines changed

resources/scripts/components/elements/dialog/Dialog.tsx

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import React, { useRef } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import { Dialog as HDialog } from '@headlessui/react';
33
import { Button } from '@/components/elements/button/index';
44
import { XIcon } from '@heroicons/react/solid';
5-
import DialogIcon from '@/components/elements/dialog/DialogIcon';
6-
import { AnimatePresence, motion } from 'framer-motion';
5+
import DialogIcon, { IconPosition } from '@/components/elements/dialog/DialogIcon';
6+
import { AnimatePresence, motion, useAnimation } from 'framer-motion';
77
import ConfirmationDialog from '@/components/elements/dialog/ConfirmationDialog';
88
import DialogContext from './context';
99
import DialogFooter from '@/components/elements/dialog/DialogFooter';
@@ -16,20 +16,56 @@ export interface DialogProps {
1616

1717
export interface FullDialogProps extends DialogProps {
1818
hideCloseIcon?: boolean;
19+
preventExternalClose?: boolean;
1920
title?: string;
2021
description?: string | undefined;
2122
children?: React.ReactNode;
2223
}
2324

24-
const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }: FullDialogProps) => {
25-
const ref = useRef<HTMLDivElement>(null);
26-
const icon = useRef<HTMLDivElement>(null);
27-
const buttons = useRef<HTMLDivElement>(null);
25+
const spring = { type: 'spring', damping: 15, stiffness: 300, duration: 0.15 };
26+
const variants = {
27+
open: { opacity: 1, scale: 1, transition: spring },
28+
closed: { opacity: 0, scale: 0.85, transition: spring },
29+
bounce: {
30+
scale: 0.95,
31+
transition: { type: 'linear', duration: 0.075 },
32+
},
33+
};
34+
35+
const Dialog = ({
36+
open,
37+
title,
38+
description,
39+
onClose,
40+
hideCloseIcon,
41+
preventExternalClose,
42+
children,
43+
}: FullDialogProps) => {
44+
const controls = useAnimation();
45+
46+
const [icon, setIcon] = useState<React.ReactNode>();
47+
const [footer, setFooter] = useState<React.ReactNode>();
48+
const [iconPosition, setIconPosition] = useState<IconPosition>('title');
49+
50+
const onDialogClose = (): void => {
51+
if (!preventExternalClose) {
52+
return onClose();
53+
}
54+
55+
controls
56+
.start('bounce')
57+
.then(() => controls.start('open'))
58+
.catch(console.error);
59+
};
60+
61+
useEffect(() => {
62+
controls.start(open ? 'open' : 'closed').catch(console.error);
63+
}, [open]);
2864

2965
return (
3066
<AnimatePresence>
3167
{open && (
32-
<DialogContext.Provider value={{ icon, buttons }}>
68+
<DialogContext.Provider value={{ setIcon, setFooter, setIconPosition }}>
3369
<HDialog
3470
static
3571
as={motion.div}
@@ -38,23 +74,22 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
3874
exit={{ opacity: 0 }}
3975
transition={{ duration: 0.15 }}
4076
open={open}
41-
onClose={onClose}
77+
onClose={onDialogClose}
4278
>
4379
<div className={'fixed inset-0 bg-gray-900/50 z-40'} />
4480
<div className={'fixed inset-0 overflow-y-auto z-50'}>
4581
<div className={styles.container}>
4682
<HDialog.Panel
4783
as={motion.div}
48-
initial={{ opacity: 0, scale: 0.85 }}
49-
animate={{ opacity: 1, scale: 1 }}
50-
exit={{ opacity: 0 }}
51-
transition={{ type: 'spring', damping: 15, stiffness: 300, duration: 0.15 }}
84+
animate={controls}
85+
variants={variants}
5286
className={styles.panel}
5387
>
5488
<div className={'flex p-6 overflow-y-auto'}>
55-
<div ref={ref} className={'flex-1 max-h-[70vh]'}>
89+
{iconPosition === 'container' && icon}
90+
<div className={'flex-1 max-h-[70vh]'}>
5691
<div className={'flex items-center'}>
57-
<div ref={icon} />
92+
{iconPosition !== 'container' && icon}
5893
<div>
5994
{title && (
6095
<HDialog.Title className={styles.title}>{title}</HDialog.Title>
@@ -67,7 +102,7 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
67102
{children}
68103
</div>
69104
</div>
70-
<div ref={buttons} />
105+
{footer}
71106
{/* Keep this below the other buttons so that it isn't the default focus if they're present. */}
72107
{!hideCloseIcon && (
73108
<div className={'absolute right-0 top-0 m-4'}>
Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import React, { useContext } from 'react';
2-
import { createPortal } from 'react-dom';
32
import DialogContext from '@/components/elements/dialog/context';
3+
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
44

55
export default ({ children }: { children: React.ReactNode }) => {
6-
const { buttons } = useContext(DialogContext);
6+
const { setFooter } = useContext(DialogContext);
77

8-
if (!buttons.current) {
9-
return null;
10-
}
8+
useDeepCompareEffect(() => {
9+
setFooter(
10+
<div className={'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'}>{children}</div>
11+
);
12+
}, [children]);
1113

12-
const element = (
13-
<div className={'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'}>{children}</div>
14-
);
15-
16-
return createPortal(element, buttons.current);
14+
return null;
1715
};
Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import React, { useContext } from 'react';
1+
import React, { useContext, useEffect } from 'react';
22
import { CheckIcon, ExclamationIcon, InformationCircleIcon, ShieldExclamationIcon } from '@heroicons/react/outline';
33
import classNames from 'classnames';
44
import DialogContext from '@/components/elements/dialog/context';
5-
import { createPortal } from 'react-dom';
65
import styles from './style.module.css';
76

7+
export type IconPosition = 'title' | 'container' | undefined;
8+
89
interface Props {
910
type: 'danger' | 'info' | 'success' | 'warning';
11+
position?: IconPosition;
1012
className?: string;
1113
}
1214

@@ -17,18 +19,22 @@ const icons = {
1719
info: InformationCircleIcon,
1820
};
1921

20-
export default ({ type, className }: Props) => {
21-
const { icon } = useContext(DialogContext);
22+
export default ({ type, position, className }: Props) => {
23+
const { setIcon, setIconPosition } = useContext(DialogContext);
24+
25+
useEffect(() => {
26+
const Icon = icons[type];
2227

23-
if (!icon.current) {
24-
return null;
25-
}
28+
setIcon(
29+
<div className={classNames(styles.dialog_icon, styles[type], className)}>
30+
<Icon className={'w-6 h-6'} />
31+
</div>
32+
);
33+
}, [type, className]);
2634

27-
const element = (
28-
<div className={classNames(styles.dialog_icon, styles[type], className)}>
29-
{React.createElement(icons[type], { className: 'w-6 h-6' })}
30-
</div>
31-
);
35+
useEffect(() => {
36+
setIconPosition(position);
37+
}, [position]);
3238

33-
return createPortal(element, icon.current);
39+
return null;
3440
};
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import React from 'react';
2+
import { IconPosition } from './DialogIcon';
3+
4+
type Callback<T> = ((value: T) => void) | React.Dispatch<React.SetStateAction<T>>;
25

36
interface DialogContextType {
4-
icon: React.RefObject<HTMLDivElement | undefined>;
5-
buttons: React.RefObject<HTMLDivElement | undefined>;
7+
setIcon: Callback<React.ReactNode>;
8+
setFooter: Callback<React.ReactNode>;
9+
setIconPosition: Callback<IconPosition>;
610
}
711

812
const DialogContext = React.createContext<DialogContextType>({
9-
icon: React.createRef(),
10-
buttons: React.createRef(),
13+
setIcon: () => null,
14+
setFooter: () => null,
15+
setIconPosition: () => null,
1116
});
1217

1318
export default DialogContext;

0 commit comments

Comments
 (0)