All files / src/components/ui/TouchButton index.tsx

0% Statements 0/158
100% Branches 0/0
0% Functions 0/1
0% Lines 0/158

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159                                                                                                                                                                                                                                                                                                                             
'use client';

/**
 * TouchButton Component
 *
 * A button component optimized for touch interactions with minimum 44px tap targets.
 * Follows WCAG 2.1 Success Criterion 2.5.5 for Target Size.
 *
 * @example
 * ```tsx
 * <TouchButton onClick={handleClick} icon="home" label="Home" />
 * <TouchButton onClick={handleClick} variant="primary">Submit</TouchButton>
 * ```
 */

import { forwardRef, ButtonHTMLAttributes, ReactNode } from 'react';
import { cn } from '@/lib/core';
import { Icon, IconName } from '@/components/ui/icons';

export interface TouchButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  /** Visual variant of the button */
  variant?: 'default' | 'primary' | 'secondary' | 'ghost' | 'danger';
  /** Size variant - all maintain 44px minimum touch target */
  size?: 'sm' | 'md' | 'lg';
  /** Icon name from the icon registry */
  icon?: IconName;
  /** Icon position relative to children */
  iconPosition?: 'left' | 'right';
  /** Whether the button is in a loading state */
  isLoading?: boolean;
  /** Whether the button should take full width */
  fullWidth?: boolean;
  /** Badge count to display */
  badge?: number;
  /** Accessible label (required when using icon-only button) */
  'aria-label'?: string;
  /** Button children */
  children?: ReactNode;
}

const variantStyles = {
  default: 'bg-white dark:bg-gray-800 text-gray-900 dark:text-white border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700',
  primary: 'bg-primary-600 text-white hover:bg-primary-700 dark:bg-primary-500 dark:hover:bg-primary-600',
  secondary: 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-600',
  ghost: 'bg-transparent text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800',
  danger: 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600',
};

const sizeStyles = {
  sm: 'min-h-[44px] min-w-[44px] px-3 text-sm',
  md: 'min-h-[44px] min-w-[44px] px-4 text-base',
  lg: 'min-h-[56px] min-w-[56px] px-6 text-lg',
};

const iconSizes = {
  sm: 20,
  md: 24,
  lg: 28,
};

export const TouchButton = forwardRef<HTMLButtonElement, TouchButtonProps>(
  (
    {
      variant = 'default',
      size = 'md',
      icon,
      iconPosition = 'left',
      isLoading = false,
      fullWidth = false,
      badge,
      className,
      children,
      disabled,
      type = 'button',
      ...props
    },
    ref
  ) => {
    const iconSize = iconSizes[size];
    const hasChildren = !!children;
    const isIconOnly = icon && !hasChildren;

    return (
      <button
        ref={ref}
        type={type}
        disabled={disabled || isLoading}
        className={cn(
          // Base styles
          'relative inline-flex items-center justify-center',
          'rounded-lg font-medium',
          'transition-colors duration-200',
          'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2',
          'disabled:opacity-50 disabled:cursor-not-allowed',
          // Touch optimization
          'touch-manipulation',
          // Variant & size
          variantStyles[variant],
          sizeStyles[size],
          // Full width
          fullWidth && 'w-full',
          // Icon only adjustment
          isIconOnly && 'aspect-square p-0',
          // Gap for icon + text
          hasChildren && icon && 'gap-2',
          className
        )}
        {...props}
      >
        {/* Loading spinner */}
        {isLoading && (
          <span className="absolute inset-0 flex items-center justify-center">
            <Icon name="loader" size={iconSize} className="animate-spin" />
          </span>
        )}

        {/* Button content */}
        <span
          className={cn(
            'inline-flex items-center justify-center gap-2',
            isLoading && 'opacity-0'
          )}
        >
          {/* Left icon */}
          {icon && iconPosition === 'left' && (
            <Icon name={icon} size={iconSize} />
          )}

          {/* Children */}
          {children}

          {/* Right icon */}
          {icon && iconPosition === 'right' && (
            <Icon name={icon} size={iconSize} />
          )}
        </span>

        {/* Badge */}
        {badge !== undefined && badge > 0 && (
          <span
            className={cn(
              'absolute -top-1 -right-1',
              'min-w-[18px] h-[18px] px-1',
              'flex items-center justify-center',
              'bg-red-500 text-white text-xs font-bold rounded-full'
            )}
          >
            {badge > 99 ? '99+' : badge}
          </span>
        )}
      </button>
    );
  }
);

TouchButton.displayName = 'TouchButton';

export default TouchButton;