Slider
是一个滑动型输入器,允许使用者在数值区间内进行选择,选择的值可为连续值或是离散值。
这边不免俗的来名词解释一下,在 Slider 上面可让我们拖拉的圆形小球一般都称为 thumb,而整个 Slider 的可拖拉轨迹我们称为 rail,而标示我们所选取范围的轨迹我们称为 track。
命名之所以重要不在於我们多麽想要装逼来炫耀自己的英文,而是希望对於维护专案有一个共识,让别人来维护这套程序的时候不至於因为命名的不统一而影响他对於程序码的理解。
这个元素看起来在 MUI 以及 Antd 都还蛮有共识的,在样式上和他的变化型态都没有太大的差异。大致上我们可以发现有几种 Slider 的应用型态:
我们常看见的应用有
为了简化复杂度,我们会先只讨论单一 thumb 的 Slider。
选取范围属性
由於 Slider 是为了方便我们在数值范围内做选择,当然首先我们必须要先定义他的范围,在 MUI 及 Antd 在定义范围时,都用 min, max 这两个 props 来实现,输入的值皆为 number。
step 属性
定义完 min, max 的范围之後,到底我们选取数值的颗粒度有多细呢?若 min, max 为 1 ~ 10,step 为 1 的话,表示 1 ~ 10 我们只有 10 种可能,最小单位为 1;在这边建议 step 必须要大於 0,而且建议可被 min, max 整除。意思是,若 step 为 2,那我们 min, max 就不适合 1 ~ 10 ,因为 1 无法被 2 整除。
数值属性
在数值输入元件当中,最重要的 props 莫过於 value 以及 defaultValue 了,延续前一篇 Text Field 提到的,这边也会分成 Controlled component 以及 Uncontrolled component,因此在使用 value 以及 defaultValue 时,同样需要特别留意。
外观属性
外观属性我这边挑一个简单但常用的来做,因为有些网站需要配合主题来改变颜色,因此 themeColor 属性算是还蛮常会被使用的属性。
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
min | 最小值 | number | 0 |
max | 最大值 | number | |
step | 步长,取值必须大於 0,并且可被 (max - min) 整除 | number | |
value | 当前数值 | number | |
defaultValue | 预设数值 | number | |
onChange | 数值改变的 callback function | function(event: object) => void | |
themeColor | 颜色 | primary, secondary, 色票 |
这边分享两种 Slider 元件的做法给大家
方法一:纯手刻
我准备的 DOM 的结构如下:
<CustomSliderContainer
ref={railRef}
$thumbPosX={thumbPosX}
>
<div ref={thumbRef} className="custom-slider__thumb" />
</CustomSliderContainer>
其中需要包含 Slider 三元素 rail
, track
, thumb
,我的结构只有两层,父层是 rail 以及 track,子层是 thumb。其中 track 会需要知道 thumb 的位置,并以 before
这个 Pseudo-elements
来画出:
const CustomSliderContainer = styled.div`
width: 320px;
height: 6px;
background: #ddd; /* rail */
border-radius: 5px;
position: relative;
.custom-slider__thumb {
width: ${SIZE_THUMB}px;
height: ${SIZE_THUMB}px;
border-radius: 100%;
background: ${(props) => props.theme.color.primary};
position: absolute;
top: 50%;
left: ${(props) => props.$thumbPosX}px;
transform: translateY(-50%) translateX(-50%);
cursor: pointer;
}
&:before {
/* track */
content: '';
position: absolute;
height: 6px;
border-radius: 5px;
width: ${(props) => props.$thumbPosX}px;
background: ${(props) => props.theme.color.primary};
}
`;
再来就是最重要的 拖拉
效果,我使用的方式是透过 RxJS
来实作拖拉功能,RxJS 是一套藉由 Observable sequences 来组合非同步行为和事件基础程序的 Library。
2017 铁人赛的得奖作品有一篇神作是讲关於 RxJS 的观念以及实作,其中的实作有包含简易的拖拉功能,推荐大家可以看一看:
https://ithelp.ithome.com.tw/articles/10187333
其中拖拉的关键步骤及程序码如下:
const thumbDOM = thumbRef.current;
const { body } = document;
const mouseDown = fromEvent(thumbDOM, 'mousedown');
const mouseUp = fromEvent(body, 'mouseup');
const mouseMove = fromEvent(body, 'mousemove');
mouseDown
.pipe(
concatMap(() => mouseMove.pipe(takeUntil(mouseUp))),
map((moveEvent) => moveEvent.clientX),
)
.subscribe((mousePosX) => {
handleUpdatePosition({ mousePosX });
});
到这边其实重点功能就都已经完成了,手刻虽然很爽很屌,但是会需要自己处理许多细节,可能有些被习以为常,觉得是很自然会有的功能,但是你没有实作的话,他就是没有,用起来就是会怪怪的。例如 Slider 除了可以拖拉以外,还会有人希望他点击 rail 的任何一个地方,thumb 就可以跳到那里。
为了做到这个功能,我们就必须要在 rail 上面监听点击事件,关键步骤跟拖拉很像,只是这边是直接把点击事件的位置资讯取出来,这样就完成了:
const railDOM = railRef.current;
const mouseDown = fromEvent(railDOM, 'mousedown');
mouseDown
.pipe(
map((mouseEvent) => mouseEvent.clientX),
)
.subscribe((mousePosX) => {
handleUpdatePosition({ mousePosX });
});
以上两个部分,包含拖拉以及点击,最终我们取得的 mousePosX
需要再把它转换成 Slider bar 上面 track 的长度,做法是我们需要拿到 rail 的位置
,把他跟 mousePosX
做相减,我们就能够拿到以 rail 原点为中心距离点击位置的长度了:
除了上述提到的点击之後 thumb 要跳到点击位置的功能之外,其实手刻还蛮多细节要处理的,下面是我想到的一些例子:
当我们真的很懒得处理这些麻烦事的时候,或许我们可以采用方法二。
方法二:覆写原生 input range 的样式
由於 range 是 input 的一种类型,我们无法用传统的 CSS 编辑方法来修改样式,所以需要使用到 -webkit-appearance
这个特殊属性,这是 webkit 特有的属性,代表使用系统预设的外观
,只要我们将这个属性设为 none
,那麽原本 range 的样式就不会呈现,我们就能加入自己希望的 CSS 属性来改变 rail 的样式:
<StyledSlider
{...props}
/>
const railStyle = css`
background: #ddd; /* rail color */
width: 320px;
height: 6px;
border-radius: 5px;
`;
const StyledSlider = styled.input`
&[type='range'] {
-webkit-appearance: none;
${railStyle}
}
`;
接下来我们要处理 thumb 的样式,这时候我们要使用另外一个 webkit 的伪元素 ::-webkit-slider-thumb
来修改:
const StyledSlider = styled.input`
&[type='range'] {
-webkit-appearance: none;
${railStyle}
}
&[type='range']::-webkit-slider-thumb {
/* thumb style */
-webkit-appearance: none;
width: ${SIZE_THUMB}px;
height: ${SIZE_THUMB}px;
border-radius: 100%;
border: 2px solid white;
background: white;
cursor: pointer;
}
`;
到目前为止,我们就能够做到以下这样的样式:
只要稍微调整一下样式,我们就充分能够做到像 Google Color Pikcer
这样的 Slider:
接下来我们要处理 track 的样式,track 样式我提供的方法是使用 input 的为元素 before
来实践:
const trackStyle = css`
background: ${(props) => props.$color};
border-radius: 5px;
height: 6px;
`;
const StyledSlider = styled.input`
&[type='range'] {
-webkit-appearance: none;
${railStyle}
&:before {
content: '';
position: absolute;
z-index: -1;
width: ${(props) => props.$widthRatio}%;
left: 0px;
${trackStyle}
}
}
//...略
`;
主要的概念是,track 的长度是 rail 起始位置到 thumb 的距离,所以我们只要计算出这个距离并把他换算成 width
属性的百分比就可以了。
<StyledSlider
ref={sliderRef}
type="range"
$widthRatio={(currentValue / max) * 100}
{...props}
/>
最後跟大家分享一个我过去做过跟 Slider 相关的 Sideproject,这个 Sideproject 详细的内容我写在 github 的 Readme:
https://github.com/TimingJL/dribbble-404-images-typescript
Slider 元件原始码(纯手刻版本):
Source code
Slider 元件原始码(覆写 input range 版本):
Source code
Storybook:
Slider
改变 HTML5 range 样式的两种方法
https://www.oxxostudio.tw/articles/201503/html5-input-range-style.html
How to style range input with CSS and JavaScript for better usability
https://tippingpoint.dev/style-range-input-css
<<: Day 5: 人工智慧在音乐领域的应用 (AI发展史与简介 - 第二次寒冬)
连续 30 天不中断每天上传一支教学影片,教你如何用 React 加上 Firebase 打造社群...
=x= 🌵 Yachts 前台页面 Layout & deck plan / Video -...
今日文章目录 需求说明 过程纪录 参考文章 今天来练习Router切换页面。 需求说明 两个页面:...
现在我们可以用各种方法将资料读取出来,不过通常读取後还要将资料做一些转换才适用,举个例子像是 boo...
在现在这种讲求快速开发的开发模式,我们通常不太会自己将所有功能都自己硬刻出来,而是会去使用第三方的套...