【UI组件库】icon组件和transition组件

目录

  • icon组件
    • 解决图标Icon的问题
    • fort awesome使用我们的属性
    • 添加样式
    • 动画效果:使用CSS实现
    • 动画效果:使用ReactTransitionGroup实现
  • transition组件

icon组件

解决图标Icon的问题

前面我们封装的组件都没图标,这一节我们要解决图标问题。

  1. iconfont有很多奇怪的bug,比如,如果没加载好会有出现方框
  2. svg完全可控,即取即用
  3. 本文介绍fort awesome,他底层基于SVG实现的图标库
  4. 安装fort awesome:
npm i --save @fortawesome/fortawesome-svg-core// 安装fortawesome核心库
npm i --save @fortawesome/free-solid-svg-icons// 你要安装什么样的icon,其余icon可以去官网查看
npm i --save @fortawesome/react-fortawesome// 安装react-fortawesome组件
  1. 使用:
    方法一:
    (1)导入:import {FortAwesomeIcon} from @fortawesome/react-fortawesome
    (2)导入你具体想用的图标:import {faCoffee} from @fortawesome/free-solid-svg-icons
    (3)使用图标:
// FortAwesomeIcon标签还有很多属性,如size设置尺寸,rotation旋转图标以及一些animation的属性(spin、pulse等)
const element = <FortAwesomeIcon icon={faCoffee} size='lg'/>
ReactDom.render(element, document.body)

按上面的使用方法,我们每用一个图标就需要引入一下,太麻烦了,我们可以引入fortawesome核心库中的library:
(1)引入:import {library} from '@fortawesome/fortawesome-svg-core'
(2)引入你具体想用的图标:import {faCheckSquare, faCoffee} from '@fortawesome/free-solid-svg-icons',也可以引入所有图标:import {fas} from '@fortawesome/free-solid-svg-icons'
(3)将你要用的图标添加进library:library.add(faCheckSquare, faCoffee)library.add(fas)
(4)使用时给icon属性赋值:

fort awesome使用我们的属性

将我们组件的主题颜色添加进fort awesome的theme属性中,fort awesome的属性不仅仅局限于它自身的,也可以使用我们的属性

import React from 'react'
import classNames from 'classnames'
import {FortAwesomeIcon, FortAwesomeIconProps} from @fortawesome/react-fortawesome

export type ThemeProps = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'light' | 'dark'
export interface IconProps extends FortAwesomeIconProps{
	theme?: ThemeProps
}
const Icon: React.FC<IconProps> = (props) => {
	const {className, theme, ...restProps} = props;
	const classes = classNames('viking-icon', className, {
		[`icon-${theme}`]: theme
	})
	return (
		<FontAwesomeIcon className={classes} {...restProps} />
	)
}
export default Icon

在app.tsx中使用:

import Icon from ./components/Icon/icon
import {library} from '@fortawesome/fortawesome-svg-core'
<Icon icon='coffee' theme='danger' size=10px' />

得到的效果如下:
【UI组件库】icon组件和transition组件_第1张图片

添加样式

新建src/components/Icon/_style.scss,icon用的样式的变量是我们在src/styles/_variables.scss中定义的,但是我们在src/styles/_variables.scss中定义了很多变量,一个个去找,再一个个去用很麻烦,所以我们使用SCSS的each来遍历map,我们先在src/styles/_variables.scss中定义一个map,里面保存着我们要用的变量:

$theme-colors: 
(
  "primary":    $primary,
  "secondary":  $secondary,
  "success":    $success,
  "info":       $info,
  "warning":    $warning,
  "danger":     $danger,
  "light":      $light,
  "dark":       $dark
);

在src/components/Icon/_style.scss中使用each来遍历上述map

@each $key, $val in $theme-colors {
  // 在选择器中要使用变量名,需要用#{变量名}包起来
  .icon-#{$key} {
    color: $val;
  }
}

在app.tsx中使用:


得到的效果:
【UI组件库】icon组件和transition组件_第2张图片

动画效果:使用CSS实现

