【Day26】反馈元件 - Progress bar

元件介绍

Progress bar 是能够展示当前进度的进度条元件。当一个操作需要显示目前百分比,或是需要较长时间等待运行的时候,能够使用这样的元件提示用户目前进度,藉此来缓解用户等待的焦虑感,或者提供使用者完成复杂任务的成就感。

以下举例可能会使用到进度条的情境:

  • 上传、下载档案的进度
  • 线上课程网站的课程完成进度
  • 募资网站的募资进度
  • 填写复杂表单的时候显示已完成、剩余完成的进度

参考设计 & 属性分析

其实 Progress bar 某种程度上我觉得跟先前提到的 Slider 元件在结构上有点像,会需要一个轨道 rail,在轨道上面有目前的进度 track 条,再厉害一点就是可以在进度条的旁边加上进度数字。

下图是 Antd 的 Progress bar 结构,看起来结构上也是符合我们的直觉,也蛮单纯的,ant-progress-inner 就是 rail 元件,ant-progress-bg 就是 track 元件,彼此的关系是 parent and children;另外我们也可以看到 ant-progress-text 是我们的进度百分比,然後主要的 bar 结构跟文字结构是同一层,外面再用一层 div 包起来来帮助他们做排版。

再来我们来看 MUI 的 Progress bar,看起来结构上应该是跟 Antd Progress bar 一模一样

接下来我们来看一些大家都会设计的 props

进度

Antd 的进度数值 props 叫做 percent,而 MUI 则叫做 value,其实我个人觉得 value 会有点看不出来到底范围值在哪里,只是文件上面有说明是 0~100。

在 MUI Progress bar 的 value 里面我们故意塞了一个超出 100 的数字,他没有禁止你这麽做,但是果然是跑出一个非预期的样子。

在 Antd Progress bar 里面我们一样塞了 120 这个数字,我们可以看到在 ant-progress-bg 的地方是 width: 100%,所以如果超出 100 的话,应该就是自动帮你修正成 100。这样的设计我觉得会比较符合我对这个元件的预期,只不过我没有很喜欢他自作主张的帮我变色,还帮我把数字变成绿色勾勾。

颜色

MUI 一样是统一他的风格,props 用 color,内容一样是只支援 primary, secondary 保留字,要客制化的话需要透过他的 JSS。

Antd 就直接分成进度条颜色(strokeColor)以及未完成的分段颜色(trailColor),以套件使用者的叫度来看,我看到这两个 props 命名其实是有点怪怪的。

但我的猜想是这样的,Antd Progress bar 支援 Linear Progress 以及 Circular Progress,Linear Progress 大部分是由 Html div 结构所组成,而 Circular Progress 其实就是一个 SVG,而 strokeColor 应该是取自 SVG 的参数命名。但是 Linear Progress 明明就不是 SVG 结构,但却用了 SVG 的参数命名,我个人是觉得有点不太搭。

但以目前为止我们设计的元件,为了一致性,track 的颜色应该会统一叫做 themeColor,传入颜色一样支援 primary, secondary 关键字以及色票,另外 railColor 其实比较少会有需要去改变它,所以该脆不设计 props 我觉得应该也没关系,但若要设计的话,会希望与 rail 元件的名称一致,所以应该会叫做 railColor。

是否显示进度数值或状态图标

如果在专案上面很常用需要显示进度数值的话,我觉得加入像 Antd 这个 showInfo 的 props 设计也是很方便

但也有可能有些专案不是很需要显示这个数值,或者说数值是显示在 bar 上面,如下图所示,那我觉得也是按照自己的需要做调整就可

介面设计

属性 说明 类型 默认值
className 客制化样式 string
value 进度 number 0
themeColor 主题配色,primary、secondary 或是自己传入色票 primary, secondary, 色票 primary
showInfo 是否显示进度数值 boolean true
isStatusActive 是否显示等待进度动画 boolean false

元件实作

我们希望用下面这样一个简单的形式就能够得到一个进度条:

<ProgressBar value={50} />

其实我们进度条跟 Slider 元件有点类似,不妨可以一起参考。

因为一个进度条主要会需要 trackrail 两个元素,所以以下是主要的结构:

<Trail className="progress-bar__trail">
  <Track
    className="progress-bar__track"
    $color={color}
    $value={value}
    $isStatusActive={isStatusActive}
  />
</Trail>

那我们这次也加入了如同进度数值的资料,所以在同一层加入 Info ,并且透过父层做排版,如下:

const StyledProgressBar = styled.div`
  display: flex;
  align-items: center;
`;


<StyledProgressBar className={className}>
  <Trail className="progress-bar__trail">
    <Track
      className="progress-bar__track"
      $color={color}
      $value={value}
      $isStatusActive={isStatusActive}
    />
  </Trail>
  {showInfo && (
    <Info className="progress-bar__info">
      {`${value}%`}
    </Info>
  )}
</StyledProgressBar>

那我们的进度条元件的主干就完成了!

数值限制

我们希望我们的数值可以显示超过 100%,然後最小不能低於 0%。
不过虽然数值能够超过 100%,但是 track 的长度总不能无限超出 trail 长度,否则会破版,因此我们还是将 track 的长度限制在 0~100 之间,所以在 track 的 value 我们需要做一些限制:

const formatValue = (value) => {
  if (value > 100) {
    return 100;
  }
  if (value < 0) {
    return 0;
  }
  return value;
};


