import { forwardRef, useRef, useMemo } from 'react'; import { ActivityIndicator, StyleSheet as RNStyleSheet, TouchableOpacity, View } from 'react-native'; import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons'; import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg'; import { useUnistyles } from 'react-native-unistyles'; import { iconButtonStyles } from './IconButton.styles'; import { IconButtonProps } from './types'; import { createPressEvent } from '../utils/events'; import { getNativeInteractiveAccessibilityProps } from '../utils/accessibility'; import useMergeRefs from '../hooks/useMergeRefs'; import type { IdealystElement } from '../utils/refTypes'; import type { Theme } from '@idealyst/theme'; const IconButton = forwardRef((props, ref) => { const { icon, onPress, onClick, disabled = false, loading = false, type = 'contained', intent = 'primary', size = 'md', gradient, style, testID, id, // Accessibility props accessibilityLabel, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole, accessibilityLabelledBy, accessibilityDescribedBy, accessibilityControls, accessibilityExpanded, accessibilityPressed, } = props; const internalRef = useRef(null); const mergedRef = useMergeRefs(ref, internalRef); // Get theme for icon size const { theme } = useUnistyles() as { theme: Theme }; // Button is effectively disabled when loading const isDisabled = disabled || loading; // Get icon size directly from theme const iconSize = theme.sizes.iconButton[size]?.iconSize ?? 24; // Determine the handler to use - onPress takes precedence const pressHandler = onPress ?? onClick; // Warn about deprecated onClick usage in development if (__DEV__ && onClick && !onPress) { console.warn( 'IconButton: onClick prop is deprecated. Use onPress instead for cross-platform compatibility.' ); } // Apply variants for size, disabled, gradient iconButtonStyles.useVariants({ size, disabled: isDisabled, gradient, }); // Compute dynamic styles with all props for full flexibility const dynamicProps = { intent, type, size, disabled: isDisabled, gradient }; const buttonStyle = (iconButtonStyles.button as any)(dynamicProps); const iconStyle = (iconButtonStyles.icon as any)(dynamicProps); const spinnerStyle = (iconButtonStyles.spinner as any)(dynamicProps); // Gradient is only applicable to contained buttons const showGradient = gradient && type === 'contained'; // Generate native accessibility props const nativeA11yProps = useMemo(() => { const computedLabel = accessibilityLabel ?? (typeof icon === 'string' ? icon : undefined); return getNativeInteractiveAccessibilityProps({ accessibilityLabel: computedLabel, accessibilityHint, accessibilityDisabled: accessibilityDisabled ?? isDisabled, accessibilityHidden, accessibilityRole: accessibilityRole ?? 'button', accessibilityLabelledBy, accessibilityDescribedBy, accessibilityControls, accessibilityExpanded, accessibilityPressed, }); }, [ accessibilityLabel, icon, accessibilityHint, accessibilityDisabled, isDisabled, accessibilityHidden, accessibilityRole, accessibilityLabelledBy, accessibilityDescribedBy, accessibilityControls, accessibilityExpanded, accessibilityPressed, ]); // Render gradient background layer const renderGradientLayer = () => { if (!showGradient) return null; const [startColor, endColor] = useMemo(() => { switch (gradient) { case 'darken': return [{ stopColor: 'black', stopOpacity: 0, }, { stopColor: 'black', stopOpacity: 0.15, }]; case 'lighten': return [{ stopColor: 'white', stopOpacity: 0, }, { stopColor: 'white', stopOpacity: 0.2, }]; default: return [{ stopColor: 'black', stopOpacity: 0, }, { stopColor: 'black', stopOpacity: 0, }]; } }, [gradient]); return ( ); }; // TouchableOpacity types don't include nativeID but it's a valid RN prop const touchableProps = { ref: mergedRef, onPress: pressHandler ? (e: any) => pressHandler(createPressEvent(e, 'press', internalRef)) : undefined, disabled: isDisabled, testID, nativeID: id, activeOpacity: 0.7, style: [ buttonStyle, showGradient && { overflow: 'hidden' }, style, ], accessibilityState: loading ? { busy: true } : undefined, ...nativeA11yProps, }; // Get spinner color from the spinner style (matches icon color) const spinnerColor = spinnerStyle?.color || (type === 'contained' ? '#fff' : undefined); // Content opacity - hide when loading but keep for sizing const contentOpacity = loading ? 0 : 1; // Render icon const renderIcon = () => { if (typeof icon === 'string') { return ( ); } // Custom ReactNode icon return ( {icon} ); }; return ( {renderGradientLayer()} {/* Centered spinner overlay */} {loading && ( )} {/* Icon renders directly - TouchableOpacity has alignItems/justifyContent center */} {renderIcon()} ); }); IconButton.displayName = 'IconButton'; export default IconButton;