mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(tooltip): animation changed to framer-motion
This commit is contained in:
parent
3844e9b501
commit
133f1b494a
@ -1,7 +1,7 @@
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import {useMemo, ReactNode} from "react";
|
||||
import {ChevronIcon} from "@nextui-org/shared-icons";
|
||||
import {Collapse} from "@nextui-org/framer-transitions";
|
||||
import {CollapseTransition} from "@nextui-org/framer-transitions";
|
||||
|
||||
import {UseAccordionItemProps, useAccordionItem} from "./use-accordion-item";
|
||||
|
||||
@ -46,9 +46,9 @@ const Accordion = forwardRef<AccordionItemProps, "div">((props, ref) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse in={isOpen} {...motionProps}>
|
||||
<CollapseTransition in={isOpen} {...motionProps}>
|
||||
<div {...getContentProps()}>{children}</div>
|
||||
</Collapse>
|
||||
</CollapseTransition>
|
||||
);
|
||||
}, [isOpen, disableAnimation, children, motionProps]);
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import type {
|
||||
AccordionItemSlots,
|
||||
SlotsToClasses,
|
||||
} from "@nextui-org/theme";
|
||||
import type {CollapseProps} from "@nextui-org/framer-transitions";
|
||||
import type {CollapseTransitionProps} from "@nextui-org/framer-transitions";
|
||||
|
||||
import {BaseItem, ItemProps} from "@nextui-org/aria-utils";
|
||||
import {FocusableProps} from "@react-types/shared";
|
||||
@ -53,7 +53,7 @@ export interface Props<T extends object = {}>
|
||||
/**
|
||||
* The properties passed to the underlying `Collapse` component.
|
||||
*/
|
||||
motionProps?: CollapseProps;
|
||||
motionProps?: CollapseTransitionProps;
|
||||
/**
|
||||
* Classname or List of classes to change the styles of the element.
|
||||
* if `className` is passed, it will be added to the base slot.
|
||||
|
||||
@ -43,11 +43,13 @@
|
||||
"@nextui-org/system": "workspace:*",
|
||||
"@nextui-org/theme": "workspace:*",
|
||||
"@nextui-org/aria-utils": "workspace:*",
|
||||
"@nextui-org/framer-transitions": "workspace:*",
|
||||
"@react-aria/focus": "^3.11.0",
|
||||
"@react-aria/overlays": "^3.13.0",
|
||||
"@react-aria/tooltip": "^3.4.0",
|
||||
"@react-stately/tooltip": "^3.3.0",
|
||||
"@react-aria/utils": "^3.15.0"
|
||||
"@react-aria/utils": "^3.15.0",
|
||||
"framer-motion": "^10.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-types/overlays": "^3.7.0",
|
||||
|
||||
33
packages/components/tooltip/src/tooltip-transition.ts
Normal file
33
packages/components/tooltip/src/tooltip-transition.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import {Variants} from "framer-motion";
|
||||
|
||||
export const scale: Variants = {
|
||||
exit: {
|
||||
scale: 0.6,
|
||||
opacity: 0,
|
||||
transition: {
|
||||
opacity: {
|
||||
duration: 0.15,
|
||||
easings: "easeInOut",
|
||||
},
|
||||
scale: {
|
||||
duration: 0.2,
|
||||
easings: "easeInOut",
|
||||
},
|
||||
},
|
||||
},
|
||||
enter: {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
opacity: {
|
||||
easings: "easeOut",
|
||||
duration: 0.2,
|
||||
},
|
||||
scale: {
|
||||
type: "spring",
|
||||
stiffness: 800,
|
||||
damping: 35,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -1,10 +1,12 @@
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import {warn} from "@nextui-org/shared-utils";
|
||||
import {cloneElement, Children} from "react";
|
||||
import {cloneElement, Children, useMemo} from "react";
|
||||
import {OverlayContainer} from "@react-aria/overlays";
|
||||
import {CSSTransition} from "@nextui-org/react-utils";
|
||||
import {AnimatePresence, motion} from "framer-motion";
|
||||
|
||||
import {UseTooltipProps, useTooltip} from "./use-tooltip";
|
||||
import {scale} from "./tooltip-transition";
|
||||
import {getOrigins} from "./utils";
|
||||
|
||||
export interface TooltipProps extends Omit<UseTooltipProps, "ref"> {}
|
||||
|
||||
@ -13,8 +15,10 @@ const Tooltip = forwardRef<TooltipProps, "div">((props, ref) => {
|
||||
Component,
|
||||
children,
|
||||
content,
|
||||
mountOverlay,
|
||||
transitionProps,
|
||||
isOpen,
|
||||
placement,
|
||||
disableAnimation,
|
||||
motionProps,
|
||||
getTriggerProps,
|
||||
getTooltipProps,
|
||||
} = useTooltip({
|
||||
@ -38,16 +42,44 @@ 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 {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>
|
||||
);
|
||||
}, [isOpen, content, disableAnimation, children, motionProps, getTooltipProps]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{trigger}
|
||||
{mountOverlay && (
|
||||
<OverlayContainer>
|
||||
<CSSTransition {...transitionProps}>
|
||||
<OverlayContainer>
|
||||
{/* <CSSTransition {...transitionProps}>
|
||||
<Component {...getTooltipProps()}>{content}</Component>
|
||||
</CSSTransition>
|
||||
</OverlayContainer>
|
||||
)}
|
||||
</CSSTransition> */}
|
||||
{contentComponent}
|
||||
</OverlayContainer>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
1
packages/components/tooltip/src/types.ts
Normal file
1
packages/components/tooltip/src/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type TooltipPlacement = "start" | "end" | "right" | "left" | "top" | "bottom";
|
||||
@ -1,8 +1,9 @@
|
||||
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, Ref} from "react";
|
||||
import type {HTMLMotionProps} from "framer-motion";
|
||||
import type {TooltipPlacement} from "./types";
|
||||
|
||||
import {useTooltipTriggerState} from "@react-stately/tooltip";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
@ -11,7 +12,7 @@ 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 {useMemo, useRef, useState, useCallback} from "react";
|
||||
import {useMemo, useRef, useCallback} from "react";
|
||||
|
||||
export interface UseTooltipProps
|
||||
extends HTMLNextUIProps<"div", TooltipVariantProps>,
|
||||
@ -22,6 +23,9 @@ export interface UseTooltipProps
|
||||
* Ref to the DOM node.
|
||||
*/
|
||||
ref?: ReactRef<HTMLElement | null>;
|
||||
/**
|
||||
* The children to render. Usually a trigger element.
|
||||
*/
|
||||
children?: ReactNode;
|
||||
/**
|
||||
* The content of the tooltip.
|
||||
@ -36,6 +40,11 @@ export interface UseTooltipProps
|
||||
* @default 0
|
||||
*/
|
||||
delay?: number;
|
||||
/**
|
||||
* The delay time for the tooltip to hide.
|
||||
* @default 0
|
||||
*/
|
||||
closeDelay?: number;
|
||||
/**
|
||||
* The additional offset applied along the main axis between the element and its
|
||||
* anchor element.
|
||||
@ -56,13 +65,17 @@ export interface UseTooltipProps
|
||||
* The placement of the element with respect to its anchor element.
|
||||
* @default 'top'
|
||||
*/
|
||||
placement?: "start" | "end" | "right" | "left" | "top" | "bottom";
|
||||
placement?: TooltipPlacement;
|
||||
/**
|
||||
* The placement padding that should be applied between the element and its
|
||||
* surrounding container.
|
||||
* @default 12
|
||||
*/
|
||||
containerPadding?: number;
|
||||
/**
|
||||
* The properties passed to the underlying `Collapse` component.
|
||||
*/
|
||||
motionProps?: HTMLMotionProps<"div">;
|
||||
/** Handler that is called when the overlay should close. */
|
||||
onClose?: () => void;
|
||||
}
|
||||
@ -84,6 +97,7 @@ export function useTooltip(originalProps: UseTooltipProps) {
|
||||
containerPadding = 12,
|
||||
placement: placementProp = "top",
|
||||
delay = 0,
|
||||
closeDelay = 0,
|
||||
offset = 7,
|
||||
isDismissable = true,
|
||||
shouldCloseOnBlur = false,
|
||||
@ -91,31 +105,30 @@ export function useTooltip(originalProps: UseTooltipProps) {
|
||||
shouldCloseOnInteractOutside,
|
||||
className,
|
||||
onClose,
|
||||
motionProps,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const Component = as || "div";
|
||||
|
||||
const state = useTooltipTriggerState({delay, isDisabled, isOpen, defaultOpen, onOpenChange});
|
||||
|
||||
const [exited, setExited] = useState(!state.isOpen);
|
||||
const state = useTooltipTriggerState({
|
||||
delay,
|
||||
isDisabled,
|
||||
isOpen,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
});
|
||||
|
||||
const triggerRef = useRef<HTMLElement>(null);
|
||||
const overlayRef = useRef<HTMLElement>(null);
|
||||
const domRef = mergeRefs(overlayRef, ref);
|
||||
|
||||
const immediate = closeDelay === 0;
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
onClose?.();
|
||||
state.close();
|
||||
}, [state, onClose]);
|
||||
|
||||
const onEntered = useCallback(() => {
|
||||
setExited(false);
|
||||
}, []);
|
||||
|
||||
const onExited = useCallback(() => {
|
||||
setExited(true);
|
||||
}, []);
|
||||
state.close(immediate);
|
||||
}, [state, immediate, onClose]);
|
||||
|
||||
const {triggerProps, tooltipProps: triggerTooltipProps} = useTooltipTrigger(
|
||||
{
|
||||
@ -129,6 +142,10 @@ export function useTooltip(originalProps: UseTooltipProps) {
|
||||
|
||||
const {tooltipProps} = useReactAriaTooltip(mergeProps(props, triggerTooltipProps), state);
|
||||
|
||||
tooltipProps.onPointerLeave = () => {
|
||||
state.close(immediate);
|
||||
};
|
||||
|
||||
const {
|
||||
overlayProps: positionProps,
|
||||
// arrowProps,
|
||||
@ -143,8 +160,6 @@ export function useTooltip(originalProps: UseTooltipProps) {
|
||||
containerPadding,
|
||||
});
|
||||
|
||||
const mountOverlay = (state.isOpen || !exited) && !isDisabled;
|
||||
|
||||
const {overlayProps} = useOverlay(
|
||||
{
|
||||
isOpen: state.isOpen,
|
||||
@ -157,18 +172,6 @@ export function useTooltip(originalProps: UseTooltipProps) {
|
||||
overlayRef,
|
||||
);
|
||||
|
||||
const transitionProps = useMemo<CSSTransitionProps>(
|
||||
() => ({
|
||||
enterTime: originalProps?.disableAnimation ? 0 : 250,
|
||||
leaveTime: originalProps?.disableAnimation ? 0 : 60,
|
||||
clearTime: originalProps?.disableAnimation ? 0 : 60,
|
||||
isVisible: state.isOpen,
|
||||
onEntered: onEntered,
|
||||
onExited: onExited,
|
||||
}),
|
||||
[originalProps?.disableAnimation, state.isOpen],
|
||||
);
|
||||
|
||||
const styles = useMemo(
|
||||
() =>
|
||||
tooltip({
|
||||
@ -201,8 +204,11 @@ export function useTooltip(originalProps: UseTooltipProps) {
|
||||
Component,
|
||||
content,
|
||||
children,
|
||||
mountOverlay,
|
||||
transitionProps,
|
||||
placement: placementProp,
|
||||
isOpen: state.isOpen,
|
||||
disableAnimation: originalProps?.disableAnimation,
|
||||
isDisabled,
|
||||
motionProps,
|
||||
getTriggerProps,
|
||||
getTooltipProps,
|
||||
};
|
||||
|
||||
26
packages/components/tooltip/src/utils.ts
Normal file
26
packages/components/tooltip/src/utils.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import type {TooltipPlacement} from "./types";
|
||||
|
||||
export const getOrigins = (placement: TooltipPlacement) => {
|
||||
const origins = {
|
||||
top: {
|
||||
originY: 1,
|
||||
},
|
||||
bottom: {
|
||||
originY: 0,
|
||||
},
|
||||
start: {
|
||||
originX: 1,
|
||||
},
|
||||
end: {
|
||||
originX: 0,
|
||||
},
|
||||
left: {
|
||||
originX: 1,
|
||||
},
|
||||
right: {
|
||||
originX: 0,
|
||||
},
|
||||
};
|
||||
|
||||
return origins?.[placement] || {};
|
||||
};
|
||||
@ -81,6 +81,7 @@ export default {
|
||||
|
||||
const defaultProps = {
|
||||
...tooltip.defaultVariants,
|
||||
placement: "top",
|
||||
delay: 0,
|
||||
offset: 7,
|
||||
defaultOpen: false,
|
||||
|
||||
@ -9,21 +9,13 @@ import {colorVariants} from "../utils";
|
||||
* @example
|
||||
* <div>
|
||||
* <button>your trigger</button>
|
||||
* <div role="tooltip" className={styles} data-transition='enter/leave'>
|
||||
* <div role="tooltip" className={styles}>
|
||||
* // 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",
|
||||
],
|
||||
base: ["inline-flex", "flex-col", "items-center", "justify-center", "box-border"],
|
||||
variants: {
|
||||
variant: {
|
||||
solid: "",
|
||||
@ -65,7 +57,7 @@ const tooltip = tv({
|
||||
defaultVariants: {
|
||||
variant: "solid",
|
||||
color: "neutral",
|
||||
size: "sm",
|
||||
size: "md",
|
||||
radius: "lg",
|
||||
},
|
||||
compoundVariants: [
|
||||
|
||||
@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Part of this code is taken from @chakra-ui/system ❤️
|
||||
*/
|
||||
|
||||
import {warn, isNumeric} from "@nextui-org/shared-utils";
|
||||
import {AnimatePresence, HTMLMotionProps, motion, Variants as _Variants} from "framer-motion";
|
||||
import {forwardRef, useEffect, useState} from "react";
|
||||
|
||||
import {TRANSITION_EASINGS, Variants, WithTransitionConfig, withDelay} from "./transition-utils";
|
||||
|
||||
export interface CollapseTransitionOptions {
|
||||
/**
|
||||
* If `true`, the opacity of the content will be animated
|
||||
* @default true
|
||||
*/
|
||||
animateOpacity?: boolean;
|
||||
/**
|
||||
* The height you want the content in its collapsed state.
|
||||
* @default 0
|
||||
*/
|
||||
startingHeight?: number;
|
||||
/**
|
||||
* The height you want the content in its expanded state.
|
||||
* @default "auto"
|
||||
*/
|
||||
endingHeight?: number | string;
|
||||
/**
|
||||
* The y-axis offset you want the content in its collapsed state.
|
||||
* @default 10
|
||||
*/
|
||||
startingY?: number;
|
||||
/**
|
||||
* The y-axis offset you want the content in its expanded state.
|
||||
* @default 0
|
||||
*/
|
||||
endingY?: number;
|
||||
}
|
||||
|
||||
const defaultTransitions = {
|
||||
exit: {
|
||||
height: {
|
||||
duration: 0.2,
|
||||
ease: TRANSITION_EASINGS.ease,
|
||||
},
|
||||
opacity: {
|
||||
duration: 0.3,
|
||||
ease: TRANSITION_EASINGS.ease,
|
||||
},
|
||||
},
|
||||
enter: {
|
||||
height: {
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 24,
|
||||
},
|
||||
opacity: {
|
||||
duration: 0.8,
|
||||
ease: TRANSITION_EASINGS.ease,
|
||||
},
|
||||
y: {
|
||||
duration: 0.5,
|
||||
ease: TRANSITION_EASINGS.ease,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const variants: Variants<CollapseTransitionOptions> = {
|
||||
enter: ({animateOpacity, endingHeight, endingY, transition, transitionEnd, delay}) => ({
|
||||
...(animateOpacity && {opacity: 1}),
|
||||
y: endingY,
|
||||
height: endingHeight,
|
||||
transitionEnd: transitionEnd?.enter,
|
||||
transition: transition?.enter ?? withDelay.enter(defaultTransitions.enter, delay),
|
||||
}),
|
||||
exit: ({animateOpacity, startingHeight, transition, startingY, transitionEnd, delay}) => ({
|
||||
...(animateOpacity && {opacity: isNumeric(startingHeight) ? 1 : 0}),
|
||||
y: startingY,
|
||||
height: startingHeight,
|
||||
transitionEnd: transitionEnd?.exit,
|
||||
transition: transition?.exit ?? withDelay.exit(defaultTransitions.exit, delay),
|
||||
}),
|
||||
};
|
||||
|
||||
export type ICollapseTransition = CollapseTransitionProps;
|
||||
|
||||
export interface CollapseTransitionProps
|
||||
extends WithTransitionConfig<HTMLMotionProps<"div">>,
|
||||
CollapseTransitionOptions {}
|
||||
|
||||
export const CollapseTransition = forwardRef<HTMLDivElement, CollapseTransitionProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
in: isOpen,
|
||||
unmountOnExit,
|
||||
animateOpacity = true,
|
||||
startingHeight = 0,
|
||||
endingHeight = "auto",
|
||||
startingY = 10,
|
||||
endingY = 0,
|
||||
style,
|
||||
className,
|
||||
transition,
|
||||
transitionEnd,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setMounted(true);
|
||||
});
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Warn 🚨: `startingHeight` and `unmountOnExit` are mutually exclusive
|
||||
*
|
||||
* If you specify a starting height, the collapsed needs to be mounted
|
||||
* for the height to take effect.
|
||||
*/
|
||||
|
||||
if (Boolean(startingHeight > 0 && unmountOnExit)) {
|
||||
warn(
|
||||
`startingHeight and unmountOnExit are mutually exclusive. You can't use them together`,
|
||||
"FramerTransitions - Collapse",
|
||||
);
|
||||
}
|
||||
|
||||
const hasStartingHeight = parseFloat(startingHeight.toString()) > 0;
|
||||
|
||||
const custom = {
|
||||
startingHeight,
|
||||
endingHeight,
|
||||
startingY,
|
||||
endingY,
|
||||
animateOpacity,
|
||||
transition: !mounted ? {enter: {duration: 0}} : transition,
|
||||
transitionEnd: {
|
||||
enter: transitionEnd?.enter,
|
||||
exit: unmountOnExit
|
||||
? transitionEnd?.exit
|
||||
: {
|
||||
...transitionEnd?.exit,
|
||||
display: hasStartingHeight ? "block" : "none",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const show = unmountOnExit ? isOpen : true;
|
||||
const animate = isOpen || unmountOnExit ? "enter" : "exit";
|
||||
|
||||
return (
|
||||
<AnimatePresence custom={custom} initial={false}>
|
||||
{show && (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
{...rest}
|
||||
animate={animate}
|
||||
className={className}
|
||||
custom={custom}
|
||||
exit="exit"
|
||||
initial={unmountOnExit ? "exit" : false}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
display: "block",
|
||||
...style,
|
||||
}}
|
||||
variants={variants as _Variants}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CollapseTransition.displayName = "NextUI.CollapseTransition";
|
||||
@ -1,176 +0,0 @@
|
||||
/**
|
||||
* Part of this code is taken from @chakra-ui/system ❤️
|
||||
*/
|
||||
|
||||
import {warn, isNumeric} from "@nextui-org/shared-utils";
|
||||
import {AnimatePresence, HTMLMotionProps, motion, Variants as _Variants} from "framer-motion";
|
||||
import {forwardRef, useEffect, useState} from "react";
|
||||
|
||||
import {TRANSITION_EASINGS, Variants, WithTransitionConfig, withDelay} from "./transition-utils";
|
||||
|
||||
export interface CollapseOptions {
|
||||
/**
|
||||
* If `true`, the opacity of the content will be animated
|
||||
* @default true
|
||||
*/
|
||||
animateOpacity?: boolean;
|
||||
/**
|
||||
* The height you want the content in its collapsed state.
|
||||
* @default 0
|
||||
*/
|
||||
startingHeight?: number;
|
||||
/**
|
||||
* The height you want the content in its expanded state.
|
||||
* @default "auto"
|
||||
*/
|
||||
endingHeight?: number | string;
|
||||
/**
|
||||
* The y-axis offset you want the content in its collapsed state.
|
||||
* @default 10
|
||||
*/
|
||||
startingY?: number;
|
||||
/**
|
||||
* The y-axis offset you want the content in its expanded state.
|
||||
* @default 0
|
||||
*/
|
||||
endingY?: number;
|
||||
}
|
||||
|
||||
const defaultTransitions = {
|
||||
exit: {
|
||||
height: {
|
||||
duration: 0.2,
|
||||
ease: TRANSITION_EASINGS.ease,
|
||||
},
|
||||
opacity: {
|
||||
duration: 0.3,
|
||||
ease: TRANSITION_EASINGS.ease,
|
||||
},
|
||||
},
|
||||
enter: {
|
||||
height: {
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 24,
|
||||
},
|
||||
opacity: {
|
||||
duration: 0.8,
|
||||
ease: TRANSITION_EASINGS.ease,
|
||||
},
|
||||
y: {
|
||||
duration: 0.5,
|
||||
ease: TRANSITION_EASINGS.ease,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const variants: Variants<CollapseOptions> = {
|
||||
enter: ({animateOpacity, endingHeight, endingY, transition, transitionEnd, delay}) => ({
|
||||
...(animateOpacity && {opacity: 1}),
|
||||
y: endingY,
|
||||
height: endingHeight,
|
||||
transitionEnd: transitionEnd?.enter,
|
||||
transition: transition?.enter ?? withDelay.enter(defaultTransitions.enter, delay),
|
||||
}),
|
||||
exit: ({animateOpacity, startingHeight, transition, startingY, transitionEnd, delay}) => ({
|
||||
...(animateOpacity && {opacity: isNumeric(startingHeight) ? 1 : 0}),
|
||||
y: startingY,
|
||||
height: startingHeight,
|
||||
transitionEnd: transitionEnd?.exit,
|
||||
transition: transition?.exit ?? withDelay.exit(defaultTransitions.exit, delay),
|
||||
}),
|
||||
};
|
||||
|
||||
export type ICollapse = CollapseProps;
|
||||
|
||||
export interface CollapseProps
|
||||
extends WithTransitionConfig<HTMLMotionProps<"div">>,
|
||||
CollapseOptions {}
|
||||
|
||||
export const Collapse = forwardRef<HTMLDivElement, CollapseProps>((props, ref) => {
|
||||
const {
|
||||
in: isOpen,
|
||||
unmountOnExit,
|
||||
animateOpacity = true,
|
||||
startingHeight = 0,
|
||||
endingHeight = "auto",
|
||||
startingY = 10,
|
||||
endingY = 0,
|
||||
style,
|
||||
className,
|
||||
transition,
|
||||
transitionEnd,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setMounted(true);
|
||||
});
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Warn 🚨: `startingHeight` and `unmountOnExit` are mutually exclusive
|
||||
*
|
||||
* If you specify a starting height, the collapsed needs to be mounted
|
||||
* for the height to take effect.
|
||||
*/
|
||||
|
||||
if (Boolean(startingHeight > 0 && unmountOnExit)) {
|
||||
warn(
|
||||
`startingHeight and unmountOnExit are mutually exclusive. You can't use them together`,
|
||||
"FramerTransitions - Collapse",
|
||||
);
|
||||
}
|
||||
|
||||
const hasStartingHeight = parseFloat(startingHeight.toString()) > 0;
|
||||
|
||||
const custom = {
|
||||
startingHeight,
|
||||
endingHeight,
|
||||
startingY,
|
||||
endingY,
|
||||
animateOpacity,
|
||||
transition: !mounted ? {enter: {duration: 0}} : transition,
|
||||
transitionEnd: {
|
||||
enter: transitionEnd?.enter,
|
||||
exit: unmountOnExit
|
||||
? transitionEnd?.exit
|
||||
: {
|
||||
...transitionEnd?.exit,
|
||||
display: hasStartingHeight ? "block" : "none",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const show = unmountOnExit ? isOpen : true;
|
||||
const animate = isOpen || unmountOnExit ? "enter" : "exit";
|
||||
|
||||
return (
|
||||
<AnimatePresence custom={custom} initial={false}>
|
||||
{show && (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
{...rest}
|
||||
animate={animate}
|
||||
className={className}
|
||||
custom={custom}
|
||||
exit="exit"
|
||||
initial={unmountOnExit ? "exit" : false}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
display: "block",
|
||||
...style,
|
||||
}}
|
||||
variants={variants as _Variants}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
});
|
||||
|
||||
Collapse.displayName = "NextUI.Collapse";
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./collapse";
|
||||
export * from "./collapse-transition";
|
||||
export * from "./transition-utils";
|
||||
|
||||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
@ -729,6 +729,7 @@ importers:
|
||||
'@nextui-org/aria-utils': workspace:*
|
||||
'@nextui-org/button': workspace:*
|
||||
'@nextui-org/dom-utils': workspace:*
|
||||
'@nextui-org/framer-transitions': workspace:*
|
||||
'@nextui-org/react-utils': workspace:*
|
||||
'@nextui-org/shared-utils': workspace:*
|
||||
'@nextui-org/system': workspace:*
|
||||
@ -741,10 +742,12 @@ importers:
|
||||
'@react-types/overlays': ^3.7.0
|
||||
'@react-types/tooltip': ^3.3.0
|
||||
clean-package: 2.2.0
|
||||
framer-motion: ^10.6.0
|
||||
react: ^18.2.0
|
||||
dependencies:
|
||||
'@nextui-org/aria-utils': link:../../utilities/aria-utils
|
||||
'@nextui-org/dom-utils': link:../../utilities/dom-utils
|
||||
'@nextui-org/framer-transitions': link:../../utilities/framer-transitions
|
||||
'@nextui-org/react-utils': link:../../utilities/react-utils
|
||||
'@nextui-org/shared-utils': link:../../utilities/shared-utils
|
||||
'@nextui-org/system': link:../../core/system
|
||||
@ -754,6 +757,7 @@ importers:
|
||||
'@react-aria/tooltip': 3.4.0_react@18.2.0
|
||||
'@react-aria/utils': 3.15.0_react@18.2.0
|
||||
'@react-stately/tooltip': 3.3.0_react@18.2.0
|
||||
framer-motion: 10.6.0_react@18.2.0
|
||||
devDependencies:
|
||||
'@nextui-org/button': link:../button
|
||||
'@react-types/overlays': 3.7.0_react@18.2.0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user