Skip to content

Commit a4feed2

Browse files
committed
Improve dialog logic, add "asDialog" helper
1 parent 8229494 commit a4feed2

File tree

10 files changed

+131
-77
lines changed

10 files changed

+131
-77
lines changed

resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React from 'react';
2-
import { DialogProps } from '@/components/elements/dialog/Dialog';
3-
import { Dialog } from '@/components/elements/dialog';
2+
import { Dialog, DialogProps } from '@/components/elements/dialog';
43
import { Button } from '@/components/elements/button/index';
54
import CopyOnClick from '@/components/elements/CopyOnClick';
65
import { Alert } from '@/components/elements/alert';

resources/scripts/components/dashboard/forms/SetupTOTPModal.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React, { useEffect, useState } from 'react';
2-
import { Dialog } from '@/components/elements/dialog';
3-
import { DialogProps } from '@/components/elements/dialog/Dialog';
2+
import { Dialog, DialogProps } from '@/components/elements/dialog';
43
import getTwoFactorTokenData, { TwoFactorTokenData } from '@/api/account/getTwoFactorTokenData';
54
import { useFlashKey } from '@/plugins/useFlash';
65
import tw from 'twin.macro';

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import React from 'react';
2-
import { Dialog } from '@/components/elements/dialog/index';
3-
import { FullDialogProps } from '@/components/elements/dialog/Dialog';
2+
import { Dialog, RenderDialogProps } from './';
43
import { Button } from '@/components/elements/button/index';
54

