Skip to content

Commit 7c4028f

Browse files
committed
Update dialog logic to support defining buttons/icon from anywhere
1 parent 48af9bc commit 7c4028f

File tree

9 files changed

+181
-120
lines changed

9 files changed

+181
-120
lines changed

resources/scripts/components/elements/activity/ActivityLogMetaButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ export default ({ meta }: { meta: Record<string, unknown> }) => {
1212
<pre className={'bg-gray-900 rounded p-2 overflow-x-scroll font-mono text-sm leading-relaxed'}>
1313
{JSON.stringify(meta, null, 2)}
1414
</pre>
15-
<Dialog.Buttons>
15+
<Dialog.Footer>
1616
<Button.Text onClick={() => setOpen(false)}>Close</Button.Text>
17-
</Dialog.Buttons>
17+
</Dialog.Footer>
1818
</Dialog>
1919
<button
2020
aria-describedby={'View additional event metadata'}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React from 'react';
22
import { Dialog } from '@/components/elements/dialog/index';
3-
import { DialogProps } from '@/components/elements/dialog/Dialog';
3+
import { FullDialogProps } from '@/components/elements/dialog/Dialog';
44
import { Button } from '@/components/elements/button/index';
55

6-
type ConfirmationProps = Omit<DialogProps, 'description' | 'children'> & {
6+
type ConfirmationProps = Omit<FullDialogProps, 'description' | 'children'> & {
77
children: React.ReactNode;
88
confirm?: string | undefined;
99
onConfirmed: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
@@ -13,10 +13,10 @@ export default ({ confirm = 'Okay', children, onConfirmed, ...props }: Confirmat
1313
return (
1414
<Dialog {...props} description={typeof children === 'string' ? children : undefined}>
1515
{typeof children !== 'string' && children}
16-
<Dialog.Buttons>
16+
<Dialog.Footer>
1717
<Button.Text onClick={props.onClose}>Cancel</Button.Text>
1818
<Button.Danger onClick={onConfirmed}>{confirm}</Button.Danger>
19-
</Dialog.Buttons>
19+
</Dialog.Footer>
2020
</Dialog>
2121
);
2222
};

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

Lines changed: 67 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,99 @@
1-
import React from 'react';
1+
import React, { useRef } 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';
55
import DialogIcon from '@/components/elements/dialog/DialogIcon';
66
import { AnimatePresence, motion } from 'framer-motion';
7-
import classNames from 'classnames';
87
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';
911

1012
export interface DialogProps {
1113
open: boolean;
1214
onClose: () => void;
15+
}
16+
17+
export interface FullDialogProps extends DialogProps {
1318
hideCloseIcon?: boolean;
1419
title?: string;
1520
description?: string | undefined;
1621
children?: React.ReactNode;
1722
}
1823

19-
const DialogButtons = ({ children }: { children: React.ReactNode }) => <>{children}</>;
20-
21-
const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }: DialogProps) => {
22-
const items = React.Children.toArray(children || []);
23-
const [buttons, icon, content] = [
24-
// @ts-expect-error not sure how to get this correct
25-
items.find((child) => child.type === DialogButtons),
26-
// @ts-expect-error not sure how to get this correct
27-
items.find((child) => child.type === DialogIcon),
28-
// @ts-expect-error not sure how to get this correct
29-
items.filter((child) => ![DialogIcon, DialogButtons].includes(child.type)),
30-
];
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);
3128

3229
return (
3330
<AnimatePresence>
3431
{open && (
35-
<HDialog
36-
static
37-
as={motion.div}
38-
initial={{ opacity: 0 }}
39-
animate={{ opacity: 1 }}
40-
exit={{ opacity: 0 }}
41-
transition={{ duration: 0.15 }}
42-
open={open}
43-
onClose={onClose}
44-
>
45-
<div className={'fixed inset-0 bg-gray-900/50 z-40'} />
46-
<div className={'fixed inset-0 overflow-y-auto z-50'}>
47-
<div className={'flex min-h-full items-center justify-center p-4 text-center'}>
48-
<HDialog.Panel
49-
as={motion.div}
50-
initial={{ opacity: 0, scale: 0.85 }}
51-
animate={{ opacity: 1, scale: 1 }}
52-
exit={{ opacity: 0 }}
53-
transition={{ type: 'spring', damping: 15, stiffness: 300, duration: 0.15 }}
54-
className={classNames([
55-
'relative bg-gray-600 rounded max-w-xl w-full mx-auto shadow-lg text-left',
56-
'ring-4 ring-gray-800 ring-opacity-80',
57-
])}
58-
>
59-
<div className={'flex p-6 overflow-y-auto'}>
60-
{icon && <div className={'mr-4'}>{icon}</div>}
61-
<div className={'flex-1 max-h-[70vh]'}>
62-
{title && (
63-
<HDialog.Title
64-
className={'font-header text-xl font-medium mb-2 text-gray-50 pr-4'}
65-
>
66-
{title}
67-
</HDialog.Title>
68-
)}
69-
{description && <HDialog.Description>{description}</HDialog.Description>}
70-
{content}
32+
<DialogContext.Provider value={{ icon, buttons }}>
33+
<HDialog
34+
static
35+
as={motion.div}
36+
initial={{ opacity: 0 }}
37+
animate={{ opacity: 1 }}
38+
exit={{ opacity: 0 }}
39+
transition={{ duration: 0.15 }}
40+
open={open}
41+
onClose={onClose}
42+
>
43+
<div className={'fixed inset-0 bg-gray-900/50 z-40'} />
44+
<div className={'fixed inset-0 overflow-y-auto z-50'}>
45+
<div className={styles.container}>
46+
<HDialog.Panel
47+
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 }}
52+
className={styles.panel}
53+
>
54+
<div className={'flex p-6 overflow-y-auto'}>
55+
<div ref={ref} className={'flex-1 max-h-[70vh]'}>
56+
<div className={'flex items-center'}>
57+
<div ref={icon} />
58+
<div>
59+
{title && (
60+
<HDialog.Title className={styles.title}>{title}</HDialog.Title>
61+
)}
62+
{description && (
63+
<HDialog.Description>{description}</HDialog.Description>
64+
)}
65+
</div>
66+
</div>
67+
{children}
68+
</div>
7169
</div>
72-
</div>
73-
{buttons && (
74-
<div
75-
className={
76-
'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'
77-
}
78-
>
79-
{buttons}
80-
</div>
81-
)}
82-
{/* Keep this below the other buttons so that it isn't the default focus if they're present. */}
83-
{!hideCloseIcon && (
84-
<div className={'absolute right-0 top-0 m-4'}>
85-
<Button.Text
86-
size={Button.Sizes.Small}
87-
shape={Button.Shapes.IconSquare}
88-
onClick={onClose}
89-
className={'hover:rotate-90'}
90-
>
91-
<XIcon className={'w-5 h-5'} />
92-
</Button.Text>
93-
</div>
94-
)}
95-
</HDialog.Panel>
70+
<div ref={buttons} />
71+
{/* Keep this below the other buttons so that it isn't the default focus if they're present. */}
72+
{!hideCloseIcon && (
73+
<div className={'absolute right-0 top-0 m-4'}>
74+
<Button.Text
75+
size={Button.Sizes.Small}
76+
shape={Button.Shapes.IconSquare}
77+
onClick={onClose}
78+
className={'group'}
79+
>
80+
<XIcon className={styles.close_icon} />
81+
</Button.Text>
82+
</div>
83+
)}
84+
</HDialog.Panel>
85+
</div>
9686
</div>
97-
</div>
98-
</HDialog>
87+
</HDialog>
88+
</DialogContext.Provider>
9989
)}
10090
</AnimatePresence>
10191
);
10292
};
10393