我们先将刚才讲的icon运用到上一节讲的subMenu中,subMenu.tsx中代码如下:

  return (
    <li key={index} className={classes} {...hoverEvents}>
      <div className="submenu-title" {...clickEvents}>
        {title}
        <Icon icon="angle-down" className="arrow-icon"/>
      </div>
      {renderChildren()}
    </li>
  )

src/components/Menu/_style.scss中添加以下代码:

  .submenu-item {
    position: relative;
    .submenu-title {
      display: flex;
      align-items: center;// 垂直
    }
    .arrow-icon {
    	//transition:提供了在更改CSS属性时,控制动画速度的方法,让属性变化持续一段时间,而不是立马生效
      	//transition运用到.arrow-icon选择器上,当.arrow-icon的transform属性发生变化时,变化持续0.25s,且变化速度为ease-in-out
      transition: transform .25s ease-in-out;
      margin-left: 3px;
    }
    &:hover {
      .arrow-icon {
        transform: rotate(180deg);
      }
    }
  }

在横向菜单栏中,当鼠标悬停在子菜单上,title旁边的图标会变化
【UI组件库】icon组件和transition组件_第3张图片
【UI组件库】icon组件和transition组件_第4张图片
但是在纵向菜单栏中,只有点击时,才会打开/关闭title旁边的图标,在hover时不需要做任何处理,所以我们需要给subMenu添加两个特殊的类:

const classes = classNames('menu-item submenu-item', className, {
	'is-active': context.index === index,
    'is-opened': menuOpen,// subMenu是否打开
    'is-vertical': context.mode === 'vertical'// 是不是纵向菜单栏
})

src/components/Menu/_style.scss中为以上两个类添加以下代码:

  //如果有is-vertical这个类名,说明是垂直菜单栏,就让他不旋转,且优先级最高
  .is-vertical {
    .arrow-icon {
      transform: rotate(0deg) !important;
    }
  }
  //如果有is-vertical、is-opened两个类名,说明是垂直菜单栏且subMenu是打开的,那么此时让他旋转180°
  .is-vertical.is-opened {
    .arrow-icon {
      transform: rotate(180deg) !important;
    }
  }

动画效果:使用ReactTransitionGroup实现

我们想给下拉菜单实现渐行渐远的动画效果,如果用CSS来实现:

  .viking-submenu {
  	opacity: 0;
  	// 名为viking-submenu的类的opacity属性发生变化时,变化持续0.5s,变化速度为ease-in
  	transition: opacity .5s ease-in;
    display: none;
    list-style:none;
    padding-left: 0;
    white-space: nowrap;
    transition: $menu-transition;
    .menu-item {
      padding: $menu-item-padding-y $menu-item-padding-x;
      cursor: pointer;
      transition: $menu-transition;
      color: $body-color;
      &.is-active, &:hover {
        color: $menu-item-active-color !important;
      }
    }
  }
  .viking-submenu.menu-opened {
    display: block;
    opacity: 1;
  }

但是我们发现效果并未生效,这是因为display从none转化为block时,其他所有的动画效果都会失效,因为display不是一个标准的支持animation的属性,所以transition根本不起作用,并且display: block;opacity: 1;是同时生效的,自然缺少opacity: 1;的变化,我们可以把display:none;去掉,就能够成功实现。但这并不是一个好的解决方案,那我们可以使用延时方案,让display和opacity不同时变换,不同时生效,过程图如下所示,第一排是出现的流程图,第二排是隐藏时的流程图
【UI组件库】icon组件和transition组件_第5张图片
针对react动画实现的库:ReactTransitionGroup
ReactTransitionGroup其实是在你组件从无到有或从有到无这个过程中,给组件添加多个描述组件生命周期的class名称,这些名称由时间顺序排列,ReactTransitionGroup中有三个组件:CSSTransition、SwitchTransition、TransitionGroup
ReactTransitionGroup本身并没有实现动画效果,他只是使用不同的class来区分组件进入和离开DOM的各个阶段,我们可以自行添加样式来实现最终的动画效果
【UI组件库】icon组件和transition组件_第6张图片
组件出现时:在*-enter时添加动画开始的效果,在*-enter-active时添加动画结束的效果,如果你添加了自定义的timeout,会有一个*-enter-done的类。组件消失时流程同上。
安装ReactTransitionGroup:

