feat(tooltip): migrated to react-aria and tw

This commit is contained in:
Junior Garcia 2023-02-20 18:13:40 -03:00
parent 93eafd7738
commit 27f2ae1fab
23 changed files with 1075 additions and 74 deletions

View File

@ -38,6 +38,7 @@
},
"dependencies": {
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/shared-css": "workspace:*",
"@nextui-org/dom-utils": "workspace:*"

View File

@ -6,6 +6,7 @@ module.exports = {
"../../button/stories/*.stories.@(js|jsx|ts|tsx)",
"../../spinner/stories/*.stories.@(js|jsx|ts|tsx)",
"../../code/stories/*.stories.@(js|jsx|ts|tsx)",
"../../tooltip/stories/*.stories.@(js|jsx|ts|tsx)",
],
staticDirs: ["../public"],
addons: [

View File

@ -0,0 +1,24 @@
# @nextui-org/tooltip
A Quick description of the component
> This is an internal utility, not intended for public usage.
## Installation
```sh
yarn add @nextui-org/tooltip
# or
npm i @nextui-org/tooltip
```
## Contribution
Yes please! See the
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
for details.
## Licence
This project is licensed under the terms of the
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).

View File

@ -0,0 +1,72 @@
import * as React from "react";
import {render, fireEvent} from "@testing-library/react";
import {Button} from "@nextui-org/button";
import {Tooltip} from "../src";
describe("Tooltip", () => {
it("should throw error if no children is passed", () => {
const spy = jest.spyOn(console, "warn").mockImplementation(() => {});
render(<Tooltip content="tooltip" />);
expect(spy).toHaveBeenCalled();
});
it("should render correctly", () => {
const wrapper = render(
<Tooltip content="tooltip">
<Button>Trigger</Button>
</Tooltip>,
);
expect(() => wrapper.unmount()).not.toThrow();
});
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();
render(
<Tooltip ref={ref} defaultOpen content="tooltip">
<Button>Trigger</Button>
</Tooltip>,
);
expect(ref.current).not.toBeNull();
});
it("should hide the tooltip when pressing the escape key", () => {
const onClose = jest.fn();
const wrapper = render(
<Tooltip defaultOpen content={<p data-testid="content-test">tooltip</p>} onClose={onClose}>
<Button>Trigger</Button>
</Tooltip>,
);
const content = wrapper.getByTestId("content-test");
fireEvent.keyDown(content, {key: "Escape"});
expect(onClose).toHaveBeenCalledTimes(1);
});
it("should still hide the tooltip when pressing the escape key if isDismissable is false", () => {
const onClose = jest.fn();
const wrapper = render(
<Tooltip
defaultOpen
content={<p data-testid="content-test">tooltip</p>}
isDismissable={false}
onClose={onClose}
>
<Button>Trigger</Button>
</Tooltip>,
);
const content = wrapper.getByTestId("content-test");
fireEvent.keyDown(content, {key: "Escape"});
expect(onClose).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,11 @@
{
"replace": {
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"exports": {
".": {"import": "./dist/index.esm.js", "require": "./dist/index.cjs.js"},
"./package.json": "./package.json"
}
}
}

View File

@ -0,0 +1,67 @@
{
"name": "@nextui-org/tooltip",
"version": "2.0.0-beta.1",
"description": "A React Component for rendering dynamically positioned Tooltips",
"keywords": [
"tooltip"
],
"author": "Junior Garcia <jrgarciadev@gmail.com>",
"homepage": "https://nextui.org",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nextui-org/nextui.git",
"directory": "packages/components/tooltip"
},
"bugs": {
"url": "https://github.com/nextui-org/nextui/issues"
},
"scripts": {
"build": "tsup src --dts",
"build:fast": "tsup src",
"dev": "yarn build:fast -- --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"dependencies": {
"@nextui-org/dom-utils": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/react-utils": "workspace:*",
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@nextui-org/aria-utils": "workspace:*",
"@react-aria/focus": "^3.9.0",
"@react-aria/overlays": "^3.11.0",
"@react-aria/tooltip": "^3.3.4",
"@react-stately/tooltip": "^3.2.4",
"@react-aria/utils": "^3.14.0"
},
"devDependencies": {
"@react-types/overlays": "^3.6.5",
"@react-types/tooltip": "^3.2.5",
"@nextui-org/button": "workspace:*",
"clean-package": "2.1.1",
"react": "^17.0.2"
},
"tsup": {
"clean": true,
"target": "es2019",
"format": [
"cjs",
"esm"
]
}
}

View File

@ -0,0 +1,10 @@
import Tooltip from "./tooltip";
// export types
export type {TooltipProps} from "./tooltip";
// export hooks
export {useTooltip} from "./use-tooltip";
// export component
export {Tooltip};

View File

@ -0,0 +1,59 @@
import {forwardRef} from "@nextui-org/system";
import {warn, __DEV__} from "@nextui-org/shared-utils";
import {cloneElement, Children} from "react";
import {OverlayContainer} from "@react-aria/overlays";
import {CSSTransition} from "@nextui-org/react-utils";
import {UseTooltipProps, useTooltip} from "./use-tooltip";
export interface TooltipProps extends Omit<UseTooltipProps, "ref"> {}
const Tooltip = forwardRef<TooltipProps, "div">((props, ref) => {
const {
Component,
children,
content,
mountOverlay,
transitionProps,
getTriggerProps,
getTooltipProps,
} = useTooltip({
ref,
...props,
});
let trigger: React.ReactElement;
try {
/**
* Ensure tooltip has only one child node
*/
const child = Children.only(children) as React.ReactElement & {
ref?: React.Ref<any>;
};
trigger = cloneElement(child, getTriggerProps(child.props, child.ref));
} catch (error) {
trigger = <span />;
warn("Tooltip must have only one child node. Please, check your code.");
}
return (
<>
{trigger}
{mountOverlay && (
<OverlayContainer>
<CSSTransition {...transitionProps}>
<Component {...getTooltipProps()}>{content}</Component>
</CSSTransition>
</OverlayContainer>
)}
</>
);
});
if (__DEV__) {
Tooltip.displayName = "NextUI.Tooltip";
}
export default Tooltip;

View File

@ -0,0 +1,211 @@
import type {TooltipVariantProps} from "@nextui-org/theme";
import type {AriaTooltipProps} from "@react-types/tooltip";
import type {OverlayTriggerProps} from "@react-types/overlays";
import type {CSSTransitionProps} from "@nextui-org/react-utils";
import type {ReactNode} from "react";
import {useTooltipTriggerState} from "@react-stately/tooltip";
import {mergeProps} from "@react-aria/utils";
import {useTooltip as useReactAriaTooltip, useTooltipTrigger} from "@react-aria/tooltip";
import {useOverlayPosition, useOverlay, AriaOverlayProps} from "@react-aria/overlays";
import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system";
import {tooltip} from "@nextui-org/theme";
import {ReactRef, mergeRefs} from "@nextui-org/shared-utils";
import {useMemo, useRef, useState, useCallback} from "react";
export interface UseTooltipProps
extends HTMLNextUIProps<"div", TooltipVariantProps>,
AriaTooltipProps,
AriaOverlayProps,
OverlayTriggerProps {
/**
* Ref to the DOM node.
*/
ref?: ReactRef<HTMLElement | null>;
children?: ReactNode;
/**
* The content of the tooltip.
*/
content?: string | React.ReactNode;
/**
* Whether the tooltip should be disabled, independent from the trigger.
*/
isDisabled?: boolean;
/**
* The delay time for the tooltip to show up.
* @default 0
*/
delay?: number;
/**
* The additional offset applied along the main axis between the element and its
* anchor element.
* @default 7 (px)
*/
offset?: 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.
* @default true
*/
shouldFlip?: boolean;
/**
* By default, opens for both focus and hover. Can be made to open only for focus.
*/
trigger?: "focus";
/**
* The placement of the element with respect to its anchor element.
* @default 'top'
*/
placement?: "start" | "end" | "right" | "left" | "top" | "bottom";
/**
* The placement padding that should be applied between the element and its
* surrounding container.
* @default 12
*/
containerPadding?: number;
/** Handler that is called when the overlay should close. */
onClose?: () => void;
}
export function useTooltip(originalProps: UseTooltipProps) {
const [props, variantProps] = mapPropsVariants(originalProps, tooltip.variantKeys);
const {
ref,
as,
isOpen,
content,
children,
defaultOpen,
onOpenChange,
isDisabled,
trigger: triggerAction = "focus",
shouldFlip = true,
containerPadding = 12,
placement: placementProp = "top",
delay = 0,
offset = 7,
isDismissable = true,
shouldCloseOnBlur = false,
isKeyboardDismissDisabled = false,
shouldCloseOnInteractOutside,
className,
onClose,
...otherProps
} = props;
const Component = as || "div";
const state = useTooltipTriggerState({delay, isDisabled, isOpen, defaultOpen, onOpenChange});
const [exited, setExited] = useState(!state.isOpen);
const triggerRef = useRef<HTMLElement>(null);
const overlayRef = useRef<HTMLElement>(null);
const domRef = mergeRefs(overlayRef, ref);
const handleClose = useCallback(() => {
onClose?.();
state.close();
}, [state, onClose]);
const onEntered = useCallback(() => {
setExited(false);
}, []);
const onExited = useCallback(() => {
setExited(true);
}, []);
const {triggerProps, tooltipProps: triggerTooltipProps} = useTooltipTrigger(
{
delay,
isDisabled,
trigger: triggerAction,
},
state,
triggerRef,
);
const {tooltipProps} = useReactAriaTooltip(mergeProps(props, triggerTooltipProps), state);
const {
overlayProps: positionProps,
// arrowProps,
// placement,
} = useOverlayPosition({
isOpen: state.isOpen,
targetRef: triggerRef,
placement: placementProp,
overlayRef,
offset,
shouldFlip,
containerPadding,
});
const mountOverlay = (state.isOpen || !exited) && !isDisabled;
const {overlayProps} = useOverlay(
{
isOpen: state.isOpen,
isDismissable: isDismissable && state.isOpen,
onClose: handleClose,
shouldCloseOnBlur,
isKeyboardDismissDisabled,
shouldCloseOnInteractOutside,
},
overlayRef,
);
const transitionProps = useMemo<CSSTransitionProps>(
() => ({
clearTime: originalProps?.disableAnimation ? 0 : 100,
enterTime: originalProps?.disableAnimation ? 0 : 250,
leaveTime: originalProps?.disableAnimation ? 0 : 60,
isVisible: state.isOpen,
onEntered: onEntered,
onExited: onExited,
}),
[originalProps?.disableAnimation, state.isOpen],
);
const styles = useMemo(
() =>
tooltip({
...variantProps,
className,
}),
[variantProps, className],
);
const getTriggerProps = useCallback(
(props = {}, _ref = null) => ({
...mergeProps(triggerProps, props),
ref: mergeRefs(triggerRef, _ref),
onPointerEnter: () => state.open(),
onPointerLeave: () => !isDismissable && state.close(),
}),
[isDismissable, triggerRef, triggerProps],
);
const getTooltipProps = useCallback(
() => ({
ref: domRef,
className: styles,
...mergeProps(tooltipProps, positionProps, overlayProps, otherProps),
}),
[domRef, styles, tooltipProps, positionProps, overlayProps, otherProps],
);
return {
Component,
content,
children,
mountOverlay,
transitionProps,
getTriggerProps,
getTooltipProps,
};
}
export type UseTooltipReturn = ReturnType<typeof useTooltip>;

View File

@ -0,0 +1,98 @@
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {tooltip} from "@nextui-org/theme";
import {Button} from "@nextui-org/button";
import {Tooltip, TooltipProps} from "../src";
export default {
title: "Display/Tooltip",
component: Tooltip,
argTypes: {
variant: {
control: {
type: "select",
options: ["solid", "bordered", "light", "flat", "shadow", "ghost"],
},
},
color: {
control: {
type: "select",
options: ["neutral", "foreground", "primary", "secondary", "success", "warning", "danger"],
},
},
size: {
control: {
type: "select",
options: ["xs", "sm", "md", "lg", "xl"],
},
},
radius: {
control: {
type: "select",
options: ["none", "base", "sm", "md", "lg", "xl", "full"],
},
},
placement: {
control: {
type: "select",
options: ["start", "end", "right", "left", "top", "bottom"],
},
},
delay: {
control: {
type: "number",
},
},
offset: {
control: {
type: "number",
},
},
defaultOpen: {
control: {
type: "boolean",
},
},
isDisabled: {
control: {
type: "boolean",
},
},
disableAnimation: {
control: {
type: "boolean",
},
},
children: {
control: {
disable: true,
},
},
},
decorators: [
(Story) => (
<div className="flex items-center justify-center w-screen h-screen">
<Story />
</div>
),
],
} as ComponentMeta<typeof Tooltip>;
const defaultProps = {
...tooltip.defaultVariants,
delay: 0,
offset: 7,
defaultOpen: false,
isDisabled: false,
disableAnimation: false,
content: "I am a tooltip",
};
const Template: ComponentStory<typeof Tooltip> = (args: TooltipProps) => <Tooltip {...args} />;
export const Default = Template.bind({});
Default.args = {
...defaultProps,
children: <Button>Hover me</Button>,
};

View File

@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"paths": {
"@stitches/react": ["../../../node_modules/@stitches/react"]
}
},
"include": ["src", "index.ts"]
}

