import { forwardRef, useRef, useMemo } from 'react'; import { ActivityIndicator, StyleSheet as RNStyleSheet, Text, 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 { buttonStyles } from './Button.styles'; import { ButtonProps } from './types'; import { createPressEvent } from '../utils/events'; import { getNativeInteractiveAccessibilityProps } from '../utils/accessibility'; import useMergeRefs from '../hooks/useMergeRefs'; import type { IdealystElement } from '../utils/refTypes'; const Button = forwardRef((props, ref) => { const { children, onPress, onClick, disabled = false, loading = false, type = 'contained', intent = 'primary', size = 'md', gradient, leftIcon, rightIcon, 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); // Button is effectively disabled when loading const isDisabled = disabled || loading; // 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( 'Button: onClick prop is deprecated. Use onPress instead for cross-platform compatibility.' ); } // Apply variants for size, disabled, gradient buttonStyles.useVariants({ size, disabled: isDisabled, gradient, }); // Compute dynamic styles with all props for full flexibility const dynamicProps = { intent, type, size, disabled: isDisabled, gradient }; const buttonStyle = (buttonStyles.button as any)(dynamicProps); const textStyle = (buttonStyles.text as any)(dynamicProps); const iconStyle = (buttonStyles.icon as any)(dynamicProps); const iconContainerStyle = (buttonStyles.iconContainer as any)(dynamicProps); const spinnerStyle = (buttonStyles.spinner as any)(dynamicProps); // Gradient is only applicable to contained buttons const showGradient = gradient && type === 'contained'; // Map button size to icon size const iconSizeMap: Record = { xs: 12, sm: 14, md: 16, lg: 18, xl: 20, }; const iconSize = iconSizeMap[size] ?? 16; const buttonContent = children; // Determine if we need to wrap content in icon container const hasIcons = leftIcon || rightIcon; // Generate native accessibility props - especially important for icon-only buttons const nativeA11yProps = useMemo(() => { const isIconOnly = !buttonContent && (leftIcon || rightIcon); const computedLabel = accessibilityLabel ?? (isIconOnly && typeof leftIcon === 'string' ? leftIcon : undefined); return getNativeInteractiveAccessibilityProps({ accessibilityLabel: computedLabel, accessibilityHint, accessibilityDisabled: accessibilityDisabled ?? isDisabled, accessibilityHidden, accessibilityRole: accessibilityRole ?? 'button', accessibilityLabelledBy, accessibilityDescribedBy, accessibilityControls, accessibilityExpanded, accessibilityPressed, }); }, [ accessibilityLabel, buttonContent, leftIcon, rightIcon, 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 text color) const spinnerColor = spinnerStyle?.color || (type === 'contained' ? '#fff' : undefined); // Content opacity - hide when loading but keep for sizing const contentOpacity = loading ? 0 : 1; return ( {renderGradientLayer()} {/* Centered spinner overlay */} {loading && ( )} {/* Content with opacity 0 when loading to maintain size */} {hasIcons ? ( {leftIcon && ( )} {buttonContent} {rightIcon && ( )} ) : ( {buttonContent} )} ); }); Button.displayName = 'Button'; export default Button;