<Track
  {...略}
  $value={formatValue(value)}
/>

效果如下图,如果数值超过 100%,那我们的 bar 还是显示 100%。

客制化颜色

跟之前的招数如初一彻,我们用准备好的 useColor 来处理我们的颜色:

const { makeColor } = useColor();
const color = makeColor({ themeColor });

跟之前的元件一样,我们允许让他随意可以变色:

渐层 track 颜色

我们其实也常看见一些进度条会有渐层的变化,例如从左到右越来越深色,表示进度即将要完成,用前面说的 themeColor 做不到怎麽办?

没关系,我们在 <Track /> 上面有留下 className,因此可以帮助我们随意调整样式:

<Track
  className="progress-bar__track"
  {...略}
/>

所以我们就能够这样做:

import ProgressBar from '../components/ProgressBar';

const GradientProgressBar = styled(ProgressBar)`
  .progress-bar__track {
    background: linear-gradient(45deg, #FF8E53 30%, #FE6B8B 90%);
  }
`;

<GradientProgressBar {...args} />

达到的效果如下,让你炫炮得不要不要的:

isStatusActive,光晕移动动画

等待进度的时间总是特别漫长,所以总是需要做一些奇怪的动画让用户觉得你的系统真的有在忙着帮他处理事情,那我们何不把上一篇 Skeleton 的光晕移动动画抄过来用呢?

所以我们可以在 <Track /> 加上一个 Boolean props,这边我绞尽我残破的英文来取一个叫做 isStatusActive 的参数,藉此来控制要不要显示这个动画:

<Track
  {...略}
  $isStatusActive={isStatusActive}
/>

动画方面我的范例如下:

const activeAnimation = css`
  position: relative;
  overflow: hidden;
  &:before {
    content: '';
    position: absolute;
    height: 100%;
    width: 80px;
    top: 0px;
    background: linear-gradient(to right, transparent 0%, #FFFFFF99 50%, transparent 100%);
    animation: ${slide} 1s cubic-bezier(0.4, 0.0, 0.2, 1) infinite;
    box-shadow: 0 4px 10px 0 #FFFFFF33;
  }
`;

const Track = styled.div`
  /* ...略 */
  ${(props) => props.$isStatusActive && activeAnimation}
`;

原理跟前篇一样,我用一个为元素当作渐层光晕,在背景上面重复由左往右移动,超出的部分就用 overflow: hidden; 处理掉,详细原理的部分可参考前篇,我做出来的效果如下:

看起来就觉得这个系统真的很忙,有在认真!

进度条慢慢长出来

那如果我们想要让人家看到我们进度条努力长出来的过程,可以加上一些 setInterval 的动画,那我这个范例是没有将这个功能做进去 <ProgressBar /> 元件里面,我怕里面做的事情太复杂,所以我把他拉到外面做,但如果有考量到很常用到这个效果,或许再把他做进去元件里面也不迟,或是另外再基於 <ProgressBar /> 做出另一个元件也可以:

const [playKey, setPlayKey] = useState(true);
const [transitionValue, setTransitionValue] = useState(0);

useEffect(() => {
  let intervalId;
  setTransitionValue(0);
  if (playKey) {
    setPlayKey(false);
    intervalId = setInterval(() => {
      setTransitionValue((prev) => {
        if (prev >= 120) {
          clearInterval(intervalId);
        }
        return prev + 1;
      });
    }, 30);
  }
}, [playKey]);

Demo 的地方我做一个重播按钮,按下去改变 playKey ,藉此触发 useEffect 内容可以再次执行:

<Button onClick={() => setPlayKey(true)}>
  重播
</Button>

然後藉由上面 setInterval,我们不断的改变 progress 的 value,慢慢把他 +1 加上去,直到终点进度为止就停止加总:

<ProgressBar {...args} value={transitionValue < 50 ? transitionValue : 50} />

最後一定要记得在 <Track /> 上面的 width 加上 transition,这样 width 在变长的时候,才会有连续的过场动画,不会一格跳一格的跳上去:

const Track = styled.div`
  background: ${(props) => props.$color};
  width: ${(props) => props.$value}%;
  height: 8px;
  border-radius: 50px;
  transition: width 0.2s;
  ${(props) => props.$isStatusActive && activeAnimation}
`;

这样就可以看到下面这样慢慢长出来的效果啦!


ProgressBar 元件原始码:
Source code

Storybook:
ProgressBar


<<:  Day 24 Redux 简介

>>:  [Day 27] 机器学习常犯错的十件事

Day 17 管道的应用

Kernel里面除了前述几种物件之外,我们以下介绍其他几种比较重要的物件,首先我们来看所谓的pipe...

2021-Day26. Serverless(十 四):AWS - SSL / TLS 凭证

影片一录好,就把AWS帐号给关掉了,就不用像昨天一样後制到怀疑人生... ...

Day14 v-cloak与v-pre

今天再多来看看两个Vue的指令,v-cloak与v-pre v-cloak 使用v-cloak的原因...

Two Sum 演算法初阶题,Ruby 30 天刷题修行篇第九话

大家好,我是阿飞,今天的题目是演算法初阶题目 Two Sum,让我们一起来看看: 题目来源 Code...

iT铁人赛完赛感想 - 30天的结束不是完结

今天原本要发表的内容是「用Keycloak学习JWT权杖格式」,然後应该还会有1-2篇与JWT相关...