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 = ( + +
+
+
+ {title} +

{text}

+
+
+ {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