W-Design组件库:Input 组件

118 阅读2分钟

组件效果

image.png

  1. 支持设置lg、sm 尺寸大小
  2. 支持设置前后缀的文字、图标
  3. 支持设置在右侧悬浮添加一个icon

组件结构

src/components/Input/input.tsx

  1. input-size-${size}类控制input组件的padding来达到尺寸的控制
  2. 当有传入append或者prepend,input-group类控制w-input-group-append左边上下的圆角为0、w-input-group-prepend的右边上下的圆角为0
  3. 当有传入append,input-group-append类控制w-input-inner的右边上下的圆角为0
  4. 当有传入prepend,input-group-prepend控制w-input-inner的左边上下的圆角为0
  5. 当传入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

  1. 最外层的盒子flex布局,并设置relative,使icon图标右悬停
  2. 输入框聚焦时设置边框颜色、边框阴影,具有蓝色效果样式
  3. 在禁用和只读状态下,禁止鼠标事件的触发,透明度为完全不透明
.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()
  })
})