import React, { isValidElement, forwardRef, useMemo } from 'react'; import { getWebProps } from 'react-native-unistyles/web'; import { ButtonProps } from './types'; import { buttonStyles } from './Button.styles'; import { IconSvg } from '../Icon/IconSvg/IconSvg.web'; import { createPressEvent } from '../utils/events'; import useMergeRefs from '../hooks/useMergeRefs'; import { getWebInteractiveAriaProps, generateAccessibilityId } from '../utils/accessibility'; import type { IdealystElement } from '../utils/refTypes'; import { flattenStyle } from '../utils/flattenStyle'; /** * Interactive button component with multiple visual variants, sizes, and icon support. * Supports contained, outlined, and text styles with customizable intent colors. */ 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, accessibilityOwns, accessibilityHasPopup, } = props; // Button is effectively disabled when loading const isDisabled = disabled || loading; // Apply variants for size, disabled, gradient buttonStyles.useVariants({ size, disabled: isDisabled, gradient, }); // Determine the handler to use - onPress takes precedence const pressHandler = onPress ?? onClick; // Warn about deprecated onClick usage in development if (process.env.NODE_ENV !== 'production' && onClick && !onPress) { console.warn( 'Button: onClick prop is deprecated. Use onPress instead for cross-platform compatibility.' ); } const handleClick = (e: React.MouseEvent) => { if (!isDisabled && pressHandler) { pressHandler(createPressEvent(e)); } }; // Generate unique ID for accessibility const buttonId = useMemo(() => id || generateAccessibilityId('button'), [id]); // Generate ARIA props - especially important for icon-only buttons const ariaProps = useMemo(() => { // For icon-only buttons, accessibilityLabel is critical const isIconOnly = !children && (leftIcon || rightIcon); const computedLabel = accessibilityLabel ?? (isIconOnly && typeof leftIcon === 'string' ? leftIcon : undefined); return getWebInteractiveAriaProps({ accessibilityLabel: computedLabel, accessibilityHint, accessibilityDisabled: accessibilityDisabled ?? isDisabled, accessibilityHidden, accessibilityRole: accessibilityRole ?? 'button', accessibilityLabelledBy, accessibilityDescribedBy, accessibilityControls, accessibilityExpanded, accessibilityPressed, accessibilityOwns, accessibilityHasPopup, }); }, [ accessibilityLabel, children, leftIcon, rightIcon, accessibilityHint, accessibilityDisabled, isDisabled, accessibilityHidden, accessibilityRole, accessibilityLabelledBy, accessibilityDescribedBy, accessibilityControls, accessibilityExpanded, accessibilityPressed, accessibilityOwns, accessibilityHasPopup, ]); // Compute dynamic styles with all props for full flexibility const dynamicProps = { intent, type, size, disabled: isDisabled, gradient }; const buttonStyleArray = [ (buttonStyles.button as any)(dynamicProps), (buttonStyles.text as any)(dynamicProps), flattenStyle(style), ]; // Use getWebProps to generate className and ref for web const webProps = getWebProps(buttonStyleArray); // Icon container styles const iconContainerProps = getWebProps([(buttonStyles.iconContainer as any)(dynamicProps)]); // Icon styles with dynamic function const iconStyleArray = [(buttonStyles.icon as any)(dynamicProps)]; const iconProps = getWebProps(iconStyleArray); // Spinner styles that match the text color const spinnerStyleArray = [(buttonStyles.spinner as any)(dynamicProps)]; const spinnerProps = getWebProps(spinnerStyleArray); // Helper to render icon - now uses icon name directly const renderIcon = (icon: string | React.ReactNode) => { if (typeof icon === 'string') { // Render IconSvg with the icon name - registry lookup happens inside return ( ); } else if (isValidElement(icon)) { // Render custom component as-is return icon; } return null; }; const buttonContent = children; // Determine if we need to wrap content in icon container const hasIcons = leftIcon || rightIcon; // Render spinner with inline CSS animation (absolutely centered) const renderSpinner = () => ( <> ); // Merge unistyles web ref with forwarded ref const mergedRef = useMergeRefs(ref, webProps.ref); // Content opacity - hide when loading but keep for sizing const contentStyle = loading ? { opacity: 0 } : undefined; return ( ); }); Button.displayName = 'Button'; export default Button;