View File

@ -7,3 +7,4 @@ export * from "./button-group";
export * from "./drip";
export * from "./spinner";
export * from "./code";
export * from "./tooltip";

View File

@ -0,0 +1,25 @@
import {tv, type VariantProps} from "tailwind-variants";
/**
* Snippet wrapper **Tailwind Variants** component
*
* const {base, svg} = snippet({...})
*
* @example
* <div className={base())}>
* <svg className={svg()}>
* // drip svg content
* </svg>
* </div>
*/
const snippet = tv({
slots: {
base: "",
svg: "",
},
});
export type SnippetVariantProps = VariantProps<typeof snippet>;
export type SnippetSlots = keyof ReturnType<typeof snippet>;
export {snippet};

View File

@ -0,0 +1,221 @@
import {tv, type VariantProps} from "tailwind-variants";
/**
* Tooltip wrapper **Tailwind Variants** component
*
* const styles = tooltip({...})
*
* @example
* <div>
* <button>your trigger</button>
* <div role="tooltip" className={styles} data-transition='enter/leave'>
* // tooltip content
* </div>
* </div>
*/
const tooltip = tv({
base: [
"inline-flex",
"flex-col",
"items-center",
"justify-center",
"box-border",
"animate-appearance-in",
"data-[transition=leave]:animate-appearance-out",
],
variants: {
variant: {
solid: "",
bordered: "border-2 !bg-transparent",
light: "!bg-transparent",
flat: "",
shadow: "",
},
color: {
neutral: "bg-neutral-300 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-100",
foreground: "bg-foreground text-foreground-dark dark:text-foreground-light",
primary: "bg-primary text-white",
secondary: "bg-secondary text-white",
success: "bg-success text-success-800",
warning: "bg-warning text-warning-800",
danger: "bg-danger text-white",
},
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",
},
radius: {
none: "rounded-none",
base: "rounded",
sm: "rounded-sm",
md: "rounded-md",
lg: "rounded-lg",
xl: "rounded-xl",
full: "rounded-full",
},
disableAnimation: {
true: "animate-none",
},
},
defaultVariants: {
variant: "solid",
color: "neutral",
size: "sm",
radius: "lg",
},
compoundVariants: [
// shadow / color
{
variant: "shadow",
color: "neutral",
class: "shadow-lg shadow-neutral/40",
},
{
variant: "shadow",
color: "foreground",
class: "shadow-lg shadow-foreground/40",
},
{
variant: "shadow",
color: "primary",
class: "shadow-lg shadow-primary/40",
},
{
variant: "shadow",
color: "secondary",
class: "shadow-lg shadow-secondary/40",
},
{
variant: "shadow",
color: "success",
class: "shadow-lg shadow-success/40",
},
{
variant: "shadow",
color: "warning",
class: "shadow-lg shadow-warning/40",
},
{
variant: "shadow",
color: "danger",
class: "shadow-lg shadow-danger/40",
},
// bordered / color
{
variant: "bordered",
color: "neutral",
class: "border-neutral-300 dark:border-neutral-700 text-neutral-700 dark:text-neutral-100",
},
{
variant: "bordered",
color: "foreground",
class: "border-foreground text-foreground dark:text-foreground-dark",
},
{
variant: "bordered",
color: "primary",
class: "border-primary text-primary",
},
{
variant: "bordered",
color: "secondary",
class: "border-secondary text-secondary",
},
{
variant: "bordered",
color: "success",
class: "border-success text-success",
},
{
variant: "bordered",
color: "warning",
class: "border-warning text-warning",
},
{
variant: "bordered",
color: "danger",
class: "border-danger text-danger",
},
// flat / color
{
variant: "flat",
color: "neutral",
class: "bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-100",
},
{
variant: "flat",
color: "foreground",
class:
"bg-foreground/20 text-foreground dark:bg-foreground-dark/20 dark:text-foreground-dark",
},
{
variant: "flat",
color: "primary",
class: "bg-primary-50 dark:bg-primary-900 text-primary",
},
{
variant: "flat",
color: "secondary",
class: "bg-secondary-50 dark:bg-secondary-900 text-secondary dark:text-secondary-400",
},
{
variant: "flat",
color: "success",
class: "bg-success-50 dark:bg-success-900 text-success-600 dark:text-success",
},
{
variant: "flat",
color: "warning",
class: "bg-warning-50 dark:bg-warning-900 text-warning-600 dark:text-warning",
},
{
variant: "flat",
color: "danger",
class: "bg-danger-50 dark:bg-danger-900 text-danger dark:text-danger-400",
},
// light / color
{
variant: "light",
color: "neutral",
class: "text-neutral-700 dark:text-neutral-100",
},
{
variant: "light",
color: "foreground",
class: "text-foreground dark:text-foreground-dark",
},
{
variant: "light",
color: "primary",
class: "text-primary",
},
{
variant: "light",
color: "secondary",
class: "text-secondary",
},
{
variant: "light",
color: "success",
class: "text-success",
},
{
variant: "light",
color: "warning",
class: "text-warning",
},
{
variant: "light",
color: "danger",
class: "text-danger",
},
],
});
export type TooltipVariantProps = VariantProps<typeof tooltip>;
export type TooltipSlots = keyof ReturnType<typeof tooltip>;
export {tooltip};

