You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于cva为React TextInput组件正确定义多状态?

解决class-variance-authority定义TextInput组件状态样式不生效的问题

问题描述

需要创建一个可根据hasError、showIcon、isActive等条件切换样式的TextInput组件,但使用class-variance-authority(cva)定义的状态样式未生效,比如存在错误时边框、图标及文字未变为红色。

当前代码

import HomeIconComponent from "./Icons/HomeIcon";
import {Field, Input, Label} from '@headlessui/react'
import ErrorIcon from "./Icons/ErrorIcon";
import { useState } from "react";
import { cva } from "class-variance-authority";

export interface textInputProps {
  disabled?: boolean | undefined; //if true, the input is disabled
  groupName?: string | undefined; 
  icon?: boolean; //represents the icon path
  label?: string | null; //represents the label name
  modelValue?: string | undefined; //represents the value of the input. To think, if it is necessary
  placeholder?: string | undefined; //displays value before user inputs something
  error?: string | null; //represents the error message
}

const textInputStyles = cva(['k1-h-[3.5rem] k1-w-[18.5rem] k1-p-4 k1-flex k1-flex-row k1-flex-no-wrap k1-justify-start k1-items-center k1-relative k1-border-solid k1-border-2 k1-rounded-l-3xl k1-rounded-tr-2xl'], {
  variants: {
    variant:{
      default: ["k1-color-red"],
      hover: ["k1-border-blue-70"],
      readonly: ["k1-border-yellow-70"],
      active: ["k1-border-sky-70"],
      error: ["k1-border-rose-80"],
    }
  }
});

const TextInput = ({
    disabled,
    groupName,
    icon,
    label,
    modelValue,
    placeholder,    
    error,
}: textInputProps ) => {
  const [text, setText] = useState(modelValue || '');
  const [isActive, setIsActive] = useState(false);
  const [isHovered, setIsHovered] = useState(false);
  const showIcon = icon || false;
  const hasError = error || false;
  const isDisabled = disabled || false;
  //const isFilled = text !== '';

 const state = (() => {
    if (hasError) return 'error';
    if (isDisabled) return 'readonly';
    if (isActive) return 'active';
    if (!isHovered) return 'hover';
    return 'default';    
  })()

  return (
      <div className={textInputStyles({ variant: state })}>
      <Field className="input-field">
        {label && <Label className="input-label k1-pl-1 k1-pr-1 k1-flex k1-flex-row k1-justify-start k1-items-start k1-relative k1-top-[-1.25rem] k1-left-[0.75rem)] k1-uppercase k1-bg-white">{label}</Label>}
         <div className="input-container k1-flex">
         {showIcon && <HomeIconComponent />}
          <Input
            type="text"
            name={groupName}
            className="input"
            disabled={disabled}
            value={text}
            placeholder={placeholder}
            onChange={(e) => {
              setText(e.target.value);
              setIsActive(true);
            }}
            onFocus={() => setIsActive(true)}
            onBlur={() => setIsActive(false)}
            onMouseEnter={() => setIsHovered(true)}
            onMouseLeave={() => setIsHovered(false)}
          />
        </div>
          {error && (
            <div className="k1-pl-1 k1-pr-1 k1-flex k1-flex-row k1-justify-space-between k1-items-start k1-absolute k1-right-[0.75rem] k1-bottom-[-0.75rem] k1-bg-neutral-fg-hover k1-bg-white">
              <div>
                {error}
              </div>
              <ErrorIcon />            
            </div>
          )}
      </Field>
    </div>
  );
};

export {TextInput};

解决方案

1. 重构CVA变体定义

将单一variant拆分为独立状态变体,提升灵活性并明确状态优先级,确保错误状态能覆盖其他样式:

const textInputStyles = cva(
  ['k1-h-[3.5rem] k1-w-[18.5rem] k1-p-4 k1-flex k1-flex-row k1-flex-no-wrap k1-justify-start k1-items-center k1-relative k1-border-solid k1-border-2 k1-rounded-l-3xl k1-rounded-tr-2xl'],
  {
    variants: {
      hasError: {
        true: ['k1-border-rose-80', 'k1-text-rose-80'],
        false: []
      },
      isDisabled: {
        true: ['k1-border-yellow-70', 'k1-opacity-50'],
        false: []
      },
      isActive: {
        true: ['k1-border-sky-70'],
        false: []
      },
      isHovered: {
        true: ['k1-border-blue-70'],
        false: []
      }
    },
    // 强制错误状态覆盖其他边框样式
    compoundVariants: [
      { hasError: true, isDisabled: true, class: ['k1-border-rose-80'] },
      { hasError: true, isActive: true, class: ['k1-border-rose-80'] },
      { hasError: true, isHovered: true, class: ['k1-border-rose-80'] },
    ],
    defaultVariants: {
      hasError: false,
      isDisabled: false,
      isActive: false,
      isHovered: false
    }
  }
);

2. 修正状态判断逻辑

移除原有的state函数,直接传入真实状态参数调用CVA,避免逻辑反转错误:

