feat: create reusable tooltip

This commit is contained in:
MAZE 2023-10-29 18:12:06 +03:30
parent 0888aaa0f0
commit c637e2d631
5 changed files with 149 additions and 64 deletions

View file

@ -1,19 +1,8 @@
import { useState } from 'react';
import { BiUndo, BiTrash } from 'react-icons/bi/index'; import { BiUndo, BiTrash } from 'react-icons/bi/index';
import {
useFloating,
autoUpdate,
offset,
flip,
shift,
useHover,
useFocus,
useDismiss,
useRole,
useInteractions,
} from '@floating-ui/react';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { Tooltip } from '@/components/tooltip';
import { useSoundStore } from '@/store'; import { useSoundStore } from '@/store';
import { cn } from '@/helpers/styles'; import { cn } from '@/helpers/styles';
import { fade, mix, slideX } from '@/lib/motion'; import { fade, mix, slideX } from '@/lib/motion';
@ -21,28 +10,6 @@ import { fade, mix, slideX } from '@/lib/motion';
import styles from './unselect.module.css'; import styles from './unselect.module.css';
export function UnselectButton() { export function UnselectButton() {
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
const { context, floatingStyles, refs } = useFloating({
middleware: [offset(15), flip(), shift()],
onOpenChange: setIsTooltipOpen,
open: isTooltipOpen,
placement: 'top',
whileElementsMounted: autoUpdate,
});
const hover = useHover(context, { move: false });
const focus = useFocus(context);
const dismiss = useDismiss(context);
const role = useRole(context, { role: 'tooltip' });
const { getFloatingProps, getReferenceProps } = useInteractions([
hover,
focus,
dismiss,
role,
]);
const noSelected = useSoundStore(state => state.noSelected()); const noSelected = useSoundStore(state => state.noSelected());
const restoreHistory = useSoundStore(state => state.restoreHistory); const restoreHistory = useSoundStore(state => state.restoreHistory);
const hasHistory = useSoundStore(state => !!state.history); const hasHistory = useSoundStore(state => !!state.history);
@ -59,13 +26,20 @@ export function UnselectButton() {
exit="hidden" exit="hidden"
initial="hidden" initial="hidden"
variants={variants} variants={variants}
>
<Tooltip
content={
hasHistory
? 'Restore unselected sounds.'
: 'Unselect all sounds.'
}
> >
<button <button
disabled={noSelected && !hasHistory} disabled={noSelected && !hasHistory}
ref={refs.setReference}
{...getReferenceProps}
aria-label={ aria-label={
hasHistory ? 'Restore Unselected Sounds' : 'Unselect All Sounds' hasHistory
? 'Restore Unselected Sounds'
: 'Unselect All Sounds'
} }
className={cn( className={cn(
styles.unselectButton, styles.unselectButton,
@ -79,19 +53,10 @@ export function UnselectButton() {
> >
{hasHistory ? <BiUndo /> : <BiTrash />} {hasHistory ? <BiUndo /> : <BiTrash />}
</button> </button>
</Tooltip>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
{isTooltipOpen && (
<div
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps({ className: styles.tooltip })}
>
{hasHistory ? 'Restore unselected sounds.' : 'Unselect all sounds.'}
</div>
)}
</> </>
); );
} }

View file

@ -0,0 +1 @@
export { Tooltip } from './tooltip';

View file

@ -0,0 +1,8 @@
.tooltip {
width: max-content;
padding: 6px 12px;
border: 1px solid var(--color-neutral-200);
border-radius: 100px;
background-color: var(--color-neutral-100);
font-size: var(--font-xsm);
}

View file

@ -0,0 +1,111 @@
import { useState, cloneElement } from 'react';
import {
useFloating,
autoUpdate,
offset,
flip,
shift,
useHover,
useFocus,
useDismiss,
useRole,
useInteractions,
type Placement,
} from '@floating-ui/react';
import { motion, AnimatePresence } from 'framer-motion';
import styles from './tooltip.module.css';
interface TooltipProps {
children: JSX.Element;
content: React.ReactNode;
hideDelay?: number;
placement?: Placement;
showDelay?: number;
}
export function Tooltip({
children,
content,
hideDelay = 100,
placement = 'top',
showDelay = 500,
}: TooltipProps) {
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
const {
context,
floatingStyles,
placement: computedPlacement,
refs,
strategy,
x,
y,
} = useFloating({
middleware: [offset(12), flip(), shift()],
onOpenChange: setIsTooltipOpen,
open: isTooltipOpen,
placement: placement,
whileElementsMounted: autoUpdate,
});
const hover = useHover(context, {
delay: showDelay,
move: false,
restMs: hideDelay,
});
const focus = useFocus(context);
const dismiss = useDismiss(context);
const role = useRole(context, { role: 'tooltip' });
const { getFloatingProps, getReferenceProps } = useInteractions([
hover,
focus,
dismiss,
role,
]);
const translate = {
bottom: { translateY: -5 },
left: { translateX: 5 },
right: { translateX: -5 },
top: { translateY: 5 },
}[
computedPlacement.includes('-')
? computedPlacement.split('-')[0]
: computedPlacement
];
const variants = {
hidden: { opacity: 0, ...translate },
show: { opacity: 1, translateX: 0, translateY: 0 },
};
return (
<>
{cloneElement(
children,
getReferenceProps({ ref: refs.setReference, ...children.props }),
)}
<AnimatePresence>
{isTooltipOpen && (
<motion.div
animate="show"
exit="hidden"
initial="hidden"
ref={refs.setFloating}
style={floatingStyles}
variants={variants}
{...getFloatingProps({
className: styles.tooltip,
style: { left: x ?? 0, position: strategy, top: y ?? 0 },
})}
>
{content}
</motion.div>
)}
</AnimatePresence>
</>
);
}

View file

@ -30,8 +30,8 @@ export function slideX(from = -10, to = 0): Motion {
export function slideY(from = -10, to = 0): Motion { export function slideY(from = -10, to = 0): Motion {
return { return {
hidden: { Y: from }, hidden: { y: from },
show: { Y: to }, show: { y: to },
}; };
} }