npm install react-transition-group --save
npm install @types/react-transition-group --save

在subMenu.tsx中引入:import {CSSTransition} from 'react-transition-group'
使用:下拉菜单栏renderChildren中返回的结果:

in:从无到有这个过程会自动给你添加这个类名
timeout:从active到done这个时间段持续的时间,单位为ms
classNames:我们要自定义的名称
appear:假如menuOpen第一次是true,第一次执行也会运行整个动画过程在垂直菜单栏中,子菜单可能一开始是打开的

return (
	<CSSTransition
		in={menuOpen}
		timeout={300}
		classNames='zoom-in-top'
		appear
	>
		<ul className={subMenuClasses}>
          {childrenComponent}
        </ul>
	</CSSTransition>
)

animation.css中集合了各种各样的动画
新建src/styles/_animation.scss,其中代码为:

.zoom-in-top-enter{
	opacity:0;
	transform:scaleY(0);
}
.zoom-in-top-active{
	opacity:1;
	transform:scaleY(1);
	transition: transform 300ms cubic-bezier(0.23,1,0.23,1) 100ms,opacity 300ms cubic-bezier(0.23,1,0.23,1) 100ms;
	transform-origin: center top;// 改变元素变形的原点 让动画开始的原点在中心靠上
}
.zoom-in-top-exit{
	opacity:1;
}
.zoom-in-top-exit-active{
	opacity:0;
	transform:scaleY(0);
	transition: transform 300ms cubic-bezier(0.23,1,0.23,1) 100ms,opacity 300ms cubic-bezier(0.23,1,0.23,1) 100ms;
	transform-origin: center top;
}

在src/styles/index.scss中导入src/styles/_animation.scss
但是我们只有在下拉菜单出现时,才会出现我们刚才添加的动画效果,下拉菜单消失时不会出现。
一开始我们组件是.viking-submenu,当我们把鼠标hover上去,下拉菜单出现时,触发mouseEnter事件,改变state,添加.menu-opened,但由于CSSTransition的出现,会自动添加.zoom-in-top-enter这个类,然后触发reflow,添加.zoom-in-top-enter-active,由于我们自定义了timeout,所以接下来会添加.zoom-in-top-enter-done
【UI组件库】icon组件和transition组件_第7张图片
下拉菜单消失:一开始我们组件是.viking-submenu和.menu-opened,当我们把鼠标移开,state发生改变,.menu-opened被移除,.zoom-in-top-exit被添加进来,因为.menu-opened被移除,所以有display:none;,因为.zoom-in-top-exit被添加,所以有opacity:1;transform:scaleY(1);,此时元素已经消失了,在后面添加的任何动画效果都没有意义了,所以后面的动画效果都消失了。第一种方法是将display:none;移到.zoom-in-top-exit-done中。第二种方法是脱离.menu-opened来控制子菜单栏的打开和关闭,我们应该从ReactTransitionGroup包裹的子节点来做文章,如果我们能让它里面包裹的节点一开始是不存在的,我们点击后,in属性值menuOpen切换到true时,节点才被动态添加进去,当鼠标移除时,当in属性值menuOpen切换到false时,我们删除节点,就完全不用display:none;display:block;来控制。针对这一需求,ReactTransitionGroup给我们提供了unmountOnExit,unmountOnExit默认为false,当他为true时,被ReactTransitionGroup包裹的组件到达.zoom-in-top-exit-done时,组件会被卸载。当unmountOnExit为false,里面的组件不存在,in从false转到true时,组件会被动态添加到DOM节点中去。
【UI组件库】icon组件和transition组件_第8张图片
据上述,我们先在src/components/Menu/_style.scss中:.viking-submenu删除display:none;,.viking-submenu.menu-opened删除display:block;
然后使用unmountOnExit:

return (
	<CSSTransition
		in={menuOpen}
		timeout={300}
		classNames='zoom-in-top'
		appear
		unmountOnExit
	>
		<ul className={subMenuClasses}>
          {childrenComponent}
        </ul>
	</CSSTransition>
)

transition组件

