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