const containerClasses = textInputStyles({
  hasError,
  isDisabled,
  isActive,
  isHovered
});

3. 扩展样式到内部元素

当前CVA样式仅作用于外层容器,需给内部元素添加条件样式,确保错误状态下图标、文字同步变色:

// 标签样式
<Label className={`input-label k1-pl-1 k1-pr-1 k1-flex k1-flex-row k1-justify-start k1-items-start k1-relative k1-top-[-1.25rem] k1-left-[0.75rem] k1-uppercase k1-bg-white ${hasError ? 'k1-text-rose-80' : ''}`}>
  {label}
</Label>

// 左侧图标样式
{showIcon && <HomeIconComponent className={hasError ? 'k1-text-rose-80' : ''} />}

// 输入框样式
<Input className={`input ${hasError ? 'k1-text-rose-80' : ''}`} />

// 错误提示样式
<div className="k1-pl-1 k1-pr-1 k1-flex k1-flex-row k1-justify-space-between k1-items-start k1-absolute k1-right-[0.75rem] k1-bottom-[-0.75rem] k1-bg-white k1-text-rose-80">
  <div>{error}</div>
  <ErrorIcon className="k1-text-rose-80" />            
</div>

4. 修复事件逻辑冗余

移除onChange中不必要的setIsActive(true),由onFocus统一处理激活状态:

onChange={(e) => setText(e.target.value)}

完整修正后的组件代码

import HomeIconComponent from "./Icons/HomeIcon";
import {Field, Input, Label} from '@headlessui/react'
import ErrorIcon from "./Icons/ErrorIcon";
import { useState } from "react";
import { cva } from "class-variance-authority";

export interface textInputProps {
  disabled?: boolean;
  groupName?: string; 
  icon?: boolean;
  label?: string | null;
  modelValue?: string;
  placeholder?: string;
  error?: string | null;
}

const textInputStyles = cva(
  ['k1-h-[3.5rem] k1-w-[18.5rem] k1-p-4 k1-flex k1-flex-row k1-flex-no-wrap k1-justify-start k1-items-center k1-relative k1-border-solid k1-border-2 k1-rounded-l-3xl k1-rounded-tr-2xl'],
  {
    variants: {
      hasError: {
        true: ['k1-border-rose-80', 'k1-text-rose-80'],
        false: []
      },
      isDisabled: {
        true: ['k1-border-yellow-70', 'k1-opacity-50'],
        false: []
      },
      isActive: {
        true: ['k1-border-sky-70'],
        false: []
      },
      isHovered: {
        true: ['k1-border-blue-70'],
        false: []
      }
    },
    compoundVariants: [
      { hasError: true, isDisabled: true, class: ['k1-border-rose-80'] },
      { hasError: true, isActive: true, class: ['k1-border-rose-80'] },
      { hasError: true, isHovered: true, class: ['k1-border-rose-80'] },
    ],
    defaultVariants: {
      hasError: false,
      isDisabled: false,
      isActive: false,
      isHovered: false
    }
  }
);

const TextInput = ({
    disabled = false,
    groupName,
    icon = false,
    label,
    modelValue = '',
    placeholder,    
    error,
}: textInputProps ) => {
  const [text, setText] = useState(modelValue);
  const [isActive, setIsActive] = useState(false);
  const [isHovered, setIsHovered] = useState(false);
  const showIcon = icon;
  const hasError = !!error;
  const isDisabled = disabled;

  const containerClasses = textInputStyles({
    hasError,
    isDisabled,
    isActive,
    isHovered
  });

  return (
    <div className={containerClasses}>
      <Field className="input-field">
        {label && (
          <Label className={`input-label k1-pl-1 k1-pr-1 k1-flex k1-flex-row k1-justify-start k1-items-start k1-relative k1-top-[-1.25rem] k1-left-[0.75rem] k1-uppercase k1-bg-white ${hasError ? 'k1-text-rose-80' : ''}`}>
            {label}
          </Label>
        )}
        <div className="input-container k1-flex">
          {showIcon && <HomeIconComponent className={hasError ? 'k1-text-rose-80' : ''} />}
          <Input
            type="text"
            name={groupName}
            className={`input ${hasError ? 'k1-text-rose-80' : ''}`}
            disabled={isDisabled}
            value={text}
            placeholder={placeholder}
            onChange={(e) => setText(e.target.value)}
            onFocus={() => setIsActive(true)}
            onBlur={() => setIsActive(false)}
            onMouseEnter={() => setIsHovered(true)}
            onMouseLeave={() => setIsHovered(false)}
          />
        </div>
        {error && (
          <div className="k1-pl-1 k1-pr-1 k1-flex k1-flex-row k1-justify-space-between k1-items-start k1-absolute k1-right-[0.75rem] k1-bottom-[-0.75rem] k1-bg-white k1-text-rose-80">
            <div>{error}</div>
            <ErrorIcon className="k1-text-rose-80" />            
          </div>
        )}
      </Field>
    </div>
  );
};

export {TextInput};

内容的提问来源于stack exchange,提问作者Sergio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 22:22:32