Fix dialog and tooltip design
This commit is contained in:
parent
064a942574
commit
33823b65de
5 changed files with 99 additions and 107 deletions
|
@ -1,49 +1,25 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import { Dialog as HeadlessDialog, Transition } from '@headlessui/react';
|
||||
import React from 'react';
|
||||
import { Dialog as HDialog } from '@headlessui/react';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
import styles from './style.module.css';
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
onDismissed: () => void;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
hideCloseIcon?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
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 }) => (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
const Dialog = ({ visible, title, onDismissed, children }: Props) => {
|
||||
const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }: Props) => {
|
||||
const items = React.Children.toArray(children || []);
|
||||
const [ buttons, icon, content ] = [
|
||||
// @ts-expect-error
|
||||
|
@ -55,55 +31,63 @@ const Dialog = ({ visible, title, onDismissed, children }: Props) => {
|
|||
];
|
||||
|
||||
return (
|
||||
<Transition show={visible} as={Fragment}>
|
||||
<HeadlessDialog onClose={() => onDismissed()} className={styles.wrapper}>
|
||||
<div className={'flex items-center justify-center min-h-screen'}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter={'ease-out duration-200'}
|
||||
enterFrom={'opacity-0'}
|
||||
enterTo={'opacity-100'}
|
||||
leave={'ease-in duration-100'}
|
||||
leaveFrom={'opacity-100'}
|
||||
leaveTo={'opacity-0'}
|
||||
>
|
||||
<HeadlessDialog.Overlay className={styles.overlay}/>
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter={'ease-out duration-200'}
|
||||
enterFrom={'opacity-0 scale-95'}
|
||||
enterTo={'opacity-100 scale-100'}
|
||||
leave={'ease-in duration-100'}
|
||||
leaveFrom={'opacity-100 scale-100'}
|
||||
leaveTo={'opacity-0 scale-95'}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={'flex p-6'}>
|
||||
{icon && <div className={'mr-4'}>{icon}</div>}
|
||||
<div className={'flex-1'}>
|
||||
{title &&
|
||||
<HeadlessDialog.Title className={styles.title}>
|
||||
{title}
|
||||
</HeadlessDialog.Title>
|
||||
}
|
||||
<HeadlessDialog.Description className={'pr-4'}>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<HDialog
|
||||
static
|
||||
as={motion.div}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={'fixed inset-0 bg-gray-900/50'}/>
|
||||
<div className={'fixed inset-0 overflow-y-auto'}>
|
||||
<div className={'flex min-h-full items-center justify-center p-4 text-center'}>
|
||||
<HDialog.Panel
|
||||
as={motion.div}
|
||||
initial={{ opacity: 0, scale: 0.85 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ type: 'spring', damping: 15, stiffness: 300, duration: 0.15 }}
|
||||
className={classNames([
|
||||
'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'}>
|
||||
{icon && <div className={'mr-4'}>{icon}</div>}
|
||||
<div className={'flex-1 max-h-[70vh] overflow-y-scroll overflow-x-hidden'}>
|
||||
{title &&
|
||||
<HDialog.Title className={'font-header text-xl font-medium mb-2 text-white pr-4'}>
|
||||
{title}
|
||||
</HDialog.Title>
|
||||
}
|
||||
{description && <HDialog.Description>{description}</HDialog.Description>}
|
||||
{content}
|
||||
</HeadlessDialog.Description>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{buttons && <div className={styles.button_bar}>{buttons}</div>}
|
||||
{/* Keep this below the other buttons so that it isn't the default focus if they're present. */}
|
||||
<div className={'absolute right-0 top-0 m-4'}>
|
||||
<Button.Text square small onClick={() => onDismissed()} className={'hover:rotate-90'}>
|
||||
<XIcon className={'w-5 h-5'}/>
|
||||
</Button.Text>
|
||||
</div>
|
||||
{buttons &&
|
||||
<div className={'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'}>
|
||||
{buttons}
|
||||
</div>
|
||||
}
|
||||
{/* Keep this below the other buttons so that it isn't the default focus if they're present. */}
|
||||
{!hideCloseIcon &&
|
||||
<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>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</HeadlessDialog>
|
||||
</Transition>
|
||||
</div>
|
||||
</HDialog>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
29
resources/scripts/components/elements/dialog/DialogIcon.tsx
Normal file
29
resources/scripts/components/elements/dialog/DialogIcon.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -1,2 +1 @@
|
|||
export { default as Dialog } from './Dialog';
|
||||
export { default as styles } from './style.module.css';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import {
|
|||
useRole,
|
||||
} from '@floating-ui/react-dom-interactions';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
rest?: number;
|
||||
|
@ -30,11 +31,11 @@ interface Props {
|
|||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
const arrowSides: Record<Side, Side> = {
|
||||
top: 'bottom',
|
||||
bottom: 'top',
|
||||
left: 'right',
|
||||
right: 'left',
|
||||
const arrowSides: Record<Side, string> = {
|
||||
top: 'bottom-[-6px] left-0',
|
||||
bottom: 'top-[-6px] left-0',
|
||||
right: 'top-0 left-[-6px]',
|
||||
left: 'top-0 right-[-6px]',
|
||||
};
|
||||
|
||||
export default ({
|
||||
|
@ -103,9 +104,8 @@ export default ({
|
|||
ref={arrowEl}
|
||||
style={{
|
||||
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>
|
||||
|
|
Loading…
Reference in a new issue