import { forwardRef, useMemo } from 'react'; import { View } from 'react-native'; import { GestureDetector } from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; import { CloseIcon } from '../../helpers/components/close-icon'; import { HeroText } from '../../helpers/components/hero-text'; import { AnimationSettingsProvider } from '../../helpers/contexts/animation-settings-context'; import { cn, useThemeColor } from '../../helpers/theme'; import type { ViewRef } from '../../helpers/types'; import { createContext } from '../../helpers/utils'; import * as ToastPrimitive from '../../primitives/toast'; import type { ToastComponentProps } from '../../providers/toast'; import { useToastConfig } from '../../providers/toast/toast-config.context'; import { Button } from '../button'; import type { PressableFeedbackHighlightAnimation } from '../pressable-feedback'; import { useToastRootAnimation } from './toast.animation'; import { DISPLAY_NAME } from './toast.constants'; import toastStyles, { styleSheet } from './toast.styles'; import type { DefaultToastProps, ToastActionProps, ToastCloseProps, ToastContextValue, ToastDescriptionProps, ToastRootProps, ToastTitleProps, } from './toast.types'; const AnimatedToastRoot = Animated.createAnimatedComponent(ToastPrimitive.Root); const [ToastProvider, useToast] = createContext({ name: 'ToastContext', }); // -------------------------------------------------- const ToastRoot = forwardRef((props, ref) => { const globalConfig = useToastConfig(); const { children, variant: localVariant, placement: localPlacement, index, total, heights, maxVisibleToasts, className, style, animation: localAnimation, isSwipeable: localIsSwipeable, isAnimatedStyleActive = true, hide, ...restProps } = props; /** * Merge global config with local props, ensuring local props take precedence */ const variant = localVariant ?? globalConfig?.variant ?? 'default'; const placement = localPlacement ?? globalConfig?.placement ?? 'top'; const animation = localAnimation ?? globalConfig?.animation; const isSwipeable = localIsSwipeable ?? globalConfig?.isSwipeable; // Access id from props (id is omitted from ToastRootProps type but available at runtime) const toastProps = props as ToastRootProps & Pick; const { id } = toastProps; const rootClassName = toastStyles.root({ className, }); const { rContainerStyle, entering, exiting, panGesture, isAllAnimationsDisabled, } = useToastRootAnimation({ animation, index, total, heights, placement, hide, id, isSwipeable, maxVisibleToasts, }); const rootStyle = isAnimatedStyleActive ? [styleSheet.root, rContainerStyle, style] : [styleSheet.root, style]; const animationSettingsContextValue = useMemo( () => ({ isAllAnimationsDisabled, }), [isAllAnimationsDisabled] ); const contextValue = useMemo( () => ({ variant, hide, id, }), [variant, hide, id] ); return ( {/* Animated toast instance */} {children} {/* Hidden toast instance for height measurement */} { const measuredHeight = event.nativeEvent.layout.height; heights.modify((value) => { 'worklet'; return { ...value, [id]: measuredHeight }; }); }} {...restProps} > {children} ); }); // -------------------------------------------------- const ToastTitle = forwardRef((props, ref) => { const { children, className, ...restProps } = props; const { variant } = useToast(); const tvStyles = toastStyles.label({ variant, className, }); return ( {children} ); }); // -------------------------------------------------- const ToastDescription = forwardRef( (props, ref) => { const { children, className, ...restProps } = props; const tvStyles = toastStyles.description({ className, }); return ( {children} ); } ); // -------------------------------------------------- const ToastAction = forwardRef((props, ref) => { const { children, variant, size = 'sm', className, ...restProps } = props; const { variant: toastVariant } = useToast(); const tvStyles = toastStyles.action({ variant: toastVariant, className, }); const [ themeColorDefaultHover, themeColorAccentHover, themeColorSuccessHover, themeColorWarningHover, themeColorDangerHover, ] = useThemeColor([ 'default-hover', 'accent-hover', 'success-hover', 'warning-hover', 'danger-hover', ]); const highlightColorMap = useMemo(() => { switch (toastVariant) { case 'default': return themeColorDefaultHover; case 'accent': return themeColorAccentHover; case 'success': return themeColorSuccessHover; case 'warning': return themeColorWarningHover; case 'danger': return themeColorDangerHover; } }, [ toastVariant, themeColorDefaultHover, themeColorAccentHover, themeColorSuccessHover, themeColorWarningHover, themeColorDangerHover, ]); const buttonVariant = useMemo(() => { if (variant) return variant; switch (toastVariant) { case 'accent': return 'primary'; case 'danger': return 'danger'; default: return 'tertiary'; } }, [toastVariant, variant]); const highlightAnimationConfig = useMemo( () => ({ backgroundColor: { value: highlightColorMap }, opacity: { value: [0, 1] }, }), [highlightColorMap] ); return ( ); }); // -------------------------------------------------- const ToastClose = forwardRef((props, ref) => { const { children, iconProps, size = 'sm', className, onPress, ...restProps } = props; const { hide, id } = useToast(); const themeColorMuted = useThemeColor('muted'); /** * Handle close button press * If hide and id are available from context, use them to hide the toast * Otherwise, use the provided onPress handler */ const handlePress = (event: any) => { if (hide && id) { hide(id); } if (onPress && typeof onPress === 'function') { onPress(event); } }; return ( ); }); // -------------------------------------------------- /** * Default styled toast component for simplified toast.show() API * Used internally when showing toasts with string or config object (without component) */ export function DefaultToast(props: DefaultToastProps) { const globalConfig = useToastConfig(); const { id, variant: localVariant, placement: localPlacement, isSwipeable: localIsSwipeable, animation: localAnimation, label, description, actionLabel, onActionPress, icon, hide, show, ...toastComponentProps } = props; /** * Merge global config with local props, ensuring local props take precedence */ const variant = localVariant ?? globalConfig?.variant ?? 'default'; const placement = localPlacement ?? globalConfig?.placement ?? 'top'; const isSwipeable = localIsSwipeable ?? globalConfig?.isSwipeable; const animation = localAnimation ?? globalConfig?.animation; const handleActionPress = () => { if (onActionPress) { onActionPress({ show, hide }); } }; return ( {icon && {icon}} {label && {label}} {description && {description}} {actionLabel && ( {actionLabel} )} ); } // -------------------------------------------------- ToastRoot.displayName = DISPLAY_NAME.TOAST_ROOT; ToastTitle.displayName = DISPLAY_NAME.TOAST_TITLE; ToastDescription.displayName = DISPLAY_NAME.TOAST_DESCRIPTION; ToastAction.displayName = DISPLAY_NAME.TOAST_ACTION; ToastClose.displayName = DISPLAY_NAME.TOAST_CLOSE; /** * Compound Toast component with sub-components * * @component Toast - Main toast container that displays notification messages with various variants. * * @component Toast.Title - Title/heading text of the toast notification. * * @component Toast.Description - Descriptive text content of the toast. * * @component Toast.Action - Action button within the toast. Variant is automatically determined * based on toast variant but can be overridden. * * @component Toast.Close - Close button for dismissing the toast. Renders as an icon-only button. * * Props flow from Toast to sub-components via context (variant). * * @see Full documentation: https://heroui.com/components/toast */ const CompoundToast = Object.assign(ToastRoot, { /** Toast title - renders text content */ Title: ToastTitle, /** Toast description - renders descriptive text */ Description: ToastDescription, /** Toast action button - renders action with appropriate variant */ Action: ToastAction, /** Toast close button - renders icon-only close button */ Close: ToastClose, }); export default CompoundToast;