import React, { isValidElement, forwardRef, useMemo } from 'react'; import { getWebProps } from 'react-native-unistyles/web'; import { useUnistyles } from 'react-native-unistyles'; import { IconButtonProps } from './types'; import { iconButtonStyles } from './IconButton.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 type { Theme } from '@idealyst/theme'; /** * Circular icon button component with multiple visual variants and sizes. * Supports contained, outlined, and text styles with customizable intent colors. */ 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, accessibilityOwns, accessibilityHasPopup, } = props; // 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; // Apply variants for size, disabled, gradient iconButtonStyles.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( 'IconButton: 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('icon-button'), [id]); // Generate ARIA props - accessibilityLabel is critical for icon-only buttons const ariaProps = useMemo(() => { const computedLabel = accessibilityLabel ?? (typeof icon === 'string' ? icon : undefined); return getWebInteractiveAriaProps({ accessibilityLabel: computedLabel, accessibilityHint, accessibilityDisabled: accessibilityDisabled ?? isDisabled, accessibilityHidden, accessibilityRole: accessibilityRole ?? 'button', accessibilityLabelledBy, accessibilityDescribedBy, accessibilityControls, accessibilityExpanded, accessibilityPressed, accessibilityOwns, accessibilityHasPopup, }); }, [ accessibilityLabel, icon, 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 = [ (iconButtonStyles.button as any)(dynamicProps), style as any, ]; // Use getWebProps to generate className and ref for web const webProps = getWebProps(buttonStyleArray); // Icon styles with dynamic function const iconStyleComputed = (iconButtonStyles.icon as any)(dynamicProps); const iconStyleArray = [iconStyleComputed]; const iconProps = getWebProps(iconStyleArray); // Spinner styles that match the icon color const spinnerStyleArray = [(iconButtonStyles.spinner as any)(dynamicProps)]; const spinnerProps = getWebProps(spinnerStyleArray); // Helper to render icon const renderIcon = () => { if (typeof icon === 'string') { return ( ); } else if (isValidElement(icon)) { return icon; } return null; }; // 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 ( ); }); IconButton.displayName = 'IconButton'; export default IconButton;