上面的动画效果具有可复用性,这一节我们将上述动画封装成transition组件,希望transition组件省略一些参数,transition组件用以提供组件从无到有的动画效果,所以有些参数不用指定,只用加默认值,transition组件也会提供好几种动画效果,以字符串字面量的形式传入,但依然支持CSSTransition的参数。
【UI组件库】icon组件和transition组件_第9张图片
我们在transition组件中设置了上下左右四个方向,我们将动画的css样式抽到一个mixin中,src/styles/_mixin.scss中新增代码如下:

@mixin zoom-animation(
  $direction: 'top',
  $scaleStart: scaleY(0),
  $scaleEnd: scaleY(1),
  $origin: center top,
) {
  .zoom-in-#{$direction}-enter {
    opacity: 0;
    transform: $scaleStart;
  }
  .zoom-in-#{$direction}-enter-active {
    opacity: 1;
    transform: $scaleEnd;
    transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1) 100ms, opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) 100ms;
    transform-origin: $origin
  }
  .zoom-in-#{$direction}-exit {
    opacity: 1;
  }
  .zoom-in-#{$direction}-exit-active {
    opacity: 0;
    transform: $scaleStart;
    transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1) 100ms, opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) 100ms;
    transform-origin: $origin;
  }
}

src/styles/_animation.scss中代码改为:

@include zoom-animation('top',scaleY(0),scaleY(1),center top);
@include zoom-animation('left',scale(.45,.45),scale(1,1),center left);
@include zoom-animation('right',scale(.45,.45),scale(1,1),center right);
@include zoom-animation('bottom',scaleY(0),scaleY(1),center bottom);

这样transition组件的样式就设置好了,新建src/components/Transition/transition.tsx,开始封装transition组件

import React from 'react'
import {CSSTransition} from 'react-transition-group'
import {CSSTransitionProps} from 'react-transition-group/CSSTransition'

type AnimationName = 'zoom-in-top' | 'zoom-in-left' | 'zoom-in-right' | 'zoom-in-bottom'

interface TransitionProps extends CSSTransitionProps {
	animation?: AnimationName
}

const Transition:React.FC<TransitionProps> = (props) => {
	const {classNames, children, animation, ...restProps} = props
	return (
		<CSSTransition
			classNames={classNames ? classNames : animation}
			{...restProps}
		>
		{children}
	</CSSTransition>
	)
}
Transition.defaultProps = {
	unmountOnExit: true,
	appear: true
}
export default Transition

在subMenu.tsx中导入transition组件:import Transition from '../Transition/transition',下拉菜单栏renderChildren中返回的结果改为:

return (
	<Transition
    	in={menuOpen}
        timeout={300}
        animation="zoom-in-top"
    >
    	<ul className={subMenuClasses}>
          	{childrenComponent}
        </ul>
	</Transition>
)

但是如果下拉菜单栏中有我们自定义的button组件,button组件是没有动画的,这是因为我们给button组件也设置了transition属性,button自身的transition属性覆盖了动画的transition,为了解决这个问题,我们可以给transition组件新增一个wrapper属性,在展示transition组件时,如果有wrapper,则在children外层包裹一个div标签,若没有则直接展示children,因为transition属性不会继承,所以父子的transition属性没有任何关系。src/components/Transition/transition.tsx中代码如下:

import React from 'react'
import { CSSTransition } from 'react-transition-group'
import { CSSTransitionProps } from 'react-transition-group/CSSTransition'

type AnimationName = 'zoom-in-top' | 'zoom-in-left' | 'zoom-in-bottom' | 'zoom-in-right'

interface TransitionProps extends CSSTransitionProps {
  animation?: AnimationName,
  wrapper? : boolean,
}

const Transition: React.FC<TransitionProps> = (props) => {
  const {
    children,
    classNames,
    animation,
    wrapper,
    ...restProps
  } = props
  return (
    <CSSTransition
      classNames = { classNames ? classNames : animation}
      {...restProps}
    >
      {wrapper ? <div>{children}</div> : children}
    </CSSTransition>
  )
}
Transition.defaultProps = {
  unmountOnExit: true,
  appear: true,
}

export default Transition

你可能感兴趣的:(自定义UI组件库,ui,javascript,react.js)