Update dialog logic to support defining buttons/icon from anywhere
This commit is contained in:
parent
48af9bced1
commit
7c4028f8f1
9 changed files with 182 additions and 121 deletions
|
@ -12,9 +12,9 @@ export default ({ meta }: { meta: Record<string, unknown> }) => {
|
||||||
<pre className={'bg-gray-900 rounded p-2 overflow-x-scroll font-mono text-sm leading-relaxed'}>
|
<pre className={'bg-gray-900 rounded p-2 overflow-x-scroll font-mono text-sm leading-relaxed'}>
|
||||||
{JSON.stringify(meta, null, 2)}
|
{JSON.stringify(meta, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
<Dialog.Buttons>
|
<Dialog.Footer>
|
||||||
<Button.Text onClick={() => setOpen(false)}>Close</Button.Text>
|
<Button.Text onClick={() => setOpen(false)}>Close</Button.Text>
|
||||||
</Dialog.Buttons>
|
</Dialog.Footer>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<button
|
<button
|
||||||
aria-describedby={'View additional event metadata'}
|
aria-describedby={'View additional event metadata'}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Dialog } from '@/components/elements/dialog/index';
|
import { Dialog } from '@/components/elements/dialog/index';
|
||||||
import { DialogProps } from '@/components/elements/dialog/Dialog';
|
import { FullDialogProps } from '@/components/elements/dialog/Dialog';
|
||||||
import { Button } from '@/components/elements/button/index';
|
import { Button } from '@/components/elements/button/index';
|
||||||
|
|
||||||
type ConfirmationProps = Omit<DialogProps, 'description' | 'children'> & {
|
type ConfirmationProps = Omit<FullDialogProps, '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;
|
||||||
|
@ -13,10 +13,10 @@ export default ({ confirm = 'Okay', children, onConfirmed, ...props }: Confirmat
|
||||||
return (
|
return (
|
||||||
<Dialog {...props} description={typeof children === 'string' ? children : undefined}>
|
<Dialog {...props} description={typeof children === 'string' ? children : undefined}>
|
||||||
{typeof children !== 'string' && children}
|
{typeof children !== 'string' && children}
|
||||||
<Dialog.Buttons>
|
<Dialog.Footer>
|
||||||
<Button.Text onClick={props.onClose}>Cancel</Button.Text>
|
<Button.Text onClick={props.onClose}>Cancel</Button.Text>
|
||||||
<Button.Danger onClick={onConfirmed}>{confirm}</Button.Danger>
|
<Button.Danger onClick={onConfirmed}>{confirm}</Button.Danger>
|
||||||
</Dialog.Buttons>
|
</Dialog.Footer>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,37 +1,35 @@
|
||||||
import React from 'react';
|
import React, { useRef } 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 from '@/components/elements/dialog/DialogIcon';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import classNames from 'classnames';
|
|
||||||
import ConfirmationDialog from '@/components/elements/dialog/ConfirmationDialog';
|
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 {
|
export interface DialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullDialogProps extends DialogProps {
|
||||||
hideCloseIcon?: boolean;
|
hideCloseIcon?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string | undefined;
|
description?: string | undefined;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogButtons = ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }: FullDialogProps) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }: DialogProps) => {
|
const icon = useRef<HTMLDivElement>(null);
|
||||||
const items = React.Children.toArray(children || []);
|
const buttons = useRef<HTMLDivElement>(null);
|
||||||
const [buttons, icon, content] = [
|
|
||||||
// @ts-expect-error not sure how to get this correct
|
|
||||||
items.find((child) => child.type === DialogButtons),
|
|
||||||
// @ts-expect-error not sure how to get this correct
|
|
||||||
items.find((child) => child.type === DialogIcon),
|
|
||||||
// @ts-expect-error not sure how to get this correct
|
|
||||||
items.filter((child) => ![DialogIcon, DialogButtons].includes(child.type)),
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{open && (
|
{open && (
|
||||||
|
<DialogContext.Provider value={{ icon, buttons }}>
|
||||||
<HDialog
|
<HDialog
|
||||||
static
|
static
|
||||||
as={motion.div}
|
as={motion.div}
|
||||||
|
@ -44,41 +42,32 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
|
||||||
>
|
>
|
||||||
<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={'flex min-h-full items-center justify-center p-4 text-center'}>
|
<div className={styles.container}>
|
||||||
<HDialog.Panel
|
<HDialog.Panel
|
||||||
as={motion.div}
|
as={motion.div}
|
||||||
initial={{ opacity: 0, scale: 0.85 }}
|
initial={{ opacity: 0, scale: 0.85 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ type: 'spring', damping: 15, stiffness: 300, duration: 0.15 }}
|
transition={{ type: 'spring', damping: 15, stiffness: 300, duration: 0.15 }}
|
||||||
className={classNames([
|
className={styles.panel}
|
||||||
'relative bg-gray-600 rounded max-w-xl w-full mx-auto shadow-lg text-left',
|
|
||||||
'ring-4 ring-gray-800 ring-opacity-80',
|
|
||||||
])}
|
|
||||||
>
|
>
|
||||||
<div className={'flex p-6 overflow-y-auto'}>
|
<div className={'flex p-6 overflow-y-auto'}>
|
||||||
{icon && <div className={'mr-4'}>{icon}</div>}
|
<div ref={ref} className={'flex-1 max-h-[70vh]'}>
|
||||||
<div className={'flex-1 max-h-[70vh]'}>
|
<div className={'flex items-center'}>
|
||||||
|
<div ref={icon} />
|
||||||
|
<div>
|
||||||
{title && (
|
{title && (
|
||||||
<HDialog.Title
|
<HDialog.Title className={styles.title}>{title}</HDialog.Title>
|
||||||
className={'font-header text-xl font-medium mb-2 text-gray-50 pr-4'}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</HDialog.Title>
|
|
||||||
)}
|
)}
|
||||||
{description && <HDialog.Description>{description}</HDialog.Description>}
|
{description && (
|
||||||
{content}
|
<HDialog.Description>{description}</HDialog.Description>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{buttons && (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{buttons}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref={buttons} />
|
||||||
{/* 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'}>
|
||||||
|
@ -86,9 +75,9 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
|
||||||
size={Button.Sizes.Small}
|
size={Button.Sizes.Small}
|
||||||
shape={Button.Shapes.IconSquare}
|
shape={Button.Shapes.IconSquare}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className={'hover:rotate-90'}
|
className={'group'}
|
||||||
>
|
>
|
||||||
<XIcon className={'w-5 h-5'} />
|
<XIcon className={styles.close_icon} />
|
||||||
</Button.Text>
|
</Button.Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -96,6 +85,7 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HDialog>
|
</HDialog>
|
||||||
|
</DialogContext.Provider>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
|
@ -103,7 +93,7 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
|
||||||
|
|
||||||
const _Dialog = Object.assign(Dialog, {
|
const _Dialog = Object.assign(Dialog, {
|
||||||
Confirm: ConfirmationDialog,
|
Confirm: ConfirmationDialog,
|
||||||
Buttons: DialogButtons,
|
Footer: DialogFooter,
|
||||||
Icon: DialogIcon,
|
Icon: DialogIcon,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import DialogContext from '@/components/elements/dialog/context';
|
||||||
|
|
||||||
|
export default ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const { buttons } = useContext(DialogContext);
|
||||||
|
|
||||||
|
if (!buttons.current) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<div className={'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'}>{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return createPortal(element, buttons.current);
|
||||||
|
};
|
|
@ -1,29 +1,34 @@
|
||||||
import React from 'react';
|
import React, { useContext } 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 { createPortal } from 'react-dom';
|
||||||
|
import styles from './style.module.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: 'danger' | 'info' | 'success' | 'warning';
|
type: 'danger' | 'info' | 'success' | 'warning';
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ type, className }: Props) => {
|
const icons = {
|
||||||
const [Component, styles] = (function (): [(props: React.ComponentProps<'svg'>) => JSX.Element, string] {
|
danger: ShieldExclamationIcon,
|
||||||
switch (type) {
|
warning: ExclamationIcon,
|
||||||
case 'danger':
|
success: CheckIcon,
|
||||||
return [ShieldExclamationIcon, 'bg-red-500 text-red-50'];
|
info: InformationCircleIcon,
|
||||||
case 'warning':
|
};
|
||||||
return [ExclamationIcon, 'bg-yellow-600 text-yellow-50'];
|
|
||||||
case 'success':
|
|
||||||
return [CheckIcon, 'bg-green-600 text-green-50'];
|
|
||||||
case 'info':
|
|
||||||
return [InformationCircleIcon, 'bg-primary-500 text-primary-50'];
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
export default ({ type, className }: Props) => {
|
||||||
<div className={classNames('flex items-center justify-center w-10 h-10 rounded-full', styles, className)}>
|
const { icon } = useContext(DialogContext);
|
||||||
<Component className={'w-6 h-6'} />
|
|
||||||
|
if (!icon.current) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<div className={classNames(styles.dialog_icon, styles[type], className)}>
|
||||||
|
{React.createElement(icons[type], { className: 'w-6 h-6' })}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return createPortal(element, icon.current);
|
||||||
};
|
};
|
||||||
|
|
13
resources/scripts/components/elements/dialog/context.ts
Normal file
13
resources/scripts/components/elements/dialog/context.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface DialogContextType {
|
||||||
|
icon: React.RefObject<HTMLDivElement | undefined>;
|
||||||
|
buttons: React.RefObject<HTMLDivElement | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DialogContext = React.createContext<DialogContextType>({
|
||||||
|
icon: React.createRef(),
|
||||||
|
buttons: React.createRef(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default DialogContext;
|
|
@ -0,0 +1,36 @@
|
||||||
|
.container {
|
||||||
|
@apply flex min-h-full items-center justify-center p-4 text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
@apply relative bg-gray-600 rounded max-w-xl w-full mx-auto shadow-lg text-left;
|
||||||
|
@apply ring-4 ring-gray-800 ring-opacity-80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@apply font-header text-xl font-medium mb-2 text-gray-50 pr-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close_icon {
|
||||||
|
@apply w-5 h-5 group-hover:rotate-90 transition-transform duration-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog_icon {
|
||||||
|
@apply flex items-center justify-center w-10 h-10 rounded-full mr-4;
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
@apply bg-red-500 text-red-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
@apply bg-yellow-600 text-yellow-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
@apply bg-green-600 text-green-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.info {
|
||||||
|
@apply bg-primary-500 text-primary-50;
|
||||||
|
}
|
||||||
|
}
|
|
@ -92,7 +92,7 @@ export default ({ className }: WithClassname) => {
|
||||||
</Code>
|
</Code>
|
||||||
</p>
|
</p>
|
||||||
</Form>
|
</Form>
|
||||||
<Dialog.Buttons>
|
<Dialog.Footer>
|
||||||
<Button.Text
|
<Button.Text
|
||||||
className={'w-full sm:w-auto'}
|
className={'w-full sm:w-auto'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -105,7 +105,7 @@ export default ({ className }: WithClassname) => {
|
||||||
<Button className={'w-full sm:w-auto'} onClick={submitForm}>
|
<Button className={'w-full sm:w-auto'} onClick={submitForm}>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Buttons>
|
</Dialog.Footer>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
|
|
38
yarn.lock
38
yarn.lock
|
@ -1093,33 +1093,33 @@
|
||||||
minimatch "^3.1.2"
|
minimatch "^3.1.2"
|
||||||
strip-json-comments "^3.1.1"
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
"@floating-ui/core@^0.7.2":
|
"@floating-ui/core@^0.7.3":
|
||||||
version "0.7.2"
|
version "0.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.7.2.tgz#f7af9613d080dc29360e77c970965b79b524d45a"
|
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.7.3.tgz#d274116678ffae87f6b60e90f88cc4083eefab86"
|
||||||
integrity sha512-FRVAkSNU/vGXLIsgbggcs70GkXKEOXgBBbNpYPNHSaKsCAMMd00NrjbtKTesxkdv9xm9N3+XiDlcFGY6WnatBg==
|
integrity sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==
|
||||||
|
|
||||||
"@floating-ui/dom@^0.5.1":
|
"@floating-ui/dom@^0.5.3":
|
||||||
version "0.5.2"
|
version "0.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.5.2.tgz#908f3febbfc0d6696d70921616ec194fe07af183"
|
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.5.4.tgz#4eae73f78bcd4bd553ae2ade30e6f1f9c73fe3f1"
|
||||||
integrity sha512-z1DnEa7F3d8Fm/eXSbii8UEGpcjZGkQaYYUI0WpEVgD3vBfebDW8j/3ysusxonuMexoigA+A3b/fYH7sEqiwyg==
|
integrity sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/core" "^0.7.2"
|
"@floating-ui/core" "^0.7.3"
|
||||||
|
|
||||||
"@floating-ui/react-dom-interactions@^0.6.3":
|
"@floating-ui/react-dom-interactions@^0.6.6":
|
||||||
version "0.6.3"
|
version "0.6.6"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.6.3.tgz#895c52cb06bf5ea73c00f1074c75b0535e0046bc"
|
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.6.6.tgz#8542e8c4bcbee2cd0d512de676c6a493e0a2d168"
|
||||||
integrity sha512-xvbGEtBtA7JaEngnHQjROArv2onRp3oJIpb4+bEN5EGJf0hBYDY0vD8vFGPz/5TQwN++hb6icOB1QwdOnffMzw==
|
integrity sha512-qnao6UPjSZNHnXrF+u4/n92qVroQkx0Umlhy3Avk1oIebm/5ee6yvDm4xbHob0OjY7ya8WmUnV3rQlPwX3Atwg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/react-dom" "^0.7.1"
|
"@floating-ui/react-dom" "^0.7.2"
|
||||||
aria-hidden "^1.1.3"
|
aria-hidden "^1.1.3"
|
||||||
use-isomorphic-layout-effect "^1.1.1"
|
use-isomorphic-layout-effect "^1.1.1"
|
||||||
|
|
||||||
"@floating-ui/react-dom@^0.7.1":
|
"@floating-ui/react-dom@^0.7.2":
|
||||||
version "0.7.1"
|
version "0.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-0.7.1.tgz#e0eb57cb05c7762d4c904bfbae73148684578d66"
|
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-0.7.2.tgz#0bf4ceccb777a140fc535c87eb5d6241c8e89864"
|
||||||
integrity sha512-Dd7e8AupUjzcjeGf1g3EItf/QRtEWKF5GGyEs5WA5n3zlHvEgZ4XrZM6ANhUnzgE3pUQAaXkcXLnibgFp1YBRw==
|
integrity sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/dom" "^0.5.1"
|
"@floating-ui/dom" "^0.5.3"
|
||||||
use-isomorphic-layout-effect "^1.1.1"
|
use-isomorphic-layout-effect "^1.1.1"
|
||||||
|
|
||||||
"@fortawesome/fontawesome-common-types@^0.2.32":
|
"@fortawesome/fontawesome-common-types@^0.2.32":
|
||||||
|
|
Loading…
Reference in a new issue