feat: tooltip improved, new drip handler created, button improved

This commit is contained in:
Junior Garcia 2023-03-21 23:26:16 -03:00
parent 133f1b494a
commit f9b3e0b7bc
24 changed files with 894 additions and 561 deletions

View File

@ -71,6 +71,6 @@
"babel-plugin-module-resolver": "^4.1.0",
"eslint-config-next": "^11.0.0",
"next-sitemap": "^1.6.140",
"typescript": "^5.0.2"
"typescript": "^4.9.5"
}
}

View File

@ -26,7 +26,7 @@
"@types/react": "^17.0.24",
"@types/react-dom": "^17.0.9",
"eslint": "^8.11.0",
"typescript": "^5.0.2"
"typescript": "^4.9.5"
},
"engines": {
"node": ">=14"

View File

@ -15,7 +15,7 @@
"@types/react": "^17.0.33",
"@types/react-dom": "^17.0.10",
"@vitejs/plugin-react": "^1.0.7",
"typescript": "^5.0.2",
"typescript": "^4.9.5",
"vite": "^2.8.0"
}
}

View File

@ -13,7 +13,7 @@
},
"scripts": {
"dev": "turbo dev --filter=!@nextui-org/docs",
"build": "turbo build --filter=!@nextui-org/docs --filter=!@nextui-org/snippet",
"build": "turbo build --filter=!@nextui-org/docs",
"build:fast": "turbo build:fast --filter=!@nextui-org/docs",
"dev:docs": "turbo dev --filter=@nextui-org/docs",
"build:docs": "turbo build --filter=@nextui-org/docs",
@ -125,7 +125,7 @@
"shelljs": "^0.8.4",
"tsup": "6.4.0",
"turbo": "1.6.3",
"typescript": "^5.0.2",
"typescript": "^4.9.5",
"uuid": "^8.3.2",
"webpack": "^5.53.0",
"webpack-bundle-analyzer": "^4.4.2",

View File

@ -1,6 +1,6 @@
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {accordionItem, link} from "@nextui-org/theme";
import {accordionItem} from "@nextui-org/theme";
import {
AnchorIcon,
MoonIcon,
@ -135,14 +135,8 @@ const TemplateWithLeftIndicator: ComponentStory<typeof Accordion> = (args: Accor
/>
}
subtitle={
<p>
2 issues to{" "}
<a
className={link({size: "sm"})}
href="/?path=/story/components-accordion--with-left-indicator"
>
fix now
</a>
<p className="flex">
2 issues to&nbsp;<p className="text-primary">fix now</p>
</p>
}
title="Zoey Lang"
@ -270,14 +264,8 @@ const CustomWithStylesTemplate: ComponentStory<typeof Accordion> = (args: Accord
leftIndicator={<MonitorMobileIcon className="text-primary" />}
styles={itemStyles}
subtitle={
<p>
2 issues to{" "}
<a
className={link({size: "sm"})}
href="/?path=/story/components-accordion--custom-with-styles"
>
fix now
</a>
<p className="flex">
2 issues to&nbsp;<p className="text-primary">fix now</p>
</p>
}
title="Connected devices"

View File

@ -1,5 +1,5 @@
import {forwardRef} from "@nextui-org/system";
import {Drip} from "@nextui-org/drip";
import {forwardRef} from "@nextui-org/system";
import {UseButtonProps, useButton} from "./use-button";
@ -11,10 +11,10 @@ const Button = forwardRef<ButtonProps, "button">((props, ref) => {
domRef,
children,
styles,
drips,
leftIcon,
rightIcon,
disableRipple,
dripBindings,
getButtonProps,
} = useButton({
ref,
@ -26,15 +26,7 @@ const Button = forwardRef<ButtonProps, "button">((props, ref) => {
{leftIcon}
{children}
{rightIcon}
{!disableRipple && (
<Drip
{...dripBindings}
styles={{
base: "opacity-30",
svg: "text-inherit",
}}
/>
)}
{!disableRipple && <Drip drips={drips} />}
</Component>
);
});

View File

@ -1,17 +1,16 @@
import type {ButtonVariantProps} from "@nextui-org/theme";
import type {AriaButtonProps} from "@react-types/button";
import type {PressEvent} from "@react-types/shared";
import type {ReactRef} from "@nextui-org/shared-utils";
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
import type {ReactNode} from "react";
import {dataAttr, ReactRef} from "@nextui-org/shared-utils";
import {MouseEventHandler, useCallback} from "react";
import {useButton as useAriaButton} from "@react-aria/button";
import {useFocusRing} from "@react-aria/focus";
import {mergeProps} from "@react-aria/utils";
import {chain, mergeProps} from "@react-aria/utils";
import {useDrip} from "@nextui-org/drip";
import {useDOMRef} from "@nextui-org/dom-utils";
import {warn, clsx} from "@nextui-org/shared-utils";
import {clsx} from "@nextui-org/shared-utils";
import {button} from "@nextui-org/theme";
import {isValidElement, cloneElement, useMemo} from "react";
@ -65,12 +64,8 @@ export function useButton(props: UseButtonProps) {
radius = groupContext?.radius ?? "lg",
disableRipple = groupContext?.disableRipple ?? false,
isDisabled = groupContext?.isDisabled ?? false,
onClick: deprecatedOnClick,
onPress,
onPressStart,
onPressEnd,
onPressChange,
onPressUp,
onClick,
...otherProps
} = props;
@ -78,7 +73,7 @@ export function useButton(props: UseButtonProps) {
const domRef = useDOMRef(ref);
const {isFocusVisible, focusProps} = useFocusRing({
const {isFocusVisible, isFocused, focusProps} = useFocusRing({
autoFocus,
});
@ -110,44 +105,32 @@ export function useButton(props: UseButtonProps) {
],
);
const {onClick: onDripClickHandler, ...dripBindings} = useDrip(false, domRef);
const {onClick: onDripClickHandler, drips} = useDrip();
const handleDrip = (e: React.MouseEvent<HTMLButtonElement> | PressEvent | Event) => {
const handleDrip = (e: React.MouseEvent<HTMLButtonElement>) => {
if (disableRipple || isDisabled || disableAnimation) return;
domRef.current && onDripClickHandler(e);
};
const handlePress = (e: PressEvent) => {
if (isDisabled) return;
if (e.pointerType === "keyboard" || e.pointerType === "virtual") {
handleDrip(e);
} else if (typeof window !== "undefined" && window.event) {
handleDrip(window.event);
}
if (deprecatedOnClick) {
deprecatedOnClick(e as any);
warn("onClick is deprecated, please use onPress", "Button");
}
onPress?.(e);
};
const {buttonProps: buttonAriaProps} = useAriaButton(
const {buttonProps: ariaButtonProps} = useAriaButton(
{
...otherProps,
elementType: as,
onPress: handlePress,
onPressStart,
onPressEnd,
onPressChange,
onPressUp,
isDisabled,
onPress,
onClick: chain(onClick, handleDrip),
...otherProps,
} as AriaButtonProps,
domRef,
);
const getButtonProps: PropGetter = useCallback(
() => mergeProps(buttonAriaProps, focusProps, otherProps),
[buttonAriaProps, focusProps, otherProps],
() => ({
"data-disabled": dataAttr(isDisabled),
"data-focus-visible": dataAttr(isFocusVisible),
"data-focused": dataAttr(isFocused),
...mergeProps(ariaButtonProps, focusProps, otherProps),
}),
[ariaButtonProps, focusProps, otherProps],
);
const getIconClone = (icon: ReactNode) =>
@ -170,10 +153,10 @@ export function useButton(props: UseButtonProps) {
Component,
children,
domRef,
drips,
styles,
leftIcon,
rightIcon,
dripBindings,
disableRipple,
getButtonProps,
};

View File

@ -59,11 +59,30 @@ const defaultProps = {
const Template: ComponentStory<typeof Button> = (args: ButtonProps) => <Button {...args} />;
const StateTemplate: ComponentStory<typeof Button> = (args: ButtonProps) => {
const [isOpen, setIsOpen] = React.useState(false);
const handleClick = () => {
setIsOpen((prev) => !prev);
};
return (
<Button {...args} onPress={handleClick}>
{isOpen ? "Close" : "Open"}
</Button>
);
};
export const Default = Template.bind({});
Default.args = {
...defaultProps,
};
export const WithState = StateTemplate.bind({});
WithState.args = {
...defaultProps,
};
export const IsDisabled = Template.bind({});
IsDisabled.args = {
...defaultProps,

View File

@ -1,78 +1,30 @@
import type {DripSlots, SlotsToClasses} from "@nextui-org/theme";
import type {HTMLNextUIProps} from "@nextui-org/system";
import type {DripInstance} from "./use-drip";
import {useEffect} from "react";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import {drip} from "@nextui-org/theme";
export interface DripProps extends HTMLNextUIProps<"div"> {
isVisible?: boolean;
x: number;
y: number;
color?: string;
onCompleted: () => void;
/**
* Classname or List of classes to change the styles of the avatar.
* if `className` is passed, it will be added to the base slot.
*
* @example
* ```ts
* <Drip styles={{
* base:"base-classes",
* svg: "svg-classes",
* }} />
* ```
*/
styles?: SlotsToClasses<DripSlots>;
export interface DripProps extends HTMLNextUIProps<"span"> {
drips?: DripInstance[];
}
const Drip = forwardRef<DripProps, "div">((props, ref) => {
const {isVisible, x, y, color, onCompleted, styles, className, ...otherProps} = props;
const Drip = (props: DripProps) => {
const {drips, ...otherProps} = props;
const domRef = useDOMRef(ref);
const styles = drip();
const slots = drip();
const baseStyles = clsx(styles?.base, className);
const top = Number.isNaN(+y) ? 0 : y - 10;
const left = Number.isNaN(+x) ? 0 : x - 10;
useEffect(() => {
let drip = domRef.current;
if (!drip) return;
drip.addEventListener("animationend", onCompleted);
return () => {
if (!drip) return;
drip.removeEventListener("animationend", onCompleted);
};
});
if (!isVisible) return null;
if (!drips || !Array.isArray(drips) || drips.length < 1) {
return null;
}
return (
<div ref={domRef} className={slots.base({class: baseStyles})} {...otherProps}>
<svg
className={slots.svg({class: styles?.svg})}
height="20%"
style={{top, left}}
viewBox="0 0 20 20"
width="20%"
>
<g fill="none" fillRule="evenodd" stroke="none" strokeWidth="1">
<g fill={color || "currentColor"}>
<rect height="100%" rx="10" width="100%" />
</g>
</g>
</svg>
</div>
<>
{drips.map(({key, ...dripProps}) => (
<span key={key} className={styles} {...otherProps} {...dripProps} />
))}
</>
);
});
};
if (__DEV__) {
Drip.displayName = "NextUI.Drip";
}
Drip.displayName = "NextUI.Drip";
export default Drip;

View File

@ -1,36 +1,51 @@
import {PressEvent} from "@react-types/shared";
import {MouseEvent, RefObject, useState} from "react";
import {Timer} from "@nextui-org/shared-utils";
import {MouseEvent, CSSProperties, useRef, useState} from "react";
export function useDrip(initialValue = false, ref: RefObject<HTMLElement>) {
const [isVisible, setIsVisible] = useState(initialValue);
const [dripX, setDripX] = useState(0);
const [dripY, setDripY] = useState(0);
export type DripInstance = {
key: number;
style: CSSProperties;
"data-drip": boolean;
};
const onCompleted = () => {
setIsVisible(false);
setDripX(0);
setDripY(0);
};
export function useDrip() {
const [drips, setDrips] = useState<DripInstance[]>([]);
const nextKey = useRef(0);
const onClick = (event: MouseEvent<HTMLElement> | PressEvent | Event) => {
if (!ref?.current) return;
const rect = ref.current.getBoundingClientRect?.();
const dripTimer = useRef<Timer>();
if (!rect) return;
const onClick = (event: MouseEvent<HTMLElement>) => {
const trigger = event.currentTarget;
setIsVisible(true);
if (typeof event === "object" && "clientX" in event) {
setDripX(event.clientX - rect.left);
setDripY(event.clientY - rect.top);
}
const size = Math.max(trigger.clientWidth, trigger.clientHeight);
const rect = trigger.getBoundingClientRect();
const x = event.clientX - rect.x - size / 2;
const y = event.clientY - rect.y - size / 2;
const dripStyle: CSSProperties = {
width: `${size}px`,
height: `${size}px`,
top: `${y}px`,
left: `${x}px`,
};
setDrips((prev) => [
...prev,
{
key: nextKey.current,
style: dripStyle,
"data-drip": true,
},
]);
nextKey.current++;
dripTimer.current = setTimeout(() => {
setDrips((prev) => prev.slice(1));
}, 340);
};
return {
isVisible,
x: dripX,
y: dripY,
drips,
onClick,
onCompleted,
};
}

View File

@ -63,6 +63,13 @@ export default {
},
},
},
decorators: [
(Story) => (
<div className="flex items-center justify-center w-screen h-screen">
<Story />
</div>
),
],
} as ComponentMeta<typeof Snippet>;
const defaultProps = {

View File

@ -52,9 +52,9 @@
"framer-motion": "^10.6.0"
},
"devDependencies": {
"@nextui-org/button": "workspace:*",
"@react-types/overlays": "^3.7.0",
"@react-types/tooltip": "^3.3.0",
"@nextui-org/button": "workspace:*",
"clean-package": "2.2.0",
"react": "^18.0.0"
},

View File

@ -1,8 +1,9 @@
import {forwardRef} from "@nextui-org/system";
import {warn} from "@nextui-org/shared-utils";
import {cloneElement, Children, useMemo} from "react";
import {useMemo} from "react";
import {OverlayContainer} from "@react-aria/overlays";
import {AnimatePresence, motion} from "framer-motion";
import {warn} from "@nextui-org/shared-utils";
import {Children, cloneElement} from "react";
import {UseTooltipProps, useTooltip} from "./use-tooltip";
import {scale} from "./tooltip-transition";
@ -42,44 +43,39 @@ const Tooltip = forwardRef<TooltipProps, "div">((props, ref) => {
warn("Tooltip must have only one child node. Please, check your code.");
}
const contentComponent = useMemo(() => {
if (disableAnimation) {
return <Component {...getTooltipProps()}>{content}</Component>;
}
const animatedContent = useMemo(() => {
const {className, ...otherTooltipProps} = getTooltipProps();
return (
<AnimatePresence initial={false}>
{isOpen && (
<div {...otherTooltipProps}>
<motion.div
animate="enter"
exit="exit"
initial="exit"
style={{
...getOrigins(placement),
}}
variants={scale}
{...motionProps}
>
<Component className={className}>{content}</Component>
</motion.div>
</div>
)}
</AnimatePresence>
<div {...otherTooltipProps}>
<motion.div
animate="enter"
exit="exit"
initial="exit"
style={{
...getOrigins(placement),
}}
variants={scale}
{...motionProps}
>
<Component className={className}>{content}</Component>
</motion.div>
</div>
);
}, [isOpen, content, disableAnimation, children, motionProps, getTooltipProps]);
}, [getTooltipProps, placement, motionProps, Component, content]);
return (
<>
{trigger}
<OverlayContainer>
{/* <CSSTransition {...transitionProps}>
<Component {...getTooltipProps()}>{content}</Component>
</CSSTransition> */}
{contentComponent}
</OverlayContainer>
{disableAnimation && isOpen ? (
<OverlayContainer>
<Component {...getTooltipProps()}>{content}</Component>;
</OverlayContainer>
) : (
<AnimatePresence initial={false}>
{isOpen ? <OverlayContainer>{animatedContent}</OverlayContainer> : null}
</AnimatePresence>
)}
</>
);
});

View File

@ -1 +1,13 @@
export type TooltipPlacement = "start" | "end" | "right" | "left" | "top" | "bottom";
export type TooltipPlacement =
| "top"
| "bottom"
| "right"
| "left"
| "top-start"
| "top-end"
| "bottom-start"
| "bottom-end"
| "left-start"
| "left-end"
| "right-start"
| "right-end";

View File

@ -1,10 +1,10 @@
import type {TooltipVariantProps} from "@nextui-org/theme";
import type {AriaTooltipProps} from "@react-types/tooltip";
import type {OverlayTriggerProps} from "@react-types/overlays";
import type {ReactNode, Ref} from "react";
import type {HTMLMotionProps} from "framer-motion";
import type {TooltipPlacement} from "./types";
import {ReactNode, Ref, useEffect, useImperativeHandle} from "react";
import {useTooltipTriggerState} from "@react-stately/tooltip";
import {mergeProps} from "@react-aria/utils";
import {useTooltip as useReactAriaTooltip, useTooltipTrigger} from "@react-aria/tooltip";
@ -12,8 +12,11 @@ import {useOverlayPosition, useOverlay, AriaOverlayProps} from "@react-aria/over
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
import {tooltip} from "@nextui-org/theme";
import {ReactRef, mergeRefs} from "@nextui-org/shared-utils";
import {createDOMRef} from "@nextui-org/dom-utils";
import {useMemo, useRef, useCallback} from "react";
import {toReactAriaPlacement} from "./utils";
export interface UseTooltipProps
extends HTMLNextUIProps<"div", TooltipVariantProps>,
AriaTooltipProps,
@ -86,13 +89,13 @@ export function useTooltip(originalProps: UseTooltipProps) {
const {
ref,
as,
isOpen,
isOpen: isOpenProp,
content,
children,
defaultOpen,
onOpenChange,
isDisabled,
trigger: triggerAction = "focus",
trigger: triggerAction,
shouldFlip = true,
containerPadding = 12,
placement: placementProp = "top",
@ -114,25 +117,38 @@ export function useTooltip(originalProps: UseTooltipProps) {
const state = useTooltipTriggerState({
delay,
isDisabled,
isOpen,
defaultOpen,
onOpenChange,
});
const triggerRef = useRef<HTMLElement>(null);
const overlayRef = useRef<HTMLElement>(null);
const domRef = mergeRefs(overlayRef, ref);
const overlayRef = useRef<HTMLDivElement>(null);
const immediate = closeDelay === 0;
const isOpen = state.isOpen && !isDisabled;
// Sync ref with overlayRef from passed ref.
useImperativeHandle(ref, () =>
// @ts-ignore
createDOMRef(overlayRef),
);
const handleClose = useCallback(() => {
onClose?.();
state.close(immediate);
}, [state, immediate, onClose]);
// control open state from outside
useEffect(() => {
if (isOpenProp === undefined) return;
if (isOpenProp !== state.isOpen) {
isOpenProp ? state.open() : handleClose();
}
}, [isOpenProp, handleClose]);
const {triggerProps, tooltipProps: triggerTooltipProps} = useTooltipTrigger(
{
delay,
isDisabled,
trigger: triggerAction,
},
@ -140,20 +156,22 @@ export function useTooltip(originalProps: UseTooltipProps) {
triggerRef,
);
const {tooltipProps} = useReactAriaTooltip(mergeProps(props, triggerTooltipProps), state);
tooltipProps.onPointerLeave = () => {
state.close(immediate);
};
const {tooltipProps} = useReactAriaTooltip(
{
isOpen,
...mergeProps(props, triggerTooltipProps),
},
state,
);
const {
overlayProps: positionProps,
// arrowProps,
// placement,
} = useOverlayPosition({
isOpen: state.isOpen,
isOpen: isOpen,
targetRef: triggerRef,
placement: placementProp,
placement: toReactAriaPlacement(placementProp),
overlayRef,
offset,
shouldFlip,
@ -162,8 +180,8 @@ export function useTooltip(originalProps: UseTooltipProps) {
const {overlayProps} = useOverlay(
{
isOpen: state.isOpen,
isDismissable: isDismissable && state.isOpen,
isOpen: isOpen,
isDismissable: isDismissable && isOpen,
onClose: handleClose,
shouldCloseOnBlur,
isKeyboardDismissDisabled,
@ -184,7 +202,7 @@ export function useTooltip(originalProps: UseTooltipProps) {
const getTriggerProps = useCallback<PropGetter>(
(props = {}, _ref: Ref<any> | null | undefined = null) => ({
...mergeProps(triggerProps, props),
ref: mergeRefs(triggerRef, _ref),
ref: mergeRefs(_ref, triggerRef),
onPointerEnter: () => state.open(),
onPointerLeave: () => isDismissable && state.close(),
}),
@ -193,19 +211,21 @@ export function useTooltip(originalProps: UseTooltipProps) {
const getTooltipProps = useCallback<PropGetter>(
() => ({
ref: domRef,
ref: overlayRef,
className: styles,
...mergeProps(tooltipProps, positionProps, overlayProps, otherProps),
}),
[domRef, styles, tooltipProps, positionProps, overlayProps, otherProps],
[overlayRef, styles, tooltipProps, positionProps, overlayProps, otherProps],
);
return {
Component,
content,
children,
isOpen,
triggerRef,
triggerProps,
placement: placementProp,
isOpen: state.isOpen,
disableAnimation: originalProps?.disableAnimation,
isDisabled,
motionProps,

View File

@ -1,26 +1,79 @@
import type {TooltipPlacement} from "./types";
import {Placement} from "@react-types/overlays";
export const getOrigins = (placement: TooltipPlacement) => {
const origins = {
const origins: Record<
TooltipPlacement,
{
originX?: number;
originY?: number;
}
> = {
top: {
originY: 1,
},
bottom: {
originY: 0,
},
start: {
originX: 1,
},
end: {
originX: 0,
},
left: {
originX: 1,
},
right: {
originX: 0,
},
"top-start": {
originX: 0,
originY: 1,
},
"top-end": {
originX: 1,
originY: 1,
},
"bottom-start": {
originX: 0,
originY: 0,
},
"bottom-end": {
originX: 1,
originY: 0,
},
"right-start": {
originX: 0,
originY: 0,
},
"right-end": {
originX: 0,
originY: 1,
},
"left-start": {
originX: 1,
originY: 0,
},
"left-end": {
originX: 1,
originY: 1,
},
};
return origins?.[placement] || {};
};
export const toReactAriaPlacement = (placement: TooltipPlacement) => {
const mapPositions: Record<TooltipPlacement, Placement> = {
top: "top",
bottom: "bottom",
left: "left",
right: "right",
"top-start": "top start",
"top-end": "top end",
"bottom-start": "bottom start",
"bottom-end": "bottom end",
"left-start": "left top",
"left-end": "left bottom",
"right-start": "right top",
"right-end": "right bottom",
};
return mapPositions[placement];
};

View File

@ -1,6 +1,6 @@
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {tooltip} from "@nextui-org/theme";
import {ButtonVariantProps, tooltip} from "@nextui-org/theme";
import {Button} from "@nextui-org/button";
import {Tooltip, TooltipProps} from "../src";
@ -36,7 +36,20 @@ export default {
placement: {
control: {
type: "select",
options: ["start", "end", "right", "left", "top", "bottom"],
options: [
"top",
"bottom",
"right",
"left",
"top-start",
"top-end",
"bottom-start",
"bottom-end",
"left-start",
"left-end",
"right-start",
"right-end",
],
},
},
delay: {
@ -88,12 +101,304 @@ const defaultProps = {
isDisabled: false,
disableAnimation: false,
content: "I am a tooltip",
children: <Button>Hover me</Button>,
};
const Template: ComponentStory<typeof Tooltip> = (args: TooltipProps) => <Tooltip {...args} />;
const DelayTemplate: ComponentStory<typeof Tooltip> = (args: TooltipProps) => (
<div className="flex gap-2">
<Tooltip {...args} content="Tooltip 1" delay={1000}>
<Button color="success" variant="faded">
Delay Open (1000ms)
</Button>
</Tooltip>
{/*
// TODO: Uncomment when closeDelay is deployed in react-aria
// https://github.com/adobe/react-spectrum/pull/4128
<Tooltip {...args} closeDelay={3000} content="Tooltip 2">
<Button color="success" variant="faded">
Delay Close (3000ms)
</Button>
</Tooltip> */}
</div>
);
const OpenChangeTemplate: ComponentStory<typeof Tooltip> = (args: TooltipProps) => {
const [isOpen, setIsOpen] = React.useState(false);
return (
<div className="flex flex-col gap-2">
<Tooltip {...args} onOpenChange={(open) => setIsOpen(open)}>
<Button>Hover me</Button>
</Tooltip>
<p className="text-sm">isOpen: {isOpen ? "true" : "false"}</p>
</div>
);
};
const OffsetTemplate: ComponentStory<typeof Tooltip> = (args: TooltipProps) => (
<div className="flex gap-2">
<Tooltip {...args} content="Tooltip 1">
<Button color="secondary" variant="faded">
Default offset (7)
</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 2" offset={15}>
<Button color="secondary" variant="faded">
15 offset
</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 3" offset={-7}>
<Button color="secondary" variant="faded">
-7 offset
</Button>
</Tooltip>
</div>
);
const MultipleTemplate: ComponentStory<typeof Tooltip> = (args: TooltipProps) => (
<div className="flex gap-2">
<Tooltip {...args} content="Tooltip 1" delay={1000}>
<Button>Hover me (delay 1000ms)</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 2">
<Button>Then hover me</Button>
</Tooltip>
</div>
);
const VariantsTemplate: ComponentStory<typeof Tooltip> = (args: TooltipProps) => {
const buttonColor = args.color as ButtonVariantProps["color"];
return (
<div className="flex gap-2">
<Tooltip {...args} content="Tooltip 1" variant="solid">
<Button color={buttonColor} size="sm">
Solid
</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 2" variant="bordered">
<Button color={buttonColor} size="sm" variant="bordered">
Bordered
</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 3" variant="light">
<Button color={buttonColor} size="sm" variant="light">
Light
</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 4" variant="flat">
<Button color={buttonColor} size="sm" variant="flat">
Flat
</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 5" variant="faded">
<Button color={buttonColor} size="sm" variant="faded">
Faded
</Button>
</Tooltip>
<Tooltip {...args} content="Tooltip 6" variant="shadow">
<Button color={buttonColor} size="sm" variant="shadow">
Shadow
</Button>
</Tooltip>
</div>
);
};
const PlacementsTemplate: ComponentStory<typeof Tooltip> = (args: TooltipProps) => {
return (
<div className="inline-grid grid-cols-3 gap-4">
<Tooltip {...args} content="Top Start" placement="top-start">
<Button color="primary" variant="flat">
Top Start
</Button>
</Tooltip>
<Tooltip {...args} content="Top">
<Button color="primary" variant="flat">
Top
</Button>
</Tooltip>
<Tooltip {...args} content="Top End" placement="top-end">
<Button color="primary" variant="flat">
Top End
</Button>
</Tooltip>
<Tooltip {...args} content="Bottom Start" placement="bottom-start">
<Button color="primary" variant="flat">
Bottom Start
</Button>
</Tooltip>
<Tooltip {...args} content="Bottom" placement="bottom">
<Button color="primary" variant="flat">
Bottom
</Button>
</Tooltip>
<Tooltip {...args} content="Bottom End" placement="bottom-end">
<Button color="primary" variant="flat">
Bottom End
</Button>
</Tooltip>
<Tooltip {...args} content="Right Start" placement="right-start">
<Button color="primary" variant="flat">
Right Start
</Button>
</Tooltip>
<Tooltip {...args} content="Right" placement="right">
<Button color="primary" variant="flat">
Right
</Button>
</Tooltip>
<Tooltip {...args} content="Right End" placement="right-end">
<Button color="primary" variant="flat">
Right End
</Button>
</Tooltip>
<Tooltip {...args} content="Left Start" placement="left-start">
<Button color="primary" variant="flat">
Left Start
</Button>
</Tooltip>
<Tooltip {...args} content="Left" placement="left">
<Button color="primary" variant="flat">
Left
</Button>
</Tooltip>
<Tooltip {...args} content="Left End" placement="left-end">
<Button color="primary" variant="flat">
Left End
</Button>
</Tooltip>
</div>
);
};
const ControlledTemplate: ComponentStory<typeof Tooltip> = (args: TooltipProps) => {
const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = () => {
// eslint-disable-next-line no-console
console.log("handleOpen");
setIsOpen((prev) => !prev);
};
const handleOpenChange = (open: boolean) => {
if (!open && isOpen) {
setIsOpen(false);
}
};
return (
<div className="flex flex-col items-center gap-2">
<Tooltip {...args} content="Tooltip 1" isOpen={isOpen} onOpenChange={handleOpenChange}>
<Button onPress={handleOpen}>{isOpen ? "Close" : "Open"}</Button>
</Tooltip>
</div>
);
};
export const Default = Template.bind({});
Default.args = {
...defaultProps,
children: <Button>Hover me</Button>,
};
export const OpenChange = OpenChangeTemplate.bind({});
OpenChange.args = {
...defaultProps,
};
export const Variants = VariantsTemplate.bind({});
Variants.args = {
...defaultProps,
};
export const Placements = PlacementsTemplate.bind({});
Placements.args = {
...defaultProps,
size: "sm",
color: "primary",
};
export const WithOffset = OffsetTemplate.bind({});
WithOffset.args = {
...defaultProps,
color: "secondary",
};
export const withDelay = DelayTemplate.bind({});
withDelay.args = {
...defaultProps,
};
export const CustomContent = Template.bind({});
CustomContent.args = {
...defaultProps,
shouldCloseOnInteractOutside: false,
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 CustomMotion = Template.bind({});
CustomMotion.args = {
...defaultProps,
motionProps: {
variants: {
exit: {
opacity: 0,
transition: {
opacity: {duration: 0.1, easings: "easeInOut"},
},
},
enter: {
opacity: 1,
transition: {
opacity: {easings: "easeOut", duration: 0.15},
},
},
},
},
};
export const Multiple = MultipleTemplate.bind({});
Multiple.args = {
...defaultProps,
};
export const Controlled = ControlledTemplate.bind({});
Controlled.args = {
...defaultProps,
};
export const DefaultOpen = Template.bind({});
DefaultOpen.args = {
...defaultProps,
defaultOpen: true,
};
export const AlwaysOpen = Template.bind({});
AlwaysOpen.args = {
...defaultProps,
isOpen: true,
};
export const Disabled = Template.bind({});
Disabled.args = {
...defaultProps,
isDisabled: true,
};

View File

@ -4,7 +4,7 @@
"baseUrl": ".",
"paths": {
"tailwind-variants": ["../../../node_modules/tailwind-variants"]
},
}
},
"include": ["src", "index.ts"]
}

View File

@ -17,18 +17,12 @@ export const animations = {
},
"drip-expand": {
"0%": {
opacity: "0",
transform: "scale(0.25)",
},
"30%": {
opacity: "1",
},
"80%": {
opacity: "0.5",
opacity: "0.4",
transform: "scale(0)",
},
"100%": {
transform: "scale(28)",
opacity: "0",
transform: "scale(2)",
},
},
"appearance-in": {

View File

@ -1,27 +1,23 @@
import {tv, type VariantProps} from "tailwind-variants";
import {absoluteFullClasses} from "../utils";
import {tv} from "tailwind-variants";
/**
* Drip wrapper **Tailwind Variants** component
*
* const {base, svg} = drip({...})
* const styles = drip({...})
*
* @example
* <div className={base())}>
* <svg className={svg()}>
* // drip svg content
* </svg>
* </div>
* <span ref={dripRef} className={styles())} data-drip="true/false" />
*/
const drip = tv({
slots: {
base: [...absoluteFullClasses, "overflow-hidden"],
svg: "absolute animate-drip-expand",
},
base: [
"hidden",
"absolute",
"bg-current",
"rounded-full",
"pointer-events-none",
'data-[drip="true"]:block',
'data-[drip="true"]:animate-drip-expand',
],
});
export type DripVariantProps = VariantProps<typeof drip>;
export type DripSlots = keyof ReturnType<typeof drip>;
export {drip};

View File

@ -35,11 +35,11 @@ const tooltip = tv({
danger: colorVariants.solid.danger,
},
size: {
xs: "px-2 h-4 text-xs",
sm: "px-3 h-6 text-sm",
md: "px-4 h-8 text-base",
lg: "px-6 h-10 text-lg",
xl: "px-8 h-12 text-xl",
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",
},
radius: {
none: "rounded-none",

View File

@ -134,7 +134,7 @@ import {link, button} from "@nextui-org/theme";
<br/>
<div class="block text-xs text-neutral-400">
Last updated on <time datetime="2023-03-07">March 19, 2023</time>
Last updated on <time datetime="2023-03-07">March 21, 2023</time>
</div>

View File

@ -8,7 +8,7 @@ export function warn(message: string, component?: string, ...args: any[]) {
if (warningStack[log]) return;
warningStack[log] = true;
if (process.env.NODE_ENV !== "production") {
if (process?.env?.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
return console.warn(log, args);
}

611
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff