diff --git a/web/public/index.html b/web/public/index.html
index 1ee216246..371494bf9 100644
--- a/web/public/index.html
+++ b/web/public/index.html
@@ -15,6 +15,7 @@
+
diff --git a/web/src/components/Dialog.jsx b/web/src/components/Dialog.jsx
new file mode 100644
index 000000000..22347ae59
--- /dev/null
+++ b/web/src/components/Dialog.jsx
@@ -0,0 +1,47 @@
+import { h, Fragment } from 'preact';
+import Button from './Button';
+import Heading from './Heading';
+import { createPortal } from 'preact/compat';
+import { useState, useEffect } from 'preact/hooks';
+
+export default function Dialog({ actions = [], portalRootID = 'dialogs', title, text }) {
+ const portalRoot = portalRootID && document.getElementById(portalRootID);
+ const [show, setShow] = useState(false);
+
+ useEffect(() => {
+ window.requestAnimationFrame(() => {
+ setShow(true);
+ });
+ }, []);
+
+ const dialog = (
+
+
+
+
+
+ {actions.map(({ color, text, onClick }, i) => (
+
+ ))}
+
+
+
+
+ );
+
+ return portalRoot ? createPortal(dialog, portalRoot) : dialog;
+}
diff --git a/web/src/components/__tests__/Dialog.test.jsx b/web/src/components/__tests__/Dialog.test.jsx
new file mode 100644
index 000000000..646f5a46d
--- /dev/null
+++ b/web/src/components/__tests__/Dialog.test.jsx
@@ -0,0 +1,38 @@
+import { h } from 'preact';
+import Dialog from '../Dialog';
+import { fireEvent, render, screen } from '@testing-library/preact';
+
+describe('Dialog', () => {
+ let portal;
+
+ beforeAll(() => {
+ portal = document.createElement('div');
+ portal.id = 'dialogs';
+ document.body.appendChild(portal);
+ });
+
+ afterAll(() => {
+ document.body.removeChild(portal);
+ });
+
+ test('renders to a portal', async () => {
+ render();
+ expect(screen.getByText('Tacos')).toBeInTheDocument();
+ expect(screen.getByRole('modal').closest('#dialogs')).not.toBeNull();
+ });
+
+ test('renders action buttons', async () => {
+ const handleClick = jest.fn();
+ render(
+
+ );
+ fireEvent.click(screen.getByRole('button', { name: 'Okay' }));
+ expect(handleClick).toHaveBeenCalled();
+ });
+});
diff --git a/web/src/routes/StyleGuide.jsx b/web/src/routes/StyleGuide.jsx
index b0759fd63..3e79b36f1 100644
--- a/web/src/routes/StyleGuide.jsx
+++ b/web/src/routes/StyleGuide.jsx
@@ -2,6 +2,7 @@ import { h } from 'preact';
import ArrowDropdown from '../icons/ArrowDropdown';
import ArrowDropup from '../icons/ArrowDropup';
import Button from '../components/Button';
+import Dialog from '../components/Dialog';
import Heading from '../components/Heading';
import Select from '../components/Select';
import Switch from '../components/Switch';
@@ -10,6 +11,7 @@ import { useCallback, useState } from 'preact/hooks';
export default function StyleGuide() {
const [switches, setSwitches] = useState({ 0: false, 1: true, 2: false, 3: false });
+ const [showDialog, setShowDialog] = useState(false);
const handleSwitch = useCallback(
(id, checked) => {
@@ -18,6 +20,10 @@ export default function StyleGuide() {
[switches]
);
+ const handleDismissDialog = () => {
+ setShowDialog(false);
+ };
+
return (
Button
@@ -59,6 +65,26 @@ export default function StyleGuide() {
+ Dialog
+
+ {showDialog ? (
+
+ ) : null}
+
Switch