组件效果
- 支持设置
lg、sm尺寸大小 - 支持设置
前后缀的文字、图标 - 支持设置在右侧悬浮添加一个
icon
组件结构
src/components/Input/input.tsx
input-size-${size}类控制input组件的padding来达到尺寸的控制- 当有传入append或者prepend,
input-group类控制w-input-group-append左边上下的圆角为0、w-input-group-prepend的右边上下的圆角为0 - 当有传入append,
input-group-append类控制w-input-inner的右边上下的圆角为0 - 当有传入prepend,
input-group-prepend控制w-input-inner的左边上下的圆角为0 - 当传入
value就删除defaultValue防止报错
import React, {
FC,
ReactElement,
InputHTMLAttributes,
ChangeEvent,
} from 'react'
import classNames from 'classnames'
import { IconProp } from '@fortawesome/fontawesome-svg-core'
import { Icon } from '../Icon'
export type InputSize = 'lg' | 'sm'
// Omit 忽略接口中的size值
export interface InputProps
extends Omit<InputHTMLAttributes<HTMLElement>, 'size'> {
/**是否禁用 Input*/
disabled?: boolean
/**设置 input 大小,支持 lg 或者是 sm */
size?: InputSize
/**添加图标,在右侧悬浮添加一个图标,用于提示 */
icon?: IconProp
/**添加前缀 用于配置一些固定组合 */
prepend?: string | ReactElement
/**添加后缀 用于配置一些固定组合 */
append?: string | ReactElement
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
}
export const Input: FC<InputProps> = (props) => {
// 取出所有的属性
const { disabled, size, icon, prepend, append, style, ...restProps } = props
// 根据不同的属性计算className
const classes = classNames('w-input-wrapper', {
[`input-size-${size}`]: size,
'is-disabled': disabled,
'input-group': prepend || append,
'input-group-append': !!append,
'input-group-prepend': !!prepend,
})
const fixControlledValue = (value: any) => {
if (typeof value === 'undefined' || value === null) {
return ''
}
return value
}
if ('value' in props) {
delete restProps.defaultValue
restProps.value = fixControlledValue(props.value)
}
return (
// 根据属性判断是否要添加不同的节点
<div className={classes} style={style}>
{prepend && <div className="w-input-group-prepend">{prepend}</div>}
{icon && (
<div className="icon-wrapper">
<Icon icon={icon} title={`title-${icon}`} />
</div>
)}
<input className="w-input-inner" disabled={disabled} {...restProps} />
{append && <div className="w-input-group-append">{append}</div>}
</div>
)
}
Input.defaultProps = {
disabled: false,
}
export default Input
组件样式
src/components/Input/_style.scss
- 最外层的盒子flex布局,并设置
relative,使icon图标右悬停 - 输入框聚焦时设置边框颜色、边框阴影,具有蓝色效果样式
- 在禁用和只读状态下,禁止鼠标事件的触发,透明度为完全不透明
.w-input-wrapper {
display: flex;
width: 100%;
margin-bottom: 30px;
position: relative;
.icon-wrapper {
position: absolute;
height: 100%;
width: 35px;
justify-content: center;
color: $input-color;
right: 0;
top: 0;
display: flex;
align-items: center;
svg {
color: $input-placeholder-color;
}
}
}
.icon-wrapper + .w-input-inner {
padding-right: 35px;
}
.w-input-inner {
width: 100%;
padding: $input-padding-y $input-padding-x;
font-family: $input-font-family;
font-size: $input-font-size;
font-weight: $input-font-weight;
line-height: $input-line-height;
color: $input-color;
background-color: $input-bg;
background-clip: padding-box;
border: $input-border-width solid $input-border-color;
border-radius: $input-border-radius;
box-shadow: $input-box-shadow;
transition: $input-transition;
&:focus {
color: $input-focus-color;
background-color: $input-focus-bg;
border-color: $input-focus-border-color;
outline: 0;
box-shadow: $input-focus-box-shadow;
}
&::placeholder {
color: $input-placeholder-color;
opacity: 1;
}
&:disabled,
&[readonly] {
background-color: $input-disabled-bg;
border-color: $input-disabled-border-color;
pointer-events: none;
opacity: 1;
}
}
.w-input-group-prepend,
.w-input-group-append {
display: flex;
align-items: center;
padding: $input-padding-y $input-padding-x;
margin-bottom: 0;
font-size: $input-font-size;
font-weight: $font-weight-normal;
line-height: $input-line-height;
color: $input-group-addon-color;
text-align: center;
white-space: nowrap;
background-color: $input-group-addon-bg;
border: $input-border-width solid $input-group-addon-border-color;
border-radius: $input-border-radius;
}
.w-input-group-append + .btn {
padding: 0;
border: 0;
}
.input-group > .w-input-group-prepend,
.input-group.input-group-append > .w-input-inner {
@include border-right-radius(0);
}
.input-group > .w-input-group-append,
.input-group.input-group-prepend > .w-input-inner {
@include border-left-radius(0);
}
.input-size-sm .w-input-inner {
padding: $input-padding-y-sm $input-padding-x-sm;
font-size: $input-font-size-sm;
border-radius: $input-border-radius-sm;
}
.input-size-lg .w-input-inner {
padding: $input-padding-y-lg $input-padding-x-lg;
font-size: $input-font-size-lg;
border-radius: $input-border-radius-lg;
}
组件测试
src/components/Input/input.test.tsx
import { fireEvent, render, screen } from '@testing-library/react'
import Input, { InputProps } from './input'
const defaultProps: InputProps = {
onChange: jest.fn(),
placeholder: 'test-input',
}
describe('test Input component', () => {
// 测试应该渲染正确的默认输入
it('should render the correct default Input', () => {
// 使用默认属性渲染Input组件
render(<Input {...defaultProps} />)
// 获取testNode节点,类型为HTMLInputElement
const testNode = screen.getByPlaceholderText(
'test-input'
) as HTMLInputElement
// 断言testNode节点在文档中
expect(testNode).toBeInTheDocument()
// 断言testNode节点具有w-input-inner类
expect(testNode).toHaveClass('w-input-inner')
// 触发testNode节点的change事件,传入一个对象,对象中包含target属性,值为23
fireEvent.change(testNode, { target: { value: '23' } })
// 断言defaultProps.onChange函数被调用
expect(defaultProps.onChange).toHaveBeenCalled()
// 断言testNode节点的值等于23
expect(testNode.value).toEqual('23')
})
// 测试当disabled属性为true时,渲染的Input应该被禁用
it('should render the disabled Input on disabled property', () => {
// 渲染一个Input组件,disabled属性为true,placeholder属性为disabled
render(<Input disabled placeholder="disabled" />)
// 通过placeholder属性获取到渲染的Input节点
const testNode = screen.getByPlaceholderText('disabled') as HTMLInputElement
// 预期testNode的disabled属性为true
expect(testNode.disabled).toBeTruthy()
})
// 测试当size属性不同时,渲染不同的输入大小
it('should render different input sizes on size property', () => {
// 渲染一个Input组件,并设置placeholder和size属性
render(<Input placeholder="sizes" size="lg" />)
// 获取test-input的容器
const testContainer = screen.getByTestId('test-input')
// 预期testContainer容器应该包含input-size-lg类
expect(testContainer).toHaveClass('input-size-lg')
})
// 测试prepend和append元素在prepend/append属性中渲染
it('should render prepend and append element on prepend/append property', () => {
// 渲染一个Input组件,其中包含placeholder、prepend和append属性
render(<Input placeholder="pend" prepend="https://" append=".com" />)
// 获取test-input元素
const testContainer = screen.getByTestId('test-input')
// 断言testContainer元素具有input-group、input-group-append和input-group-prepend类
expect(testContainer).toHaveClass(
'input-group input-group-append input-group-prepend'
)
// 断言文档中存在https://元素
expect(screen.getByText('https://')).toBeInTheDocument()
// 断言文档中存在.com元素
expect(screen.getByText('.com')).toBeInTheDocument()
})
})