10494
const _Dialog = Object.assign(Dialog, {
10595
Confirm: ConfirmationDialog,
106-
Buttons: DialogButtons,
96+
Footer: DialogFooter,
10797
Icon: DialogIcon,
10898
});
10999

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, { useContext } from 'react';
2+
import { createPortal } from 'react-dom';
3+
import DialogContext from '@/components/elements/dialog/context';
4+
5+
export default ({ children }: { children: React.ReactNode }) => {
6+
const { buttons } = useContext(DialogContext);
7+
8+
if (!buttons.current) {
9+
return null;
10+
}
11+
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);
17+
};
Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,34 @@
1-
import React from 'react';
1+
import React, { useContext } 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 { createPortal } from 'react-dom';
6+
import styles from './style.module.css';
47

58
interface Props {
69
type: 'danger' | 'info' | 'success' | 'warning';
710
className?: string;
811
}
912

13+
const icons = {
14+
danger: ShieldExclamationIcon,
15+
warning: ExclamationIcon,
16+
success: CheckIcon,
17+
info: InformationCircleIcon,
18+
};
19+
1020
export default ({ type, className }: Props) => {
11-
const [Component, styles] = (function (): [(props: React.ComponentProps<'svg'>) => JSX.Element, string] {
12-
switch (type) {
13-
case 'danger':
14-
return [ShieldExclamationIcon, 'bg-red-500 text-red-50'];
15-
case 'warning':
16-
return [ExclamationIcon, 'bg-yellow-600 text-yellow-50'];
17-
case 'success':
18-
return [CheckIcon, 'bg-green-600 text-green-50'];
19-
case 'info':
20-
return [InformationCircleIcon, 'bg-primary-500 text-primary-50'];
21-
}
22-
})();
21+
const { icon } = useContext(DialogContext);
2322

24-
return (
25-
<div className={classNames('flex items-center justify-center w-10 h-10 rounded-full', styles, className)}>
26-
<Component className={'w-6 h-6'} />
23+
if (!icon.current) {
24+
return null;
25+
}
26+
27+
const element = (
28+
<div className={classNames(styles.dialog_icon, styles[type], className)}>
29+
{React.createElement(icons[type], { className: 'w-6 h-6' })}
2730
</div>
2831
);
32+
33+
return createPortal(element, icon.current);
2934
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
3+
interface DialogContextType {
4+
icon: React.RefObject<HTMLDivElement | undefined>;
5+
buttons: React.RefObject<HTMLDivElement | undefined>;
6+
}
7+
8+
const DialogContext = React.createContext<DialogContextType>({
9+
icon: React.createRef(),
10+
buttons: React.createRef(),
11+
});
12+
13+
export default DialogContext;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.container {
2+
@apply flex min-h-full items-center justify-center p-4 text-center;
3+
}
4+
5+
.panel {
6+
@apply relative bg-gray-600 rounded max-w-xl w-full mx-auto shadow-lg text-left;
7+
@apply ring-4 ring-gray-800 ring-opacity-80;
8+
}
9+
10+
.title {
11+
@apply font-header text-xl font-medium mb-2 text-gray-50 pr-4;
12+
}
13+
14+
.close_icon {
15+
@apply w-5 h-5 group-hover:rotate-90 transition-transform duration-100;
16+
}
17+
18+
.dialog_icon {
19+
@apply flex items-center justify-center w-10 h-10 rounded-full mr-4;
20+
21+
&.danger {
22+
@apply bg-red-500 text-red-50;
23+
}
24+
25+
&.warning {
26+
@apply bg-yellow-600 text-yellow-50;
27+
}
28+
29+
&.success {
30+
@apply bg-green-600 text-green-50;
31+
}
32+
33+
&.info {
34+
@apply bg-primary-500 text-primary-50;
35+
}
36+
}

resources/scripts/components/server/files/NewDirectoryButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export default ({ className }: WithClassname) => {
9292
</Code>
9393
</p>
9494
</Form>
95-
<Dialog.Buttons>
95+
<Dialog.Footer>
9696
<Button.Text
9797
className={'w-full sm:w-auto'}
9898
onClick={() => {
@@ -105,7 +105,7 @@ export default ({ className }: WithClassname) => {
105105
<Button className={'w-full sm:w-auto'} onClick={submitForm}>
106106
Create
107107
</Button>
108-
</Dialog.Buttons>
108+
</Dialog.Footer>
109109
</Dialog>
110110
)}
111111
</Formik>

0 commit comments

Comments
 (0)