All files / design-system/components Input.tsx

100% Statements 8/8
100% Branches 22/22
100% Functions 1/1
100% Lines 8/8

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      12x                                                                                                           12x   43x     43x   43x 35x     8x                                                                           12x  
import { forwardRef } from 'react';
import { Input as TamaguiInput, styled, GetProps, XStack, YStack, Text } from 'tamagui';
 
const StyledInput = styled(TamaguiInput, {
  name: 'Input',
  borderWidth: 1,
  borderColor: '$borderColor',
  borderRadius: '$4',
  paddingHorizontal: '$3',
  backgroundColor: '$background',
  color: '$foreground',
  placeholderTextColor: '$placeholderColor',
  fontSize: '$5',
 
  focusStyle: {
    borderColor: '$borderColorFocus',
    outlineWidth: 0,
  },
 
  hoverStyle: {
    borderColor: '$borderColorHover',
  },
 
  variants: {
    size: {
      sm: { height: 46, fontSize: '$4' },
      md: { height: 54, fontSize: '$5' },
      lg: { height: 62, fontSize: '$6' },
    },
    error: {
      true: {
        borderColor: '$red',
        focusStyle: { borderColor: '$red' },
      },
    },
    disabled: {
      true: {
        opacity: 0.5,
        backgroundColor: '$color2',
      },
    },
  } as const,
 
  defaultVariants: {
    size: 'md',
  },
});
 
type StyledInputProps = GetProps<typeof StyledInput>;
 
interface InputProps extends StyledInputProps {
  label?: string;
  errorMessage?: string;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
}
 
export const Input = forwardRef<React.ElementRef<typeof StyledInput>, InputProps>(
  ({ label, errorMessage, leftIcon, rightIcon, error, testID, ...props }, ref) => {
    const hasError = !!errorMessage || error;
 
    // 同时设置 testID 和 accessibilityLabel 以支持 Detox E2E 测试
    const a11yProps = testID ? { testID, accessibilityLabel: testID, nativeID: testID } : {};
 
    if (!label && !errorMessage && !leftIcon && !rightIcon) {
      return <StyledInput ref={ref} error={hasError} {...a11yProps} {...props} />;
    }
 
    return (
      <YStack gap="$1.5">
        {label && (
          <Text fontSize="$4" fontWeight="500" color="$foregroundMuted">
            {label}
          </Text>
        )}
        <XStack alignItems="center" position="relative">
          {leftIcon && (
            <YStack position="absolute" left="$3" zIndex={1}>
              {leftIcon}
            </YStack>
          )}
          <StyledInput
            ref={ref}
            error={hasError}
            paddingLeft={leftIcon ? '$9' : '$3'}
            paddingRight={rightIcon ? '$9' : '$3'}
            flex={1}
            {...a11yProps}
            {...props}
          />
          {rightIcon && (
            <YStack position="absolute" right="$3" zIndex={1}>
              {rightIcon}
            </YStack>
          )}
        </XStack>
        {errorMessage && (
          <Text fontSize="$3" color="$red">
            {errorMessage}
          </Text>
        )}
      </YStack>
    );
  }
);
 
Input.displayName = 'Input';