Shadow DOM 的一次挖掘 —— 揭秘 range input 的内部结构

2022-06-29 15:01:37 浏览数 (1)

最近在使用 rc-slider 组件实现滑块功能时,遇到了一个 iOS 的 Bug,即滑动时经常会回弹到滑动前的位置,相关 issue 见链接。于是就想着用 range input 这一滑块效果。

range input 的构成:

此外 range input 还包含各种属性,除了具有 input 元素所共享属性外,还包括 max、min、step、list 等四个属性。

一、range input 的在各个浏览器上的构成差异

为了实现不同浏览器下的一致外观,那么我们首先需要了解各浏览器下的表现差异。

先来看看 range input 在不同浏览器下的内部结构:

Chrome

首先在 Settings 中勾选 Show user agent shadow DOM。

打开 Element,不难发现: ::-webkit-slider-runnable-track 匹配 track ::-webkit-slider-thumb 匹配 thumb

Firefox

在搜索栏输入 about:config 开启 showAllAnonymousContent

打开 Element 面板,我们看到 range input 元素包含三个 <div> ,我们可以分别选中每个 div,根据页面中高亮的区块分辨出他们分别代表什么。

::-moz-range-track 匹配第一个 div ::-moz-range-progress 匹配第二个 div ::-moz-range-thumb 匹配第三个 div

Edge

Edge 提供了以下伪元素来控制滑块的样式:

::-ms-fill-lower: 已填充的区域 ::-ms-fill-upper: 还没有填充的区域 ::-ms-ticks-before: 前面、上面的刻度线 ::-ms-ticks-after: 后面、下面的刻度线 ::-ms-thumb: 滑动改变 slider 数值的小圆圈 ::-ms-track: 滑块凹槽 ::ms-tooltip: 拖动时候显示的文字。注意,这个元素只能用 display:none 等隐藏样式。

Edge 中的结构比较复杂,我们只能访问带有 -ms-* id 的元素,还有很多元素无法访问到。

二、range input 的构成部分的在各个浏览器的表现差异

接着我们看下 range input 的构成部分的在各个浏览器的表现差异:

input range 

box-sizing 在 Chrome Firefox Edge 中都是 content-box,而 input range 内部元素 (track、thumb...) Chrome 是 border-box, 其他是 content-box。

在 Chrome 、Safari、Edge 下我们需要声明下面的 CSS 样式取消系统默认样式后才能设置我们想要的自定义样式。(由于某种原因,音轨已经默认设置了)

代码语言:javascript复制
input[type='range'] {
  -webkit-appearance: none;
  
  &::-webkit-slider-thumb {
    -webkit-appearance: none;
  }
}

track

在 Chrome 中,我们设置的轨道宽度会被忽略,这么看来,track 的宽度必须是依赖于 range input 宽度。而使用 tranform: scaleX 似乎是唯一的方法来使 track 比它的父滑块更宽或更窄。但是这么做在 Chrome 和 Edge 中 thumb 也是水平缩放的,因为 thumb 是 track 的子节点。不过,在 Firefox 中不是这样,因为它的大小不会受到 track 的影响,因为 track 和 thumb 是兄弟节点。

thumb

Edge 和 Firefox 的 thumb 滑动区域是 range input 的内容区域。

Chrome 的滑动区域是 track 的内容区域:

已填充的区域元素 (progress)

Firefox 中使用 :: -moz-range-progress 伪元素 和 Edge 中使用::-ms-fill-lower 伪元素匹配这个元素。

从上文的 input range 结构中我们已经知道,这个元素在 Firefox 中是 track 元素的兄弟元素,其大小相对于 range input,在 Edge 中是 track 元素的子元素,其大小相对于 track 元素。但是在 Chrome 中没有提供伪元素来匹配此元素。 Edge 中填充区域的宽度为 thumb 的中间点到 track 内容左边界的距离:

在 Firefox 中填充区域的宽度为 thumb 左右边界距离 input 内容框左右边界的比例点到 track 内容左边界的距离,这和其他浏览器的表现不一致。不过,如果 thumb 的宽度为 0 的话,那么填充区域的表现就会与其他浏览器一样了。如果一定有 thumb 的尺寸,那么就能需要自己根据当前的值来绘制填充区域。

为了实现在不同浏览器下样式都一样的滑块,需要在各浏览器的伪类下设置统一的样式。由于以下样式设置无效,

代码语言:javascript复制
input::-webkit-slider-runnable-track, 
input::-moz-range-track, 
input::-ms-track { /* common styles */ }

所以建议使用 mixin 编写通用样式。

代码语言:javascript复制
@mixin track() { /* common styles */ }

input {
  &::-webkit-slider-runnable-track { @include track }
  &::-moz-range-track { @include track }
  &::-ms-track { @include track }
}

