Improve dialog logic, add "asDialog" helper
This commit is contained in:
parent
822949408f
commit
a4feed24a8
10 changed files with 131 additions and 77 deletions
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DialogProps } from '@/components/elements/dialog/Dialog';
|
import { Dialog, DialogProps } from '@/components/elements/dialog';
|
||||||
import { Dialog } from '@/components/elements/dialog';
|
|
||||||
import { Button } from '@/components/elements/button/index';
|
import { Button } from '@/components/elements/button/index';
|
||||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||||
import { Alert } from '@/components/elements/alert';
|
import { Alert } from '@/components/elements/alert';
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Dialog } from '@/components/elements/dialog';
|
import { Dialog, DialogProps } from '@/components/elements/dialog';
|
||||||
import { DialogProps } from '@/components/elements/dialog/Dialog';
|
|
||||||
import getTwoFactorTokenData, { TwoFactorTokenData } from '@/api/account/getTwoFactorTokenData';
|
import getTwoFactorTokenData, { TwoFactorTokenData } from '@/api/account/getTwoFactorTokenData';
|
||||||
import { useFlashKey } from '@/plugins/useFlash';
|
import { useFlashKey } from '@/plugins/useFlash';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Dialog } from '@/components/elements/dialog/index';
|
import { Dialog, RenderDialogProps } from './';
|
||||||
import { FullDialogProps } from '@/components/elements/dialog/Dialog';
|
|
||||||
import { Button } from '@/components/elements/button/index';
|
import { Button } from '@/components/elements/button/index';
|
||||||
|
|
||||||
type ConfirmationProps = Omit<FullDialogProps, 'description' | 'children'> & {
|
type ConfirmationProps = Omit<RenderDialogProps, 'description' | 'children'> & {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
confirm?: string | undefined;
|
confirm?: string | undefined;
|
||||||
onConfirmed: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onConfirmed: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
|
|
|
@ -1,38 +1,37 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useRef, 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, { IconPosition } from '@/components/elements/dialog/DialogIcon';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { AnimatePresence, motion, useAnimation } from 'framer-motion';
|
import { DialogContext, IconPosition, RenderDialogProps, styles } from './';
|
||||||
import ConfirmationDialog from '@/components/elements/dialog/ConfirmationDialog';
|
|
||||||
import DialogContext from './context';
|
|
||||||
import DialogFooter from '@/components/elements/dialog/DialogFooter';
|
|
||||||
import styles from './style.module.css';
|
|
||||||
|
|
||||||
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 = {
|
const variants = {
|
||||||
open: { opacity: 1, scale: 1, transition: spring },
|
open: {
|
||||||
closed: { opacity: 0, scale: 0.85, transition: spring },
|
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: {
|
bounce: {
|
||||||
scale: 0.95,
|
scale: 0.95,
|
||||||
|
opacity: 1,
|
||||||
transition: { type: 'linear', duration: 0.075 },
|
transition: { type: 'linear', duration: 0.075 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Dialog = ({
|
export default ({
|
||||||
open,
|
open,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
@ -40,28 +39,25 @@ const Dialog = ({
|
||||||
hideCloseIcon,
|
hideCloseIcon,
|
||||||
preventExternalClose,
|
preventExternalClose,
|
||||||
children,
|
children,
|
||||||
}: FullDialogProps) => {
|
}: RenderDialogProps) => {
|
||||||
const controls = useAnimation();
|
const container = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [icon, setIcon] = useState<React.ReactNode>();
|
const [icon, setIcon] = useState<React.ReactNode>();
|
||||||
const [footer, setFooter] = useState<React.ReactNode>();
|
const [footer, setFooter] = useState<React.ReactNode>();
|
||||||
const [iconPosition, setIconPosition] = useState<IconPosition>('title');
|
const [iconPosition, setIconPosition] = useState<IconPosition>('title');
|
||||||
|
const [down, setDown] = useState(false);
|
||||||
|
|
||||||
|
const onContainerClick = (down: boolean, e: React.MouseEvent<HTMLDivElement>): void => {
|
||||||
|
if (e.target instanceof HTMLElement && container.current?.isSameNode(e.target)) {
|
||||||
|
setDown(down);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onDialogClose = (): void => {
|
const onDialogClose = (): void => {
|
||||||
if (!preventExternalClose) {
|
if (!preventExternalClose) {
|
||||||
return onClose();
|
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 && (
|
||||||
|
@ -78,10 +74,17 @@ const Dialog = ({
|
||||||
>
|
>
|
||||||
<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
|
||||||
|
ref={container}
|
||||||
|
className={styles.container}
|
||||||
|
onMouseDown={onContainerClick.bind(this, true)}
|
||||||
|
onMouseUp={onContainerClick.bind(this, false)}
|
||||||
|
>
|
||||||
<HDialog.Panel
|
<HDialog.Panel
|
||||||
as={motion.div}
|
as={motion.div}
|
||||||
animate={controls}
|
initial={'closed'}
|
||||||
|
animate={down ? 'bounce' : 'open'}
|
||||||
|
exit={'closed'}
|
||||||
variants={variants}
|
variants={variants}
|
||||||
className={styles.panel}
|
className={styles.panel}
|
||||||
>
|
>
|
||||||
|
@ -125,11 +128,3 @@ const Dialog = ({
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const _Dialog = Object.assign(Dialog, {
|
|
||||||
Confirm: ConfirmationDialog,
|
|
||||||
Footer: DialogFooter,
|
|
||||||
Icon: DialogIcon,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default _Dialog;
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import DialogContext from '@/components/elements/dialog/context';
|
import { DialogContext } from './';
|
||||||
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
|
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
|
||||||
|
|
||||||
export default ({ children }: { children: React.ReactNode }) => {
|
export default ({ children }: { children: React.ReactNode }) => {
|
||||||
|
|
|
@ -1,16 +1,7 @@
|
||||||
import React, { useContext, useEffect } 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, DialogIconProps, styles } from './';
|
||||||
import styles from './style.module.css';
|
|
||||||
|
|
||||||
export type IconPosition = 'title' | 'container' | undefined;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
type: 'danger' | 'info' | 'success' | 'warning';
|
|
||||||
position?: IconPosition;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
danger: ShieldExclamationIcon,
|
danger: ShieldExclamationIcon,
|
||||||
|
@ -19,7 +10,7 @@ const icons = {
|
||||||
info: InformationCircleIcon,
|
info: InformationCircleIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ type, position, className }: Props) => {
|
export default ({ type, position, className }: DialogIconProps) => {
|
||||||
const { setIcon, setIconPosition } = useContext(DialogContext);
|
const { setIcon, setIconPosition } = useContext(DialogContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IconPosition } from './DialogIcon';
|
import { DialogContextType, DialogWrapperContextType } from './types';
|
||||||
|
|
||||||
type Callback<T> = ((value: T) => void) | React.Dispatch<React.SetStateAction<T>>;
|
export const DialogContext = React.createContext<DialogContextType>({
|
||||||
|
|
||||||
interface DialogContextType {
|
|
||||||
setIcon: Callback<React.ReactNode>;
|
|
||||||
setFooter: Callback<React.ReactNode>;
|
|
||||||
setIconPosition: Callback<IconPosition>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DialogContext = React.createContext<DialogContextType>({
|
|
||||||
setIcon: () => null,
|
setIcon: () => null,
|
||||||
setFooter: () => null,
|
setFooter: () => null,
|
||||||
setIconPosition: () => null,
|
setIconPosition: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default DialogContext;
|
export const DialogWrapperContext = React.createContext<DialogWrapperContextType>({
|
||||||
|
props: {},
|
||||||
|
setProps: () => null,
|
||||||
|
close: () => null,
|
||||||
|
});
|
||||||
|
|
|
@ -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';
|
||||||
|
|
38
resources/scripts/components/elements/dialog/types.d.ts
vendored
Normal file
38
resources/scripts/components/elements/dialog/types.d.ts
vendored
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { IconPosition } from '@/components/elements/dialog/DialogIcon';
|
||||||
|
|
||||||
|
type Callback<T> = ((value: T) => void) | React.Dispatch<React.SetStateAction<T>>;
|
||||||
|
|
||||||
|
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<RenderDialogProps, 'children' | 'open' | 'onClose'>;
|
||||||
|
export interface DialogWrapperContextType {
|
||||||
|
props: Readonly<WrapperProps>;
|
||||||
|
setProps: Callback<Partial<WrapperProps>>;
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DialogContextType {
|
||||||
|
setIcon: Callback<React.ReactNode>;
|
||||||
|
setFooter: Callback<React.ReactNode>;
|
||||||
|
setIconPosition: Callback<IconPosition>;
|
||||||
|
}
|
23
resources/scripts/hoc/asDialog.tsx
Normal file
23
resources/scripts/hoc/asDialog.tsx
Normal file
|
@ -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
|
||||||
|
): <P extends {}>(C: React.ComponentType<P>) => React.FunctionComponent<P & DialogProps> {
|
||||||
|
return function (Component) {
|
||||||
|
return function ({ open, onClose, ...rest }) {
|
||||||
|
const [props, setProps] = useState<WrapperProps>(initialProps || {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogWrapperContext.Provider value={{ props, setProps, close: onClose }}>
|
||||||
|
<Dialog {...props} open={open} onClose={onClose}>
|
||||||
|
<Component {...(rest as React.ComponentProps<typeof Component>)} />
|
||||||
|
</Dialog>
|
||||||
|
</DialogWrapperContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default asDialog;
|
Loading…
Reference in a new issue