diff --git a/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx b/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx index 8e4cc75e2..3e2d0ba20 100644 --- a/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx +++ b/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { DialogProps } from '@/components/elements/dialog/Dialog'; -import { Dialog } from '@/components/elements/dialog'; +import { Dialog, DialogProps } from '@/components/elements/dialog'; import { Button } from '@/components/elements/button/index'; import CopyOnClick from '@/components/elements/CopyOnClick'; import { Alert } from '@/components/elements/alert'; diff --git a/resources/scripts/components/dashboard/forms/SetupTOTPModal.tsx b/resources/scripts/components/dashboard/forms/SetupTOTPModal.tsx index d9e3da072..6aa3e4797 100644 --- a/resources/scripts/components/dashboard/forms/SetupTOTPModal.tsx +++ b/resources/scripts/components/dashboard/forms/SetupTOTPModal.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Dialog } from '@/components/elements/dialog'; -import { DialogProps } from '@/components/elements/dialog/Dialog'; +import { Dialog, DialogProps } from '@/components/elements/dialog'; import getTwoFactorTokenData, { TwoFactorTokenData } from '@/api/account/getTwoFactorTokenData'; import { useFlashKey } from '@/plugins/useFlash'; import tw from 'twin.macro'; diff --git a/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx b/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx index 65203c536..bedfe4ee5 100644 --- a/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx +++ b/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { Dialog } from '@/components/elements/dialog/index'; -import { FullDialogProps } from '@/components/elements/dialog/Dialog'; +import { Dialog, RenderDialogProps } from './'; import { Button } from '@/components/elements/button/index'; -type ConfirmationProps = Omit & { +type ConfirmationProps = Omit & { children: React.ReactNode; confirm?: string | undefined; onConfirmed: (e: React.MouseEvent) => void; diff --git a/resources/scripts/components/elements/dialog/Dialog.tsx b/resources/scripts/components/elements/dialog/Dialog.tsx index 4419b39a1..7b2b906f7 100644 --- a/resources/scripts/components/elements/dialog/Dialog.tsx +++ b/resources/scripts/components/elements/dialog/Dialog.tsx @@ -1,38 +1,37 @@ -import React, { useEffect, useState } from 'react'; +import React, { useRef, 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, { 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'; -import styles from './style.module.css'; +import { AnimatePresence, motion } from 'framer-motion'; +import { DialogContext, IconPosition, RenderDialogProps, styles } from './'; -export interface DialogProps { - open: boolean; - onClose: () => void; -} - -export interface FullDialogProps extends DialogProps { - hideCloseIcon?: boolean; - preventExternalClose?: boolean; - title?: string; - description?: string | undefined; - children?: React.ReactNode; -} - -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 }, + open: { + scale: 1, + opacity: 1, + transition: { + type: 'spring', + damping: 15, + stiffness: 300, + duration: 0.15, + }, + }, + closed: { + scale: 0.75, + opacity: 0, + transition: { + type: 'easeIn', + duration: 0.15, + }, + }, bounce: { scale: 0.95, + opacity: 1, transition: { type: 'linear', duration: 0.075 }, }, }; -const Dialog = ({ +export default ({ open, title, description, @@ -40,28 +39,25 @@ const Dialog = ({ hideCloseIcon, preventExternalClose, children, -}: FullDialogProps) => { - const controls = useAnimation(); - +}: RenderDialogProps) => { + const container = useRef(null); const [icon, setIcon] = useState(); const [footer, setFooter] = useState(); const [iconPosition, setIconPosition] = useState('title'); + const [down, setDown] = useState(false); + + const onContainerClick = (down: boolean, e: React.MouseEvent): void => { + if (e.target instanceof HTMLElement && container.current?.isSameNode(e.target)) { + setDown(down); + } + }; 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 && ( @@ -78,10 +74,17 @@ const Dialog = ({ >
-
+
@@ -125,11 +128,3 @@ const Dialog = ({ ); }; - -const _Dialog = Object.assign(Dialog, { - Confirm: ConfirmationDialog, - Footer: DialogFooter, - Icon: DialogIcon, -}); - -export default _Dialog; diff --git a/resources/scripts/components/elements/dialog/DialogFooter.tsx b/resources/scripts/components/elements/dialog/DialogFooter.tsx index 8389c9ffd..37209fce8 100644 --- a/resources/scripts/components/elements/dialog/DialogFooter.tsx +++ b/resources/scripts/components/elements/dialog/DialogFooter.tsx @@ -1,5 +1,5 @@ import React, { useContext } from 'react'; -import DialogContext from '@/components/elements/dialog/context'; +import { DialogContext } from './'; import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect'; export default ({ children }: { children: React.ReactNode }) => { diff --git a/resources/scripts/components/elements/dialog/DialogIcon.tsx b/resources/scripts/components/elements/dialog/DialogIcon.tsx index 3d5445a5f..e2ed6b207 100644 --- a/resources/scripts/components/elements/dialog/DialogIcon.tsx +++ b/resources/scripts/components/elements/dialog/DialogIcon.tsx @@ -1,16 +1,7 @@ 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 styles from './style.module.css'; - -export type IconPosition = 'title' | 'container' | undefined; - -interface Props { - type: 'danger' | 'info' | 'success' | 'warning'; - position?: IconPosition; - className?: string; -} +import { DialogContext, DialogIconProps, styles } from './'; const icons = { danger: ShieldExclamationIcon, @@ -19,7 +10,7 @@ const icons = { info: InformationCircleIcon, }; -export default ({ type, position, className }: Props) => { +export default ({ type, position, className }: DialogIconProps) => { const { setIcon, setIconPosition } = useContext(DialogContext); useEffect(() => { diff --git a/resources/scripts/components/elements/dialog/context.ts b/resources/scripts/components/elements/dialog/context.ts index 65d2a2d34..b9a0f0980 100644 --- a/resources/scripts/components/elements/dialog/context.ts +++ b/resources/scripts/components/elements/dialog/context.ts @@ -1,18 +1,14 @@ import React from 'react'; -import { IconPosition } from './DialogIcon'; +import { DialogContextType, DialogWrapperContextType } from './types'; -type Callback = ((value: T) => void) | React.Dispatch>; - -interface DialogContextType { - setIcon: Callback; - setFooter: Callback; - setIconPosition: Callback; -} - -const DialogContext = React.createContext({ +export const DialogContext = React.createContext({ setIcon: () => null, setFooter: () => null, setIconPosition: () => null, }); -export default DialogContext; +export const DialogWrapperContext = React.createContext({ + props: {}, + setProps: () => null, + close: () => null, +}); diff --git a/resources/scripts/components/elements/dialog/index.ts b/resources/scripts/components/elements/dialog/index.ts index d186b2504..89e4b42e9 100644 --- a/resources/scripts/components/elements/dialog/index.ts +++ b/resources/scripts/components/elements/dialog/index.ts @@ -1 +1,15 @@ -export { default as Dialog } from './Dialog'; +import DialogComponent from './Dialog'; +import DialogFooter from './DialogFooter'; +import DialogIcon from './DialogIcon'; +import ConfirmationDialog from './ConfirmationDialog'; + +const Dialog = Object.assign(DialogComponent, { + Confirm: ConfirmationDialog, + Footer: DialogFooter, + Icon: DialogIcon, +}); + +export { Dialog }; +export * from './types.d'; +export * from './context'; +export { default as styles } from './style.module.css'; diff --git a/resources/scripts/components/elements/dialog/types.d.ts b/resources/scripts/components/elements/dialog/types.d.ts new file mode 100644 index 000000000..f69a466d3 --- /dev/null +++ b/resources/scripts/components/elements/dialog/types.d.ts @@ -0,0 +1,38 @@ +import React from 'react'; +import { IconPosition } from '@/components/elements/dialog/DialogIcon'; + +type Callback = ((value: T) => void) | React.Dispatch>; + +export interface DialogProps { + open: boolean; + onClose: () => void; +} + +export type IconPosition = 'title' | 'container' | undefined; + +export interface DialogIconProps { + type: 'danger' | 'info' | 'success' | 'warning'; + position?: IconPosition; + className?: string; +} + +export interface RenderDialogProps extends DialogProps { + hideCloseIcon?: boolean; + preventExternalClose?: boolean; + title?: string; + description?: string | undefined; + children?: React.ReactNode; +} + +export type WrapperProps = Omit; +export interface DialogWrapperContextType { + props: Readonly; + setProps: Callback>; + close: () => void; +} + +export interface DialogContextType { + setIcon: Callback; + setFooter: Callback; + setIconPosition: Callback; +} diff --git a/resources/scripts/hoc/asDialog.tsx b/resources/scripts/hoc/asDialog.tsx new file mode 100644 index 000000000..0be5dbe65 --- /dev/null +++ b/resources/scripts/hoc/asDialog.tsx @@ -0,0 +1,23 @@ +import React, { useState } from 'react'; +import { Dialog, DialogProps, DialogWrapperContext, WrapperProps } from '@/components/elements/dialog'; + +function asDialog( + initialProps?: WrapperProps + // eslint-disable-next-line @typescript-eslint/ban-types +):

(C: React.ComponentType

) => React.FunctionComponent

{ + return function (Component) { + return function ({ open, onClose, ...rest }) { + const [props, setProps] = useState(initialProps || {}); + + return ( + +

+ )} /> + + + ); + }; + }; +} + +export default asDialog;