如何基于cva为React 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
相关产品推荐
相关产品推荐

