Fix dialog and tooltip design

This commit is contained in:
DaneEveritt 2022-06-12 15:07:52 -04:00
parent 064a942574
commit 33823b65de
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
5 changed files with 99 additions and 107 deletions

View File

@ -1,49 +1,25 @@
import React, { Fragment } from 'react'; import React from 'react';
import { Dialog as HeadlessDialog, Transition } 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 styles from './style.module.css';
import { XIcon } from '@heroicons/react/solid'; import { XIcon } from '@heroicons/react/solid';
import { CheckIcon, ExclamationIcon, InformationCircleIcon, ShieldExclamationIcon } from '@heroicons/react/outline'; import DialogIcon from '@/components/elements/dialog/DialogIcon';
import { AnimatePresence, motion } from 'framer-motion';
import classNames from 'classnames'; import classNames from 'classnames';
interface Props { interface Props {
visible: boolean; open: boolean;
onDismissed: () => void; onClose: () => void;
hideCloseIcon?: boolean;
title?: string; title?: string;
description?: string;
children?: React.ReactNode; children?: React.ReactNode;
} }
interface DialogIconProps {
type: 'danger' | 'info' | 'success' | 'warning';
className?: string;
}
const DialogIcon = ({ type, className }: DialogIconProps) => {
const [ Component, styles ] = (function (): [(props: React.ComponentProps<'svg'>) => JSX.Element, string] {
switch (type) {
case 'danger':
return [ ShieldExclamationIcon, 'bg-red-500 text-red-50' ];
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 (
<div className={classNames('flex items-center justify-center w-10 h-10 rounded-full', styles, className)}>
<Component className={'w-6 h-6'} />
</div>
);
};
const DialogButtons = ({ children }: { children: React.ReactNode }) => ( const DialogButtons = ({ children }: { children: React.ReactNode }) => (
<>{children}</> <>{children}</>
); );
const Dialog = ({ visible, title, onDismissed, children }: Props) => { const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }: Props) => {
const items = React.Children.toArray(children || []); const items = React.Children.toArray(children || []);
const [ buttons, icon, content ] = [ const [ buttons, icon, content ] = [
// @ts-expect-error // @ts-expect-error
@ -55,55 +31,63 @@ const Dialog = ({ visible, title, onDismissed, children }: Props) => {
]; ];
return ( return (
<Transition show={visible} as={Fragment}> <AnimatePresence>
<HeadlessDialog onClose={() => onDismissed()} className={styles.wrapper}> {open && (
<div className={'flex items-center justify-center min-h-screen'}> <HDialog
<Transition.Child static
as={Fragment} as={motion.div}
enter={'ease-out duration-200'} initial={{ opacity: 0 }}
enterFrom={'opacity-0'} animate={{ opacity: 1 }}
enterTo={'opacity-100'} exit={{ opacity: 0 }}
leave={'ease-in duration-100'} transition={{ duration: 0.15 }}
leaveFrom={'opacity-100'} open={open}
leaveTo={'opacity-0'} onClose={onClose}
> >
<HeadlessDialog.Overlay className={styles.overlay}/> <div className={'fixed inset-0 bg-gray-900/50'}/>
</Transition.Child> <div className={'fixed inset-0 overflow-y-auto'}>
<Transition.Child <div className={'flex min-h-full items-center justify-center p-4 text-center'}>
as={Fragment} <HDialog.Panel
enter={'ease-out duration-200'} as={motion.div}
enterFrom={'opacity-0 scale-95'} initial={{ opacity: 0, scale: 0.85 }}
enterTo={'opacity-100 scale-100'} animate={{ opacity: 1, scale: 1 }}
leave={'ease-in duration-100'} exit={{ opacity: 0 }}
leaveFrom={'opacity-100 scale-100'} transition={{ type: 'spring', damping: 15, stiffness: 300, duration: 0.15 }}
leaveTo={'opacity-0 scale-95'} className={classNames([
> 'relative bg-gray-600 rounded max-w-xl w-full mx-auto shadow-lg text-left',
<div className={styles.container}> 'ring-4 ring-gray-800 ring-opacity-80',
<div className={'flex p-6'}> ])}
{icon && <div className={'mr-4'}>{icon}</div>} >
<div className={'flex-1'}> <div className={'flex p-6'}>
{title && {icon && <div className={'mr-4'}>{icon}</div>}
<HeadlessDialog.Title className={styles.title}> <div className={'flex-1 max-h-[70vh] overflow-y-scroll overflow-x-hidden'}>
{title} {title &&
</HeadlessDialog.Title> <HDialog.Title className={'font-header text-xl font-medium mb-2 text-white pr-4'}>
} {title}
<HeadlessDialog.Description className={'pr-4'}> </HDialog.Title>
}
{description && <HDialog.Description>{description}</HDialog.Description>}
{content} {content}
</HeadlessDialog.Description> </div>
</div> </div>
</div> {buttons &&
{buttons && <div className={styles.button_bar}>{buttons}</div>} <div className={'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'}>
{/* Keep this below the other buttons so that it isn't the default focus if they're present. */} {buttons}
<div className={'absolute right-0 top-0 m-4'}> </div>
<Button.Text square small onClick={() => onDismissed()} className={'hover:rotate-90'}> }
<XIcon className={'w-5 h-5'}/> {/* Keep this below the other buttons so that it isn't the default focus if they're present. */}
</Button.Text> {!hideCloseIcon &&
</div> <div className={'absolute right-0 top-0 m-4'}>
<Button.Text square small onClick={onClose} className={'hover:rotate-90'}>
<XIcon className={'w-5 h-5'}/>
</Button.Text>
</div>
}
</HDialog.Panel>
</div> </div>
</Transition.Child> </div>
</div> </HDialog>
</HeadlessDialog> )}
</Transition> </AnimatePresence>
); );
}; };

View File

@ -0,0 +1,29 @@
import React from 'react';
import { CheckIcon, ExclamationIcon, InformationCircleIcon, ShieldExclamationIcon } from '@heroicons/react/outline';
import classNames from 'classnames';
interface Props {
type: 'danger' | 'info' | 'success' | 'warning';
className?: string;
}
export default ({ type, className }: Props) => {
const [ Component, styles ] = (function (): [ (props: React.ComponentProps<'svg'>) => JSX.Element, string ] {
switch (type) {
case 'danger':
return [ ShieldExclamationIcon, 'bg-red-500 text-red-50' ];
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 (
<div className={classNames('flex items-center justify-center w-10 h-10 rounded-full', styles, className)}>
<Component className={'w-6 h-6'}/>
</div>
);
};

View File

@ -1,2 +1 @@
export { default as Dialog } from './Dialog'; export { default as Dialog } from './Dialog';
export { default as styles } from './style.module.css';

View File

@ -1,20 +0,0 @@
.wrapper {
@apply fixed z-10 inset-0 overflow-y-auto;
}
.overlay {
@apply fixed inset-0 bg-gray-900 opacity-50;
}
.container {
@apply relative bg-gray-600 rounded max-w-xl w-full mx-auto shadow-lg;
@apply ring-4 ring-gray-800 ring-opacity-80;
& .title {
@apply font-header text-xl font-medium mb-2 text-white pr-4;
}
& > .button_bar {
@apply px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b;
}
}

View File

@ -16,6 +16,7 @@ import {
useRole, useRole,
} from '@floating-ui/react-dom-interactions'; } from '@floating-ui/react-dom-interactions';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import classNames from 'classnames';
interface Props { interface Props {
rest?: number; rest?: number;
@ -30,11 +31,11 @@ interface Props {
children: React.ReactElement; children: React.ReactElement;
} }
const arrowSides: Record<Side, Side> = { const arrowSides: Record<Side, string> = {
top: 'bottom', top: 'bottom-[-6px] left-0',
bottom: 'top', bottom: 'top-[-6px] left-0',
left: 'right', right: 'top-0 left-[-6px]',
right: 'left', left: 'top-0 right-[-6px]',
}; };
export default ({ export default ({
@ -103,9 +104,8 @@ export default ({
ref={arrowEl} ref={arrowEl}
style={{ style={{
transform: `translate(${Math.round(ax || 0)}px, ${Math.round(ay || 0)}px) rotate(45deg)`, transform: `translate(${Math.round(ax || 0)}px, ${Math.round(ay || 0)}px) rotate(45deg)`,
[side]: '-6px',
}} }}
className={'absolute top-0 left-0 bg-gray-900 w-3 h-3'} className={classNames('absolute bg-gray-900 w-3 h-3', side)}
/> />
} }
</motion.div> </motion.div>