feat(tooltip): arrow support added

This commit is contained in:
Junior Garcia 2023-03-25 10:23:50 -03:00
parent 5c9b6fdc6c
commit 938f8850c6
9 changed files with 223 additions and 117 deletions

View File

@ -4,7 +4,6 @@ import {MouseEvent, CSSProperties, useRef, useState} from "react";
export type DripInstance = {
key: number;
style: CSSProperties;
"data-drip": boolean;
};
export function useDrip() {
@ -33,7 +32,6 @@ export function useDrip() {
{
key: nextKey.current,
style: dripStyle,
"data-drip": true,
},
]);
nextKey.current++;

View File

@ -20,8 +20,10 @@ const Tooltip = forwardRef<TooltipProps, "div">((props, ref) => {
placement,
disableAnimation,
motionProps,
showArrow,
getTriggerProps,
getTooltipProps,
getArrowProps,
} = useTooltip({
ref,
...props,
@ -43,6 +45,12 @@ const Tooltip = forwardRef<TooltipProps, "div">((props, ref) => {
warn("Tooltip must have only one child node. Please, check your code.");
}
const arrowContent = useMemo(() => {
if (!showArrow) return null;
return <span {...getArrowProps()} />;
}, [showArrow, getArrowProps]);
const animatedContent = useMemo(() => {
const {className, ...otherTooltipProps} = getTooltipProps();
@ -58,18 +66,25 @@ const Tooltip = forwardRef<TooltipProps, "div">((props, ref) => {
variants={scale}
{...motionProps}
>
<Component className={className}>{content}</Component>
<Component className={className}>
{content}
{arrowContent}
</Component>
</motion.div>
</div>
);
}, [getTooltipProps, placement, motionProps, Component, content]);
}, [getTooltipProps, placement, motionProps, Component, content, arrowContent]);
return (
<>
{trigger}
{disableAnimation && isOpen ? (
<OverlayContainer>
<Component {...getTooltipProps()}>{content}</Component>;
<Component {...getTooltipProps()}>
{content}
{arrowContent}
</Component>
;
</OverlayContainer>
) : (
<AnimatePresence initial={false}>

View File

@ -1,4 +1,4 @@
import type {TooltipVariantProps} from "@nextui-org/theme";
import type {TooltipVariantProps, SlotsToClasses, TooltipSlots} from "@nextui-org/theme";
import type {AriaTooltipProps} from "@react-types/tooltip";
import type {OverlayTriggerProps} from "@react-types/overlays";
import type {HTMLMotionProps} from "framer-motion";
@ -11,11 +11,11 @@ import {useTooltip as useReactAriaTooltip, useTooltipTrigger} from "@react-aria/
import {useOverlayPosition, useOverlay, AriaOverlayProps} from "@react-aria/overlays";
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
import {tooltip} from "@nextui-org/theme";
import {ReactRef, mergeRefs} from "@nextui-org/shared-utils";
import {ReactRef, mergeRefs, clsx} from "@nextui-org/shared-utils";
import {createDOMRef} from "@nextui-org/dom-utils";
import {useMemo, useRef, useCallback} from "react";
import {toReactAriaPlacement} from "./utils";
import {toReactAriaPlacement, getArrowPlacement} from "./utils";
export interface UseTooltipProps
extends HTMLNextUIProps<"div", TooltipVariantProps>,
@ -38,6 +38,11 @@ export interface UseTooltipProps
* Whether the tooltip should be disabled, independent from the trigger.
*/
isDisabled?: boolean;
/**
* Whether the tooltip should have an arrow.
* @default false
*/
showArrow?: boolean;
/**
* The delay time for the tooltip to show up.
* @default 0
@ -51,9 +56,15 @@ export interface UseTooltipProps
/**
* The additional offset applied along the main axis between the element and its
* anchor element.
* @default 7 (px)
* @default 7 (px) - if `showArrow` is true the default value is 10 (px)
*/
offset?: number;
/**
* The additional offset applied along the cross axis between the element and its
* anchor element.
* @default 0
*/
crossOffset?: number;
/**
* Whether the element should flip its orientation (e.g. top to bottom or left to right) when
* there is insufficient room for it to render completely.
@ -79,6 +90,19 @@ export interface UseTooltipProps
* The properties passed to the underlying `Collapse` component.
*/
motionProps?: HTMLMotionProps<"div">;
/**
* Classname or List of classes to change the styles of the element.
* if `className` is passed, it will be added to the base slot.
*
* @example
* ```ts
* <Tooltip styles={{
* base:"base-classes",
* arrow: "arrow-classes",
* }} />
* ```
*/
styles?: SlotsToClasses<TooltipSlots>;
/** Handler that is called when the overlay should close. */
onClose?: () => void;
}
@ -101,7 +125,9 @@ export function useTooltip(originalProps: UseTooltipProps) {
placement: placementProp = "top",
delay = 0,
closeDelay = 0,
showArrow = false,
offset = 7,
crossOffset = 0,
isDismissable = true,
shouldCloseOnBlur = false,
isKeyboardDismissDisabled = false,
@ -109,6 +135,7 @@ export function useTooltip(originalProps: UseTooltipProps) {
className,
onClose,
motionProps,
styles,
...otherProps
} = props;
@ -166,14 +193,15 @@ export function useTooltip(originalProps: UseTooltipProps) {
const {
overlayProps: positionProps,
// arrowProps,
// placement,
arrowProps,
placement,
} = useOverlayPosition({
isOpen: isOpen,
targetRef: triggerRef,
placement: toReactAriaPlacement(placementProp),
overlayRef,
offset,
offset: showArrow ? offset + 3 : offset,
crossOffset,
shouldFlip,
containerPadding,
});
@ -190,15 +218,16 @@ export function useTooltip(originalProps: UseTooltipProps) {
overlayRef,
);
const styles = useMemo(
const slots = useMemo(
() =>
tooltip({
...variantProps,
className,
}),
[...Object.values(variantProps), className],
[...Object.values(variantProps)],
);
const baseStyles = clsx(styles?.base, className);
const getTriggerProps = useCallback<PropGetter>(
(props = {}, _ref: Ref<any> | null | undefined = null) => ({
...mergeProps(triggerProps, props),
@ -212,10 +241,19 @@ export function useTooltip(originalProps: UseTooltipProps) {
const getTooltipProps = useCallback<PropGetter>(
() => ({
ref: overlayRef,
className: styles,
className: slots.base({class: baseStyles}),
...mergeProps(tooltipProps, positionProps, overlayProps, otherProps),
}),
[overlayRef, styles, tooltipProps, positionProps, overlayProps, otherProps],
[overlayRef, slots, styles, tooltipProps, positionProps, overlayProps, otherProps],
);
const getArrowProps = useCallback<PropGetter>(
() => ({
className: slots.arrow({class: styles?.arrow}),
"data-placement": getArrowPlacement(placement, placementProp),
...arrowProps,
}),
[arrowProps, placement, slots, styles],
);
return {
@ -225,12 +263,14 @@ export function useTooltip(originalProps: UseTooltipProps) {
isOpen,
triggerRef,
triggerProps,
showArrow,
placement: placementProp,
disableAnimation: originalProps?.disableAnimation,
isDisabled,
motionProps,
getTriggerProps,
getTooltipProps,
getArrowProps,
};
}

View File

@ -1,6 +1,6 @@
import type {TooltipPlacement} from "./types";
import {Placement} from "@react-types/overlays";
import {Placement, PlacementAxis} from "@react-types/overlays";
export const getOrigins = (placement: TooltipPlacement) => {
const origins: Record<
@ -77,3 +77,13 @@ export const toReactAriaPlacement = (placement: TooltipPlacement) => {
return mapPositions[placement];
};
export const getArrowPlacement = (dynamicPlacement: PlacementAxis, placement: TooltipPlacement) => {
if (placement.includes("-")) {
const [, position] = placement.split("-");
return `${dynamicPlacement}-${position}`;
}
return dynamicPlacement;
};

View File

@ -21,12 +21,6 @@ export default {
options: ["neutral", "foreground", "primary", "secondary", "success", "warning", "danger"],
},
},
size: {
control: {
type: "select",
options: ["xs", "sm", "md", "lg", "xl"],
},
},
radius: {
control: {
type: "select",
@ -174,32 +168,30 @@ const VariantsTemplate: ComponentStory<typeof Tooltip> = (args: TooltipProps) =>
return (
<div className="flex gap-2">
<Tooltip {...args} content="Tooltip 1" variant="solid">
<Button color={buttonColor} size="sm">
Solid
</Button>
<Button color={buttonColor}>Solid</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 2" variant="bordered">
<Button color={buttonColor} size="sm" variant="bordered">
<Button color={buttonColor} variant="bordered">
Bordered
</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 3" variant="light">
<Button color={buttonColor} size="sm" variant="light">
<Button color={buttonColor} variant="light">
Light
</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 4" variant="flat">
<Button color={buttonColor} size="sm" variant="flat">
<Button color={buttonColor} variant="flat">
Flat
</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 5" variant="faded">
<Button color={buttonColor} size="sm" variant="faded">
<Button color={buttonColor} variant="faded">
Faded
</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 6" variant="shadow">
<Button color={buttonColor} size="sm" variant="shadow">
<Button color={buttonColor} variant="shadow">
Shadow
</Button>
</Tooltip>
@ -314,6 +306,12 @@ Default.args = {
...defaultProps,
};
export const WithArrow = Template.bind({});
WithArrow.args = {
...defaultProps,
showArrow: true,
};
export const OpenChange = OpenChangeTemplate.bind({});
OpenChange.args = {
...defaultProps,
@ -327,7 +325,6 @@ Variants.args = {
export const Placements = PlacementsTemplate.bind({});
Placements.args = {
...defaultProps,
size: "sm",
color: "primary",
};
@ -395,6 +392,13 @@ export const AlwaysOpen = Template.bind({});
AlwaysOpen.args = {
...defaultProps,
isOpen: true,
showArrow: true,
content: (
<div className="px-1 py-2">
<div className="text-sm font-bold">Custom Content</div>
<div className="text-xs">This is a custom tooltip content</div>
</div>
),
};
export const Disabled = Template.bind({});

View File

@ -14,12 +14,13 @@ import {ringClasses, colorVariants} from "../utils";
*/
const button = tv({
base: [
"z-0",
"relative",
"inline-flex",
"items-center",
"justify-center",
"box-border",
"apparance-none",
"appearance-none",
"outline-none",
"select-none",
"font-medium",

View File

@ -6,18 +6,10 @@ import {tv} from "tailwind-variants";
* const styles = drip({...})
*
* @example
* <span ref={dripRef} className={styles())} data-drip="true/false" />
* <span ref={dripRef} className={styles())} />
*/
const drip = tv({
base: [
"hidden",
"absolute",
"bg-current",
"rounded-full",
"pointer-events-none",
'data-[drip="true"]:block',
'data-[drip="true"]:animate-drip-expand',
],
base: ["absolute", "will-change-transform", "bg-current", "rounded-full", "animate-drip-expand"],
});
export {drip};

View File

@ -4,60 +4,106 @@ import {colorVariants} from "../utils";
/**
* Tooltip wrapper **Tailwind Variants** component
*
* const styles = tooltip({...})
* const { base, arrow } = tooltip({...})
*
* @example
* <div>
* <button>your trigger</button>
* <div role="tooltip" className={styles}>
* <div role="tooltip" className={base()}>
* // tooltip content
* <span className={arrow()} data-placement="top/bottom/left/right..." /> // arrow
* </div>
* </div>
*/
const tooltip = tv({
base: ["inline-flex", "flex-col", "items-center", "justify-center", "box-border"],
slots: {
base: [
"z-10",
"inline-flex",
"flex-col",
"items-center",
"justify-center",
"box-border",
"subpixel-antialiased",
"px-4",
"py-1",
"text-base",
],
arrow: [
"-z-10",
"absolute",
"rotate-45",
"bg-inherit",
"w-2.5",
"h-2.5",
"rounded-sm",
// top
"data-[placement=top]:-bottom-1",
"data-[placement=top]:-translate-x-1/2",
"data-[placement=top-start]:-bottom-1",
"data-[placement=top-start]:-translate-x-8",
"data-[placement=top-end]:-bottom-1",
"data-[placement=top-end]:translate-x-6",
// bottom
"data-[placement=bottom]:-top-1",
"data-[placement=bottom]:-translate-x-1/2",
"data-[placement=bottom-start]:-top-1",
"data-[placement=bottom-start]:-translate-x-8",
"data-[placement=bottom-end]:-top-1",
"data-[placement=bottom-end]:translate-x-6",
// left
"data-[placement=left]:-right-1",
"data-[placement=left]:-translate-y-1/2",
"data-[placement=left-start]:-right-1",
"data-[placement=left-start]:-translate-y-3",
"data-[placement=left-end]:-right-1",
"data-[placement=left-end]:translate-y-0.5",
// right
"data-[placement=right]:-left-1",
"data-[placement=right]:-translate-y-1/2",
"data-[placement=right-start]:-left-1",
"data-[placement=right-start]:-translate-y-3",
"data-[placement=right-end]:-left-1",
"data-[placement=right-end]:translate-y-0.5",
],
},
variants: {
variant: {
solid: "",
bordered: "border-2 !bg-transparent",
light: "!bg-transparent",
faded: "border-1.5",
flat: "",
shadow: "",
solid: {base: ""},
bordered: {
base: "border-2 !bg-background",
arrow: "border-2 border-inherit !bg-background",
},
light: {base: "!bg-transparent", arrow: "hidden"},
faded: {base: "border-1.5", arrow: "border-1.5 border-inherit"},
flat: {base: ""},
shadow: {base: ""},
},
color: {
neutral: colorVariants.solid.neutral,
foreground: colorVariants.solid.foreground,
primary: colorVariants.solid.primary,
secondary: colorVariants.solid.secondary,
success: colorVariants.solid.success,
warning: colorVariants.solid.warning,
danger: colorVariants.solid.danger,
},
size: {
xs: "px-2 py-0.5 text-xs",
sm: "px-3 py-0.5 text-sm",
md: "px-4 py-1 text-base",
lg: "px-6 py-1 text-lg",
xl: "px-8 py-2 text-xl",
neutral: {base: colorVariants.solid.neutral},
foreground: {base: colorVariants.solid.foreground},
primary: {base: colorVariants.solid.primary},
secondary: {base: colorVariants.solid.secondary},
success: {base: colorVariants.solid.success},
warning: {base: colorVariants.solid.warning},
danger: {base: colorVariants.solid.danger},
},
radius: {
none: "rounded-none",
base: "rounded",
sm: "rounded-sm",
md: "rounded-md",
lg: "rounded-lg",
xl: "rounded-xl",
full: "rounded-full",
none: {base: "rounded-none"},
base: {base: "rounded"},
sm: {base: "rounded-sm"},
md: {base: "rounded-md"},
lg: {base: "rounded-lg"},
xl: {base: "rounded-xl"},
full: {base: "rounded-full"},
},
disableAnimation: {
true: "animate-none",
true: {base: "animate-none"},
},
},
defaultVariants: {
variant: "solid",
color: "neutral",
size: "md",
radius: "lg",
},
compoundVariants: [
@ -65,187 +111,183 @@ const tooltip = tv({
{
variant: "shadow",
color: "neutral",
class: colorVariants.shadow.neutral,
class: {
base: colorVariants.shadow.neutral,
},
},
{
variant: "shadow",
color: "foreground",
class: colorVariants.shadow.foreground,
class: {base: colorVariants.shadow.foreground},
},
{
variant: "shadow",
color: "primary",
class: colorVariants.shadow.primary,
class: {base: colorVariants.shadow.primary},
},
{
variant: "shadow",
color: "secondary",
class: colorVariants.shadow.secondary,
class: {base: colorVariants.shadow.secondary},
},
{
variant: "shadow",
color: "success",
class: colorVariants.shadow.success,
class: {base: colorVariants.shadow.success},
},
{
variant: "shadow",
color: "warning",
class: colorVariants.shadow.warning,
class: {base: colorVariants.shadow.warning},
},
{
variant: "shadow",
color: "danger",
class: colorVariants.shadow.danger,
class: {base: colorVariants.shadow.danger},
},
// bordered / color
{
variant: "bordered",
color: "neutral",
class: colorVariants.bordered.neutral,
class: {base: colorVariants.bordered.neutral},
},
{
variant: "bordered",
color: "foreground",
class: colorVariants.bordered.foreground,
class: {base: colorVariants.bordered.foreground},
},
{
variant: "bordered",
color: "primary",
class: colorVariants.bordered.primary,
class: {base: colorVariants.bordered.primary},
},
{
variant: "bordered",
color: "secondary",
class: colorVariants.bordered.secondary,
class: {base: colorVariants.bordered.secondary},
},
{
variant: "bordered",
color: "success",
class: colorVariants.bordered.success,
class: {base: colorVariants.bordered.success},
},
{
variant: "bordered",
color: "warning",
class: colorVariants.bordered.warning,
class: {base: colorVariants.bordered.warning},
},
{
variant: "bordered",
color: "danger",
class: colorVariants.bordered.danger,
class: {base: colorVariants.bordered.danger},
},
// flat / color
{
variant: "flat",
color: "neutral",
class: colorVariants.flat.neutral,
class: {base: colorVariants.flat.neutral},
},
{
variant: "flat",
color: "foreground",
class: colorVariants.flat.foreground,
class: {base: colorVariants.flat.foreground},
},
{
variant: "flat",
color: "primary",
class: colorVariants.flat.primary,
class: {base: colorVariants.flat.primary},
},
{
variant: "flat",
color: "secondary",
class: colorVariants.flat.secondary,
class: {base: colorVariants.flat.secondary},
},
{
variant: "flat",
color: "success",
class: colorVariants.flat.success,
class: {base: colorVariants.flat.success},
},
{
variant: "flat",
color: "warning",
class: colorVariants.flat.warning,
class: {base: colorVariants.flat.warning},
},
{
variant: "flat",
color: "danger",
class: colorVariants.flat.danger,
class: {base: colorVariants.flat.danger},
},
// faded / color
{
variant: "faded",
color: "neutral",
class: colorVariants.faded.neutral,
class: {base: colorVariants.faded.neutral},
},
{
variant: "faded",
color: "foreground",
class: colorVariants.faded.foreground,
class: {base: colorVariants.faded.foreground},
},
{
variant: "faded",
color: "primary",
class: colorVariants.faded.primary,
class: {base: colorVariants.faded.primary},
},
{
variant: "faded",
color: "secondary",
class: colorVariants.faded.secondary,
class: {base: colorVariants.faded.secondary},
},
{
variant: "faded",
color: "success",
class: colorVariants.faded.success,
class: {base: colorVariants.faded.success},
},
{
variant: "faded",
color: "warning",
class: colorVariants.faded.warning,
class: {base: colorVariants.faded.warning},
},
{
variant: "faded",
color: "danger",
class: colorVariants.faded.danger,
class: {base: colorVariants.faded.danger},
},
// light / color
{
variant: "light",
color: "neutral",
class: colorVariants.light.neutral,
class: {base: colorVariants.light.neutral},
},
{
variant: "light",
color: "foreground",
class: colorVariants.light.foreground,
class: {base: colorVariants.light.foreground},
},
{
variant: "light",
color: "primary",
class: colorVariants.light.primary,
class: {base: colorVariants.light.primary},
},
{
variant: "light",
color: "secondary",
class: colorVariants.light.secondary,
class: {base: colorVariants.light.secondary},
},
{
variant: "light",
color: "success",
class: colorVariants.light.success,
class: {base: colorVariants.light.success},
},
{
variant: "light",
color: "warning",
class: colorVariants.light.warning,
class: {base: colorVariants.light.warning},
},
{
variant: "light",
color: "danger",
class: colorVariants.light.danger,
},
// size (xs) / bordered
{
size: "xs",
variant: "bordered",
class: "border-1.5",
class: {base: colorVariants.light.danger},
},
],
});

View File

@ -46,6 +46,10 @@ const defaultTransitions = {
duration: 0.3,
ease: TRANSITION_EASINGS.ease,
},
y: {
duration: 0.3,
ease: TRANSITION_EASINGS.ease,
},
},
enter: {
height: {