mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat: tooltip improved, new drip handler created, button improved
This commit is contained in:
parent
133f1b494a
commit
f9b3e0b7bc
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 <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 <p className="text-primary">fix now</p>
|
||||
</p>
|
||||
}
|
||||
title="Connected devices"
|
||||
|
||||
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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];
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"tailwind-variants": ["../../../node_modules/tailwind-variants"]
|
||||
},
|
||||
}
|
||||
},
|
||||
"include": ["src", "index.ts"]
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
|
||||
@ -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
611
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user