import React, { forwardRef } from 'react'; import type { StyleProp, TextStyle, ViewStyle } from 'react-native'; import { Animated, LayoutAnimation, Platform, StyleSheet, UIManager, } from 'react-native'; import { useTheme } from '../../theme'; import type { IconName } from '../Icon'; import { AnimatedFABIcon } from './AnimatedFABIcon'; import { StyledFAB, StyledFABIcon, StyledFABText, StyledIconContainer, } from './StyledFAB'; import { noop } from '../../utils/functions'; export type FABHandles = { show: (animated?: boolean, callback?: Animated.EndCallback) => void; collapse: (animated?: boolean, callback?: Animated.EndCallback) => void; hide: (animated?: boolean, callback?: Animated.EndCallback) => void; }; if (Platform.OS === 'android') { if (UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); } } export interface FABProps { /** * Name of the Icon. */ icon: IconName; /** * title of the component. */ title?: string; /** * This function is called on pressing the button. */ onPress?: () => void; /** * Specify if the button is animated. */ animated?: boolean; /** * Specify if the button is in active state. It only works if animated is true. */ active?: boolean; /** * Additional style. */ style?: StyleProp; /** * Testing id of the component. */ testID?: string; } // Extends FABProps with internal-only props. Not part of the public API. type InternalFABProps = FABProps & { titleStyle?: StyleProp; }; const IconOnlyContent = ({ icon, animated, active, }: { icon: IconName; animated?: boolean; active?: boolean; }) => { if (animated) { return ( ); } return ( ); }; const IconWithTextContent = ({ icon, title, titleStyle, }: { icon: IconName; title?: string; titleStyle?: StyleProp; }) => ( <> {title} ); const animateWidth = () => { LayoutAnimation.configureNext({ duration: Platform.OS === 'ios' ? 200 : 400, update: { type: 'spring', springDamping: Platform.OS === 'ios' ? 1 : 1.5, }, }); }; // Full implementation — accepts InternalFABProps including titleStyle. // Exported as FABInternal for use by sibling HD components (e.g. FAB.Pair). const FABWithTitleStyle = forwardRef( ( { onPress, title, icon, animated: iconAnimated, testID, active, style, titleStyle, }, ref ) => { const theme = useTheme(); const [displayState, setDisplayState] = React.useState({ hideTitle: false, hideButton: false, }); const isIconOnly = displayState.hideTitle || active || !title; const animatedValues = { opacity: React.useRef(new Animated.Value(1)).current, width: React.useRef(new Animated.Value(1)).current, translateY: React.useRef(new Animated.Value(0)).current, }; const marginBottom = Number(StyleSheet.flatten(style)?.marginBottom) || 0; const [buttonWidth, setButtonWidth] = React.useState(0); const hasSetButtonWidth = buttonWidth > 0; React.useImperativeHandle( ref, () => ({ show: (animated = true, callback: Animated.EndCallback = noop) => { setDisplayState({ hideButton: false, hideTitle: false, }); animateWidth(); if (animated) { Animated.parallel([ Animated.spring(animatedValues.opacity, { toValue: 1, useNativeDriver: true, }), Animated.spring(animatedValues.translateY, { toValue: 0, useNativeDriver: true, }), ]).start(callback); } else { animatedValues.opacity.setValue(1); animatedValues.translateY.setValue(0); } }, collapse: (animated = true, callback: Animated.EndCallback = noop) => { setDisplayState({ hideButton: false, hideTitle: true, }); animateWidth(); if (animated) { Animated.parallel([ Animated.spring(animatedValues.opacity, { toValue: 1, useNativeDriver: true, }), Animated.spring(animatedValues.translateY, { toValue: 0, useNativeDriver: true, }), ]).start(callback); } else { animatedValues.opacity.setValue(1); animatedValues.translateY.setValue(0); } }, hide: (animated = true, callback: Animated.EndCallback = noop) => { if (animated) { Animated.stagger(20, [ Animated.spring(animatedValues.opacity, { toValue: 0, useNativeDriver: true, }), Animated.spring(animatedValues.translateY, { toValue: 1, useNativeDriver: true, }), ]).start((arg) => { animateWidth(); setDisplayState((previousState) => ({ ...previousState, hideButton: true, })); if (callback) { callback(arg); } }); } else { animatedValues.opacity.setValue(0); animatedValues.translateY.setValue(1); animateWidth(); setDisplayState((previousState) => ({ ...previousState, hideButton: true, })); } }, }), [] ); return ( !hasSetButtonWidth && !active && setButtonWidth(event.nativeEvent.layout.width) } activeOpacity={0.8} onPress={onPress} themeIconOnly={isIconOnly} style={[ style, { bottom: displayState.hideButton ? -(marginBottom + theme.__hd__.fab.sizes.height * 2) : StyleSheet.flatten(style)?.bottom, transform: [ { translateY: animatedValues.translateY.interpolate({ inputRange: [0, 1], outputRange: [ 0, marginBottom + theme.__hd__.fab.sizes.height * 2, ], }), }, ], }, ]} testID={testID} themeActive={active} > {isIconOnly ? ( ) : ( )} ); } ); FABWithTitleStyle.displayName = 'FAB'; // Public export — clean FABProps, no internal props exposed. // Docgen reads this component and sees only FABProps. const FAB = forwardRef((props, ref) => ( )); FAB.displayName = 'FAB'; export { FABWithTitleStyle as FABInternal }; export default FAB;