View File

@ -155,6 +155,8 @@ module.exports = plugin(
"drip-expand": "drip-expand 350ms linear",
"spinner-ease-spin": "spinner-spin 0.8s ease infinite",
"spinner-linear-spin": "spinner-spin 0.8s linear infinite",
"appearance-in": "appearance-in 250ms ease-out normal both",
"appearance-out": "appearance-out 60ms ease-in normal both",
},
keyframes: {
"spinner-spin": {
@ -181,6 +183,33 @@ module.exports = plugin(
opacity: 0,
},
},
"appearance-in": {
"0%": {
opacity: 0,
transform: "translateZ(0) scale(0.95)",
},
"60%": {
opacity: 0.75,
/* Avoid blurriness */
backfaceVisibility: "hidden",
webkitFontSmoothing: "antialiased",
transform: "translateZ(0) scale(1.05)",
},
"100%": {
opacity: 1,
transform: "translateZ(0) scale(1)",
},
},
"appearance-out": {
"0%": {
opacity: 1,
transform: "scale(1)",
},
"100%": {
opacity: 0,
transform: "scale(0.5)",
},
},
},
},
},

View File

@ -39,18 +39,19 @@
"dependencies": {
"@nextui-org/system": "workspace:*",
"@react-aria/button": "^3.6.2",
"@react-aria/focus": "^3.9.0",
"@react-aria/interactions": "^3.12.0",
"@react-aria/selection": "^3.11.0",
"@react-aria/utils": "^3.14.0",
"@react-stately/collections": "^3.4.4",
"@react-aria/interactions": "^3.12.0",
"@react-aria/focus": "^3.9.0",
"@react-stately/tree": "^3.3.4"
},
"devDependencies": {
"@react-types/button": "^3.6.2",
"@react-types/overlays": "^3.6.5",
"@react-types/shared": "^3.15.0",
"react": "^17.0.2",
"clean-package": "2.1.1"
"clean-package": "2.1.1",
"react": "^17.0.2"
},
"tsup": {
"clean": true,

View File

@ -3,3 +3,4 @@ export * from "./collections";
export * from "./utils";
export * from "./interactions";
export * from "./type-utils";
export * from "./overlays";

View File

@ -0,0 +1,73 @@
import {Placement, PlacementAxis} from "@react-types/overlays";
export type PopoverPlacement =
| "bottom"
| "bottom-left"
| "bottom-right"
| "top"
| "top-left"
| "top-right"
| "left"
| "left-top"
| "left-bottom"
| "right"
| "right-top"
| "right-bottom";
export const getAriaPlacement = (placement?: PopoverPlacement) => {
if (!placement) {
return "bottom" as Placement;
}
return placement.replace("-", " ") as Placement;
};
export const getPopoverPlacementFromAxis = (placementAxis?: PlacementAxis) => {
if (!placementAxis) {
return "bottom";
}
return placementAxis.replace("-", " ") as PopoverPlacement;
};
export const getPopoverPlacement = (ariaPlacement?: Placement) => {
if (!ariaPlacement) {
return "bottom" as Placement;
}
return ariaPlacement.replace(" ", "-") as PopoverPlacement;
};
export const getTransformOrigin = (placement?: PopoverPlacement) => {
if (!placement) {
return "bottom" as Placement;
}
switch (placement) {
case "bottom":
return "top center";
case "top":
return "bottom center";
case "left":
return "right center";
case "right":
return "left center";
case "bottom-left":
return "top left";
case "bottom-right":
return "top right";
case "top-left":
return "bottom left";
case "top-right":
return "bottom right";
case "left-top":
return "right top";
case "left-bottom":
return "right bottom";
case "right-top":
return "left top";
case "right-bottom":
return "left bottom";
default:
return "top center";
}
};

View File

@ -1,86 +1,97 @@
import React, {useEffect, useState} from "react";
import type {FC, ReactNode, RefObject} from "react";
import React, {useEffect, useLayoutEffect, useState} from "react";
import {clsx} from "@nextui-org/shared-utils";
interface CSSTransitionProps {
visible?: boolean;
childrenRef?: React.RefObject<HTMLElement>;
export interface CSSTransitionProps {
name?: string;
isVisible?: boolean;
enterTime?: number;
leaveTime?: number;
clearTime?: number;
className?: string;
name?: string;
onExited?: () => void;
onEntered?: () => void;
children?: ReactNode;
childrenRef?: RefObject<HTMLElement>;
}
export const CSSTransition: React.FC<React.PropsWithChildren<CSSTransitionProps>> = ({
children,
childrenRef,
visible = false,
enterTime = 60,
leaveTime = 60,
clearTime = 60,
className = "",
name = "transition",
onExited,
onEntered,
...props
}) => {
const [classes, setClasses] = useState<string>("");
const [renderable, setRenderable] = useState<boolean>(visible);
const CSSTransition: FC<CSSTransitionProps> = React.memo(
({
children,
onExited,
onEntered,
className,
childrenRef,
enterTime = 60,
leaveTime = 60,
clearTime = 60,
isVisible = false,
name = "transition",
...otherProps
}) => {
const [classes, setClasses] = useState<string>("");
const [statusClassName, setStatusClassName] = useState("");
const [renderable, setRenderable] = useState<boolean>(isVisible);
useEffect(() => {
const statusClassName = visible ? "enter" : "leave";
const time = visible ? enterTime : leaveTime;
useLayoutEffect(() => {
const statusClassName = isVisible ? "enter" : "leave";
if (visible && !renderable) {
setRenderable(true);
}
if (isVisible && !renderable) setRenderable(true);
setClasses(`${name}-${statusClassName}`);
setClasses(`${name}-${statusClassName}`);
setStatusClassName(statusClassName);
// set class to active
const timer = setTimeout(() => {
setClasses(`${name}-${statusClassName} ${name}-${statusClassName}-active`);
if (statusClassName === "leave") {
onExited?.();
} else {
onEntered?.();
}
clearTimeout(timer);
}, time);
const time = isVisible ? enterTime : leaveTime;
// remove classess when animation over
const clearClassesTimer = setTimeout(() => {
if (!visible) {
setClasses("");
setRenderable(false);
}
clearTimeout(clearClassesTimer);
}, time + clearTime);
// set class to active
const timer = setTimeout(() => {
setClasses(`${name}-${statusClassName} ${name}-${statusClassName}-active`);
setStatusClassName(`${statusClassName}-active`);
if (statusClassName === "leave") {
onExited?.();
} else {
onEntered?.();
}
clearTimeout(timer);
}, time);
return () => {
clearTimeout(timer);
clearTimeout(clearClassesTimer);
};
}, [visible, renderable]);
// remove classess when animation over
const clearClassesTimer = setTimeout(() => {
if (!isVisible) {
setClasses("");
setRenderable(false);
}
clearTimeout(clearClassesTimer);
}, time + clearTime);
// update children ref classes
useEffect(() => {
if (!childrenRef?.current) {
return;
}
const classesArr = classes.split(" ");
const refClassesArr = childrenRef.current.className.split(" ");
const newRefClassesArr = refClassesArr.filter((item) => !item.includes(name));
return () => {
clearTimeout(timer);
clearTimeout(clearClassesTimer);
};
}, [isVisible, renderable]);
childrenRef.current.className = clsx(newRefClassesArr, classesArr);
}, [childrenRef, classes]);
// update children ref classes
useEffect(() => {
if (!childrenRef?.current) return;
if (!React.isValidElement(children) || !renderable) return null;
const classesArr = classes.split(" ");
const refClassesArr = childrenRef.current.className.split(" ");
const newRefClassesArr = refClassesArr.filter((item) => !item.includes(name));
return React.cloneElement(children, {
...props,
className: clsx(children.props.className, className, !childrenRef?.current ? classes : ""),
});
};
childrenRef.current.className = clsx(newRefClassesArr, classesArr);
}, [childrenRef, classes]);
if (!React.isValidElement(children) || !renderable) return null;
return React.cloneElement(children, {
...otherProps,
"data-transition": statusClassName,
className: clsx(children.props.className, className, !childrenRef?.current && classes),
});
},
);
CSSTransition.displayName = "NextUI.CSSTransition";
export {CSSTransition};

View File

@ -1,4 +1,4 @@
import { {{capitalize componentName}} } from "./{{componentName}}";
import {{capitalize componentName}} from "./{{componentName}}";
// export types
export type { {{capitalize componentName}}Props } from "./{{componentName}}";

View File

@ -32,7 +32,7 @@ export function use{{capitalize componentName}}(originalProps: Use{{capitalize c
[variantProps, className],
);
return {Component, as, styles, domRef, ...otherProps};
return {Component, styles, domRef, ...otherProps};
}
export type Use{{capitalize componentName}}Return = ReturnType<typeof use{{capitalize componentName}}>;

View File

@ -35,7 +35,7 @@ export default {
} as ComponentMeta<typeof {{capitalize componentName}}>;
const defaultProps = {
...code.defaultVariants,
...{{componentName}}.defaultVariants,
};
const Template: ComponentStory<typeof {{capitalize componentName}}> = (args: {{capitalize componentName}}Props) => <{{capitalize componentName}} {...args} />;

78
pnpm-lock.yaml generated
View File

@ -657,6 +657,7 @@ importers:
'@nextui-org/shared-css': workspace:*
'@nextui-org/shared-utils': workspace:*
'@nextui-org/system': workspace:*
'@nextui-org/theme': workspace:*
clean-package: 2.1.1
react: ^17.0.2
dependencies:
@ -664,6 +665,7 @@ importers:
'@nextui-org/shared-css': link:../../utilities/shared-css
'@nextui-org/shared-utils': link:../../utilities/shared-utils
'@nextui-org/system': link:../../core/system
'@nextui-org/theme': link:../../core/theme
devDependencies:
clean-package: 2.1.1
react: 17.0.2
@ -738,6 +740,43 @@ importers:
storybook-dark-mode: 1.1.2
tailwindcss: 3.2.6_postcss@8.4.21
packages/components/tooltip:
specifiers:
'@nextui-org/aria-utils': workspace:*
'@nextui-org/button': workspace:*
'@nextui-org/dom-utils': workspace:*
'@nextui-org/react-utils': workspace:*
'@nextui-org/shared-utils': workspace:*
'@nextui-org/system': workspace:*
'@nextui-org/theme': workspace:*
'@react-aria/focus': ^3.9.0
'@react-aria/overlays': ^3.11.0
'@react-aria/tooltip': ^3.3.4
'@react-aria/utils': ^3.14.0
'@react-stately/tooltip': ^3.2.4
'@react-types/overlays': ^3.6.5
'@react-types/tooltip': ^3.2.5
clean-package: 2.1.1
react: ^17.0.2
dependencies:
'@nextui-org/aria-utils': link:../../utilities/aria-utils
'@nextui-org/dom-utils': link:../../utilities/dom-utils
'@nextui-org/react-utils': link:../../utilities/react-utils
'@nextui-org/shared-utils': link:../../utilities/shared-utils
'@nextui-org/system': link:../../core/system
'@nextui-org/theme': link:../../core/theme
'@react-aria/focus': 3.10.1_react@17.0.2
'@react-aria/overlays': 3.12.1_react@17.0.2
'@react-aria/tooltip': 3.3.4_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/tooltip': 3.2.4_react@17.0.2
devDependencies:
'@nextui-org/button': link:../button
'@react-types/overlays': 3.6.5_react@17.0.2
'@react-types/tooltip': 3.2.5_react@17.0.2
clean-package: 2.1.1
react: 17.0.2
packages/components/user:
specifiers:
'@nextui-org/avatar': workspace:*
@ -932,6 +971,7 @@ importers:
'@react-stately/collections': ^3.4.4
'@react-stately/tree': ^3.3.4
'@react-types/button': ^3.6.2
'@react-types/overlays': ^3.6.5
'@react-types/shared': ^3.15.0
clean-package: 2.1.1
react: ^17.0.2
@ -946,6 +986,7 @@ importers:
'@react-stately/tree': 3.4.1_react@17.0.2
devDependencies:
'@react-types/button': 3.7.0_react@17.0.2
'@react-types/overlays': 3.6.5_react@17.0.2
'@react-types/shared': 3.16.0_react@17.0.2
clean-package: 2.1.1
react: 17.0.2
@ -5745,6 +5786,21 @@ packages:
react: 17.0.2
dev: false
/@react-aria/tooltip/3.3.4_react@17.0.2:
resolution: {integrity: sha512-KPDkDu7fquuUOOnNh9S7KfhPMwB1w9K+yLIFrYaj4iYSOLk/HH5TDkyiUQ7j5+B963D1fWlQjYFEGQ9o2KwO/Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-aria/focus': 3.10.1_react@17.0.2
'@react-aria/interactions': 3.13.1_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/tooltip': 3.2.4_react@17.0.2
'@react-types/shared': 3.16.0_react@17.0.2
'@react-types/tooltip': 3.2.5_react@17.0.2
'@swc/helpers': 0.4.14
react: 17.0.2
dev: false
/@react-aria/utils/3.14.0_react@17.0.2:
resolution: {integrity: sha512-DHgmwNBNEhnb6DEYYAfbt99wprBqJJOBBeIpQ2g3+pxwlw4BZ+v4Qr+rDD0ZibWV0mYzt8zOhZ9StpId7iTF0Q==}
peerDependencies:
@ -6011,6 +6067,18 @@ packages:
react: 17.0.2
dev: false
/@react-stately/tooltip/3.2.4_react@17.0.2:
resolution: {integrity: sha512-t7ksDRs9jKcOS25BVLM5cNCyzSCnzrin8OZ3AEmgeNxfiS58HhHbNxYk725hyGrbdpugQ03cRcJG70EZ6VgwDQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-stately/overlays': 3.4.4_react@17.0.2
'@react-stately/utils': 3.5.2_react@17.0.2
'@react-types/tooltip': 3.2.5_react@17.0.2
'@swc/helpers': 0.4.14
react: 17.0.2
dev: false
/@react-stately/tree/3.3.4_react@17.0.2:
resolution: {integrity: sha512-CBgXvwa9qYBsJuxrAiVgGnm48eSxLe/6OjPMwH1pWf4s383Mx73MbbN4fS0oWDeXBVgdqz5/Xg/p8nvPIvl3WQ==}
peerDependencies:
@ -6161,7 +6229,6 @@ packages:
dependencies:
'@react-types/shared': 3.16.0_react@17.0.2
react: 17.0.2
dev: false
/@react-types/radio/3.3.1_react@17.0.2:
resolution: {integrity: sha512-q/x0kMvBsu6mH4bIkp/Jjrm9ff5y/p3UR0V4CmQFI7604gQd2Dt1dZMU/2HV9x70r1JfWRrDeRrVjUHVfFL5Vg==}
@ -6206,6 +6273,15 @@ packages:
react: 17.0.2
dev: false
/@react-types/tooltip/3.2.5_react@17.0.2:
resolution: {integrity: sha512-D4lN32JwQuA3JbCgcI26mgCkLHIj1WE8MTzf1McaasPkx7gVaqW+wfPyFwt99/Oo52TLvA/1oin78qePP67PSw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-types/overlays': 3.6.5_react@17.0.2
'@react-types/shared': 3.16.0_react@17.0.2
react: 17.0.2
/@rushstack/eslint-patch/1.2.0:
resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==}
dev: true