6-
type ConfirmationProps = Omit<FullDialogProps, 'description' | 'children'> & {
5+
type ConfirmationProps = Omit<RenderDialogProps, 'description' | 'children'> & {
76
children: React.ReactNode;
87
confirm?: string | undefined;
98
onConfirmed: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,63 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React, { useRef, 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, { IconPosition } from '@/components/elements/dialog/DialogIcon';
6-
import { AnimatePresence, motion, useAnimation } from 'framer-motion';
7-
import ConfirmationDialog from '@/components/elements/dialog/ConfirmationDialog';
8-
import DialogContext from './context';
9-
import DialogFooter from '@/components/elements/dialog/DialogFooter';
10-
import styles from './style.module.css';
5+
import { AnimatePresence, motion } from 'framer-motion';
6+
import { DialogContext, IconPosition, RenderDialogProps, styles } from './';
117

12-
export interface DialogProps {
13-
open: boolean;
14-
onClose: () => void;
15-
}
16-
17-
export interface FullDialogProps extends DialogProps {
18-
hideCloseIcon?: boolean;
19-
preventExternalClose?: boolean;
20-
title?: string;
21-
description?: string | undefined;
22-
children?: React.ReactNode;
23-
}
24-
25-
const spring = { type: 'spring', damping: 15, stiffness: 300, duration: 0.15 };
268
const variants = {
27-
open: { opacity: 1, scale: 1, transition: spring },
28-
closed: { opacity: 0, scale: 0.85, transition: spring },
9+
open: {
10+
scale: 1,
11+
opacity: 1,
12+
transition: {
13+
type: 'spring',
14+
damping: 15,
15+
stiffness: 300,
16+
duration: 0.15,
17+
},
18+
},
19+
closed: {
20+
scale: 0.75,
21+
opacity: 0,
22+
transition: {
23+
type: 'easeIn',
24+
duration: 0.15,
25+
},
26+
},
2927
bounce: {
3028
scale: 0.95,
29+
opacity: 1,
3130
transition: { type: 'linear', duration: 0.075 },
3231
},
3332
};
3433

35-
const Dialog = ({
34+
export default ({
3635
open,
3736
title,
3837
description,
3938
onClose,
4039
hideCloseIcon,
4140
preventExternalClose,
4241
children,
43-
}: FullDialogProps) => {
44-
const controls = useAnimation();
45-
42+
}: RenderDialogProps) => {
43+
const container = useRef<HTMLDivElement>(null);
4644
const [icon, setIcon] = useState<React.ReactNode>();
4745
const [footer, setFooter] = useState<React.ReactNode>();
4846
const [iconPosition, setIconPosition] = useState<IconPosition>('title');
47+
const [down, setDown] = useState(false);
48+
49+
const onContainerClick = (down: boolean, e: React.MouseEvent<HTMLDivElement>): void => {
50+
if (e.target instanceof HTMLElement && container.current?.isSameNode(e.target)) {
51+
setDown(down);
52+
}
53+
};
4954

5055
const onDialogClose = (): void => {
5156
if (!preventExternalClose) {
5257
return onClose();
5358
}
54-
55-
controls
56-
.start('bounce')
57-
.then(() => controls.start('open'))
58-
.catch(console.error);
5959
};
6060

61-
useEffect(() => {
62-
controls.start(open ? 'open' : 'closed').catch(console.error);
63-
}, [open]);
64-
6561
return (
6662
<AnimatePresence>
6763
{open && (
@@ -78,10 +74,17 @@ const Dialog = ({
7874
>
7975
<div className={'fixed inset-0 bg-gray-900/50 z-40'} />
8076
<div className={'fixed inset-0 overflow-y-auto z-50'}>
81-
<div className={styles.container}>
77+
<div
78+
ref={container}
79+
className={styles.container}
80+
onMouseDown={onContainerClick.bind(this, true)}
81+
onMouseUp={onContainerClick.bind(this, false)}
82+
>
8283
<HDialog.Panel
8384
as={motion.div}
84-
animate={controls}
85+
initial={'closed'}
86+
animate={down ? 'bounce' : 'open'}
87+
exit={'closed'}
8588
variants={variants}
8689
className={styles.panel}
8790
>
@@ -125,11 +128,3 @@ const Dialog = ({
125128
</AnimatePresence>
126129
);
127130
};
128-
129-
const _Dialog = Object.assign(Dialog, {
130-
Confirm: ConfirmationDialog,
131-
Footer: DialogFooter,
132-
Icon: DialogIcon,
133-
});
134-
135-
export default _Dialog;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useContext } from 'react';
2-
import DialogContext from '@/components/elements/dialog/context';
2+
import { DialogContext } from './';
33
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
44

55
export default ({ children }: { children: React.ReactNode }) => {

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

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
11
import React, { useContext, useEffect } from 'react';
22
import { CheckIcon, ExclamationIcon, InformationCircleIcon, ShieldExclamationIcon } from '@heroicons/react/outline';
33
import classNames from 'classnames';
4-
import DialogContext from '@/components/elements/dialog/context';
5-
import styles from './style.module.css';
6-
7-
export type IconPosition = 'title' | 'container' | undefined;
8-
9-
interface Props {
10-
type: 'danger' | 'info' | 'success' | 'warning';
11-
position?: IconPosition;
12-
className?: string;
13-
}
4+
import { DialogContext, DialogIconProps, styles } from './';
145

156
const icons = {
167
danger: ShieldExclamationIcon,
@@ -19,7 +10,7 @@ const icons = {
1910
info: InformationCircleIcon,
2011
};
2112

22-
export default ({ type, position, className }: Props) => {
13+
export default ({ type, position, className }: DialogIconProps) => {
2314
const { setIcon, setIconPosition } = useContext(DialogContext);
2415

2516
useEffect(() => {
Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
import React from 'react';
2-
import { IconPosition } from './DialogIcon';
2+
import { DialogContextType, DialogWrapperContextType } from './types';
33

4-
type Callback<T> = ((value: T) => void) | React.Dispatch<React.SetStateAction<T>>;
5-
6-
interface DialogContextType {
7-
setIcon: Callback<React.ReactNode>;
8-
setFooter: Callback<React.ReactNode>;
9-
setIconPosition: Callback<IconPosition>;
10-
}
11-
12-
const DialogContext = React.createContext<DialogContextType>({
4+
export const DialogContext = React.createContext<DialogContextType>({
135
setIcon: () => null,
146
setFooter: () => null,
157
setIconPosition: () => null,
168
});
179

18-
export default DialogContext;
10+
export const DialogWrapperContext = React.createContext<DialogWrapperContextType>({
11+
props: {},
12+
setProps: () => null,
13+
close: () => null,
14+
});
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,15 @@
1-
export { default as Dialog } from './Dialog';
1+
import DialogComponent from './Dialog';
2+
import DialogFooter from './DialogFooter';
3+
import DialogIcon from './DialogIcon';
4+
import ConfirmationDialog from './ConfirmationDialog';
5+
6+
const Dialog = Object.assign(DialogComponent, {
7+
Confirm: ConfirmationDialog,
8+
Footer: DialogFooter,
9+
Icon: DialogIcon,
10+
});
11+
12+
export { Dialog };
13+
export * from './types.d';
14+
export * from './context';
15+
export { default as styles } from './style.module.css';
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react';
2+
import { IconPosition } from '@/components/elements/dialog/DialogIcon';
3+
4+
type Callback<T> = ((value: T) => void) | React.Dispatch<React.SetStateAction<T>>;
5+
6+
export interface DialogProps {
7+
open: boolean;
8+
onClose: () => void;
9+
}
10+
11+
export type IconPosition = 'title' | 'container' | undefined;
12+
13+
export interface DialogIconProps {
14+
type: 'danger' | 'info' | 'success' | 'warning';
15+
position?: IconPosition;
16+
className?: string;
17+
}
18+
19+
export interface RenderDialogProps extends DialogProps {
20+
hideCloseIcon?: boolean;
21+
preventExternalClose?: boolean;
22+
title?: string;
23+
description?: string | undefined;
24+
children?: React.ReactNode;
25+
}
26+
27+
export type WrapperProps = Omit<RenderDialogProps, 'children' | 'open' | 'onClose'>;
28+
export interface DialogWrapperContextType {
29+
props: Readonly<WrapperProps>;
30+
setProps: Callback<Partial<WrapperProps>>;
31+
close: () => void;
32+
}
33+
34+
export interface DialogContextType {
35+
setIcon: Callback<React.ReactNode>;
36+
setFooter: Callback<React.ReactNode>;
37+
setIconPosition: Callback<IconPosition>;
38+
}

resources/scripts/hoc/asDialog.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React, { useState } from 'react';
2+
import { Dialog, DialogProps, DialogWrapperContext, WrapperProps } from '@/components/elements/dialog';
3+
4+
function asDialog(
5+
initialProps?: WrapperProps
6+
// eslint-disable-next-line @typescript-eslint/ban-types
7+
): <P extends {}>(C: React.ComponentType<P>) => React.FunctionComponent<P & DialogProps> {
8+
return function (Component) {
9+
return function ({ open, onClose, ...rest }) {
10+
const [props, setProps] = useState<WrapperProps>(initialProps || {});
11+
12+
return (
13+
<DialogWrapperContext.Provider value={{ props, setProps, close: onClose }}>
14+
<Dialog {...props} open={open} onClose={onClose}>
15+
<Component {...(rest as React.ComponentProps<typeof Component>)} />
16+
</Dialog>
17+
</DialogWrapperContext.Provider>
18+
);
19+
};
20+
};
21+
}
22+
23+
export default asDialog;

0 commit comments

Comments
 (0)