TS+react自建组件库 02
KGD的第二个组件-Alert
需求分析
类型分析-type:成功、默认、危险、警告
功能分析-:默认弹出框、可添加描述弹出框
设计实现
import React, {FC, useState} from 'react'
import classNames from 'classnames'
export enum AlertType {
Success = 'success',
Default = 'default',
Danger = 'danger',
Warning = 'warning',
}
interface BaseAlert {
title ?: string,
description ?: string,
type ?: AlertType,
onClose ?: () => void,
closable ?: boolean
}
type AlertProps = BaseAlert & React.HTMLAttributes<HTMLDivElement>
const Alert : FC<AlertProps> = (props) => {
const {
className,
title,
description,
type,
onClose,
closable
} = props
const classes = classNames('kgd-alert',className, {
[`kgd-alert-${type}`] : type,
'closable' : type === AlertType.Warning ? false : closable,
'zoom-in-top-appear-done' : 'zoom-in-top-appear-done',
'zoom-in-top-enter-done' : 'zoom-in-top-enter-done',
})
const [visible, setVisible] = useState(true)
const closeAlert = (onClose : Function) => {
return () => {
setVisible(false)
onClose()
}
}
return visible ?
(
<div
className={classes}
>
<span>{title}</span>
<span
className = 'kgd-alert-close'
onClick = {closeAlert(onClose as () => void)}
>
×
</span>
<p>{description ? description : null}</p>
</div>
): null
}
Alert.defaultProps = {
type : AlertType.Default,
closable : true,
onClose : () => {}
}
export default Alert;
KGD的组件测试
测试框架选择:JEST
安装指令
yarn add --dev jest
文档
调用指令
npx jest 测试文件名 --watch
React测试工具:React Testing Library
辅助小工具:jest-dom
(tips:脚手架创建项目时,已内置安装)
测试代码
Button
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Button, { ButtonProps, ButtonType, ButtonSize } from './'
const defaultProps = {
onClick: jest.fn()
}
const testProps: ButtonProps = {
btnType: ButtonType.Primary,
size: ButtonSize.Large,
className: 'klass'
}
const disabledProps: ButtonProps = {
disabled: true,
onClick: jest.fn(),
}
describe('test Button component', () => {
it('should render the correct default button', () => {
const wrapper = render(<Button {...defaultProps}>Nice</Button>)
const element = wrapper.getByText('Nice') as HTMLButtonElement
expect(element).toBeInTheDocument()
expect(element.tagName).toEqual('BUTTON')
expect(element).toHaveClass('btn btn-default')
expect(element.disabled).toBeFalsy()
fireEvent.click(element)
expect(defaultProps.onClick).toHaveBeenCalled()
})
it('should render the correct component based on different props', () => {
const wrapper = render(<Button {...testProps}>Nice</Button>)
const element = wrapper.getByText('Nice')
expect(element).toBeInTheDocument()
expect(element).toHaveClass('btn-primary btn-lg klass')
})
it('should render a link when btnType equals link and href is provided', () => {
const wrapper = render(<Button btnType={ButtonType.Link} href="http://dummyurl">Link</Button>)
const element = wrapper.getByText('Link')
expect(element).toBeInTheDocument()
expect(element.tagName).toEqual('A')
expect(element).toHaveClass('btn btn-link')
})
it('should render disabled button when disabled set to true', () => {
const wrapper = render(<Button {...disabledProps}>Nice</Button>)
const element = wrapper.getByText('Nice') as HTMLButtonElement
expect(element).toBeInTheDocument()
expect(element.disabled).toBeTruthy()
fireEvent.click(element)
expect(disabledProps.onClick).not.toHaveBeenCalled()
})
})
Alert
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Alert from './'
describe('test Alert component', () => {
it('should render the correct default Alert', () => {
const wrapper = render(<Alert title = 'Alert-test'/>)
const element = wrapper.getByText('Alert-test').parentNode as HTMLElement
expect(element).toBeInTheDocument()
expect(element.tagName).toEqual('DIV')
expect(element).toHaveClass('kgd-alert kgd-alert-default')
})
it('should render the correct component based on different closeFunction', () => {
const wrapper = render(<Alert title = 'Alert-onClose-test' onClose = {() => {console.log('aaa')}}/>)
const element = wrapper.getByText('Alert-onClose-test').nextElementSibling as HTMLElement
expect(element).toBeInTheDocument()
expect(element).toHaveClass('kgd-alert-close')
fireEvent.click(element)
})
})
KGD的第三个组件-Menu
需求分析
基本样式分析:横向、纵向
功能分析:默认、不可选、下拉菜单
属性分析:选项下标、是否选中(高亮)、用户自定义类名
设计实现
Menu
import React, {FC, createContext, useState} from 'react'
import classNames from 'classnames'
import { MenuItemProps } from './MenuItem'
type MenuMode = 'horizontal' | 'vertical'
type selectCallback = (selectedIndex: string) => void
interface BaseMenu {
mode ?: MenuMode;
defaultIndex ?: string;
onSelect ?: selectCallback;
classNames ?: string;
style ?: React.CSSProperties;
defaultOpenSubMenus ?: string[];
}
interface IMenuContext {
index : string;
onSelect ?: selectCallback;
mode ?: MenuMode;
defaultOpenSubMenus ?: string[];
}
export const MenuContext = createContext<IMenuContext>({index:'0'})
export type MenuProps = BaseMenu & React.HTMLAttributes<HTMLUListElement>
const Menu:FC<MenuProps> = (props) => {
const {
className,
style,
defaultIndex,
mode,
children,
onSelect,
defaultOpenSubMenus,
} = props
const classes = classNames('kgd-menu', className, {
'menu-vertical' : mode === 'vertical',
'menu-horizontal' : mode !== 'vertical'
})
const [currentActive, setActive] = useState(defaultIndex)
const handleClick = (index:string) => {
setActive(index);
if(onSelect) onSelect(index);
}
const passedContext : IMenuContext = {
index : currentActive ? currentActive : '0',
onSelect: handleClick,
mode,
defaultOpenSubMenus,
}
const renderChildren = () => {
return React.Children.map(children,(child, index) => {
const childElement = child as React.FunctionComponentElement<MenuItemProps>
const { displayName } = childElement.type
if(displayName === 'MenuItem' || displayName === 'SubMenu')
return React.cloneElement(childElement, {
index:index.toString()
})
else console.error('Warning: Menu has a child which is not a MenuItem component')
})
}
return(
<ul
className={classes}
style={style}
>
<MenuContext.Provider value = {passedContext}>
{renderChildren()}
</MenuContext.Provider>
</ul>
)
}
Menu.defaultProps = {
defaultIndex : '0',
mode : 'horizontal',
defaultOpenSubMenus : [],
}
export default Menu;
MenuItem
import {FC, useContext} from 'react'
import classNames from 'classnames'
import {MenuContext} from '../'
interface BaseMenuItem {
index ?: string;
disabled ?: boolean;
className?: string;
style ?: React.CSSProperties;
}
export type MenuItemProps = BaseMenuItem & React.LiHTMLAttributes<HTMLLIElement>
const MenuItem:FC<MenuItemProps> = (props) => {
const {
className,
style,
children,
index,
disabled,
} = props
const context = useContext(MenuContext)
const classes = classNames('menu-item', className, {
'is-disabled': disabled,
'is-active' : context.index === index
})
const handleClick = () => {
if(context.onSelect && !disabled && (typeof index === 'string')) {
context.onSelect(index)
}
}
return(
<li
style={style}
className = {classes}
onClick = {handleClick}
>
{children}
</li>
)
}
MenuItem.displayName = 'MenuItem'
MenuItem.defaultProps = {
index : '0',
}
export default MenuItem;
SubMenu
import React,{FC, useContext, useState} from 'react'
import classNames from 'classnames'
import {MenuContext} from '../'
import {MenuItemProps} from '../MenuItem'
interface BaseSubMenu {
index ?: string;
title : string;
className ?: string
}
export type SubMenuProps = BaseSubMenu & React.LiHTMLAttributes<HTMLLIElement>
const SubMenu : FC<SubMenuProps> = (props) => {
const {
title,
index,
className,
children
} = props
const context = useContext(MenuContext)
const classes = classNames('submenu-item menu-item', className, {
'is-active' : context.index === index,
})
const openedSubMenus = context.defaultOpenSubMenus as Array<string>
const isopened = (index && context.mode === 'vertical') ? openedSubMenus.includes(index) :false
const [menuOpen,setOpen] = useState(isopened)
const handleClick = (e:React.MouseEvent) => {
e.preventDefault()
setOpen(!menuOpen)
}
let timer:any
const handleMouse = (e:React.MouseEvent,toggle:boolean) => {
clearTimeout(timer)
e.preventDefault()
timer = setTimeout(() => {
setOpen(toggle)
},200)
}
const clickEvents = context.mode === 'vertical' ? {
onClick : handleClick
} : {}
const hoverEvents = context.mode === 'vertical' ?
{} : {
onMouseEnter:(e:React.MouseEvent) => {handleMouse(e,true)},
onMouseLeave:(e:React.MouseEvent) => {handleMouse(e,false)}
}
const renderChildren = () => {
const classes = classNames('kgd-submenu', {
'menu-opened' : menuOpen
})
const ChildComponet = React.Children.map(children,(child, i) => {
const childElement = child as React.FunctionComponentElement<MenuItemProps>
if(childElement.type.displayName === 'MenuItem') return React.cloneElement(childElement, {
index:`${index}-${i}`
})
else console.error('Warning: Menu has a child which is not a MenuItem component')
})
return(
<ul
className = {classes}
>
{ChildComponet}
</ul>
)
}
return(
<li
className = {classes}
key = {index}
{...hoverEvents}
>
<div
className = 'submenu-title'
{...clickEvents}
>
{title}
</div>
{renderChildren()}
</li>
)
}
SubMenu.displayName = 'SubMenu'
export default SubMenu;
KGD的第四个组件-Tabs
需求分析
基本样式分析:线条形式、卡片形式
功能分析:默认、不可选
属性分析:选项下标、是否选中(高亮)、用户自定义类名、用户自定义选项卡样式
设计实现
Tabs
import React, {FC, useState, createContext} from 'react'
import classNames from 'classnames'
import {TabItemProps} from './TabItem'
type tabsType = 'line' | 'card'
type SelectCallback = (SelectIndex:number) => void
interface BaseTabs {
defaultIndex ?: number;
className ?: string;
onSelect ?: SelectCallback
type ?: tabsType
defaultOpenTabs ?: number[];
}
export type TabsProps = BaseTabs & React.HTMLAttributes<HTMLUListElement>
interface ITabsContext {
type ?: tabsType;
index : number;
onSelect ?: SelectCallback;
defaultOpenTabs ?: number[];
}
export const TabsContext = createContext<ITabsContext>({
index:0,
})
const Tabs : FC<TabsProps> = (props) => {
const {
className,
type,
onSelect,
defaultIndex,
children,
defaultOpenTabs
} = props;
const classes = classNames('kgd-tabs-nav', className, {
'nav-card' : type === 'card',
'nav-line' : type === 'line'
})
const [currentActive, setActive] = useState(defaultIndex)
const [content, setContent] = useState()
const [tabsOpen,setTabsopen] = useState(false)
const handleClick = (index:number) => {
setActive(index)
onSelect && onSelect(index)
}
const passedContext : ITabsContext = {
index : currentActive ? currentActive : 0,
onSelect: handleClick,
type,
defaultOpenTabs,
}
const getContent = (content : any, tabsOpen : boolean) => {
return tabsOpen ? (
<div className = 'kgd-tabs-content'>
<div className = 'kgd-tab-panel'>
{content}
</div>
</div>
): null
}
const ChildrenContent = (content : any, tabsOpen : boolean) => {
setContent(content)
setTabsopen(tabsOpen)
}
const renderChildren = () => {
return React.Children.map(children,(child, index) => {
const childElement = child as React.FunctionComponentElement<TabItemProps>
const { displayName } = childElement.type
if(displayName === 'TabsItem')
return React.cloneElement(childElement, {
index,
ChildrenContent
})
else console.error('Warning: Tabs has a child which is not a TabsItem component')
})
}
return (
<>
<ul
className={classes}
>
<TabsContext.Provider value = {passedContext}>
{renderChildren()}
</TabsContext.Provider>
</ul>
{getContent(content,tabsOpen)}
</>
)
}
Tabs.defaultProps = {
defaultIndex : 0,
defaultOpenTabs : []
}
export default Tabs;
TabItem
import React, {FC, useContext, useState, useEffect} from 'react'
import classNames from 'classnames'
import {TabsContext} from '../'
interface BaseTabsItem {
index ?: number,
label : any,
disabled ?: boolean,
ChildrenContent ?: Function,
}
export type TabItemProps = BaseTabsItem & React.LiHTMLAttributes<HTMLLIElement>
const TabItem : FC<TabItemProps> = (props) => {
const {index, label, disabled, className, children, ChildrenContent} = props
const context = useContext(TabsContext)
const openedTabs = context.defaultOpenTabs as Array<number>
const isopened = openedTabs.includes(index as number)
const [tabsOpen,setOpen] = useState(isopened)
useEffect(() =>{
index === context.index && setOpen(true)
index === context.index && ChildrenContent && ChildrenContent(children, tabsOpen)
},[index,context.index,ChildrenContent,children,tabsOpen])
const classes = classNames('kgd-tabs-nav-item', className, {
'disabled': disabled,
'is-active' : context.index === index
})
const handleClick = () => {
if(context.onSelect && !disabled && (typeof index === 'number')) {
context.onSelect(index)
setOpen(!tabsOpen)
ChildrenContent && ChildrenContent(children, tabsOpen)
}
}
return (
<li
className = {classes}
onClick = {handleClick}
key = {index}
>
{label}
</li>
)
}
TabItem.displayName = 'TabsItem'
TabItem.defaultProps = {
index : 0,
}
export default TabItem;
|