三、应用

常见 slider 实现

分析了 range input 元素后,来看看如何使用它实现常见的 slider:

由于在 Chrome 没有提供填充区域的伪元素,那么怎么自定义填充区域的颜色呢?

也就是在一个 track div 元素中如何展示多个颜色,那么这时就可以想到用线性渐变、或者多背景这种方法。

至于填充区域位置的控制自然就是用 background-size,而这个位置值可以根据 input 的当前值通过 CSS 变量控制,或者直接在 style 里设置 background-size。

在计算填充区域范围时,需要考虑上文提到的 Chrome 已填充区域范围的表现,具体实现如下

代码语言:javascript复制
@mixin track {
    background: linear-gradient(100deg, #5dd8fb 2%, #5dc1fb 100%)
        0/ var(--sx) 100% no-repeat,  $track-color;
}

// --val: 当前input的值  $thumb-w: thumb的宽度
[type='range'] {
  --range: calc(var(--max) - var(--min));
  --ratio: calc((var(--val) - var(--min))/var(--range));
  --sx: calc(.5*#{$thumb-w}   var(--ratio)*(100% - #{$thumb-w}));
}

这里需要注意一点,由于 Chrome 和 Edge 填充区域的特点,track 高度应小于 thumb 高度,不然效果可能会不如你预期。

在线 demo

设有 step 属性的 slider 实现

要实现这个效果需要自行维护 step dot。

html 结构:

代码语言:javascript复制
const dots = [-20, 0, 20, 40, 60, 80];

 <div className="input-box">
    {dots.map((dot, index) => {
      return <div className={`dot dot-${index   1}`} />;
    })}

    <input
      type="range"
      onChange={(event) => {
        setValue( event.target.value);
      }}
      min="-20"
      max="80"
      step="20"
      style={{
        "--min": -20,
        "--max": 80,
        "--step": 20,
        "--val": value
      }}
      value={value}
    />
</div>

step dot 样式的控制:

  1. 确定位置。step dot 的水平中心点始终和已填充区域的右边界对齐,上一个案例中已经说明了如何计算这个边界值。
代码语言:javascript复制
.input-box {
  position: relative;
  width: 300px; // 宽度和input一样
  font-size: 0; // 消除input行框的strut对高度的影响
}

.dot {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translate(-50%, -50%);

  // 这里使用了sass的for循环和当前input valuez值计算位置,当然你也可以在style中计算。
  @for $i from 1 through 6 {
    &.dot-#{$i} {
      --val1: calc((#{$i} -1)*var(--step) -var(--step));
      --ratio1: calc((var(--val1) - var(--min))/ var(--range));
      left: calc(.5*#{$thumb-w}   var(--ratio1)*(100% - #{$thumb-w}));
    }
  }

  &.dot-1 {
    transform: translate(0, -50%);
    left: 0;
  }
}
  1. 提高 thumb 元素的层叠水平,使其在 dot 之上。
代码语言:javascript复制
@mixin thumb() {
    transform: translateZ(0);
}
  1. 高亮 dot
代码语言:javascript复制
.dot.active {
   border: 2px solid #00b9fa;
}

{dots.map((dot, index) => {
  const active = value >= dot ? "active" : "";
  return <div className={`dot dot-${index   1} ${active}`} />;
})}

在线 demo

带散列标记的范围控件

type=range 的 input 元素提供了 list 属性用于实现带散列标记的范围控件,其值是 details 元素的 id 值。但不幸的是,这个使用属性实现的效果很不理想,也无法自定义其样式。所以要实现跨浏览器的带散列标记的范围控件,需要自行使用 repeating-linear-gradient 实现散列标记,使用 label 元素实现标记的值。

demo 地址

tooltip 展示

Edge 是唯一一个通过: :-ms-tooltip 提供工具提示的浏览器,但是它不显示。

在 DOM 中,不能真正进行样式设置。所以在实现该功能时需要把它隐藏掉,然后使用 output 元素展示。

站点或应用程序可以将计算结果或用户操作的结果注入其中的一个容器元素

在线 demo

更多实践

  • 巧用两个 type=range input 实现区域范围选择:

思路是:两个 type=range 输入框叠在一起,然后叠在上面的选择框的只有中间的拖拽按钮,背后的拖拽背景条直接隐藏,这样,视觉上就是一个背景条,2 个拖拽按钮了。

  • 使用 type=ragne input mask 实现评星功能

具体请查看文章

实现图解:

兼容性:

参考:

  • https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input/range
  • https://css-tricks.com/sliding-nightmare-understanding-range-input/
  • https://juejin.cn/post/6919452754716934152
  • https://www.zhangxinxu.com/wordpress/2021/02/range-input/

紧追技术前沿,深挖专业领域

扫码关注我们吧!

0 人点赞