构建表单小组件之Select

2022-04-09  本文已影响0人  skoll

简介

1 .之前看了ant design的select的dom结构,是ui>li的结构,可能根源就在这里
2 .而且他的考虑还很全。

实现目标

1 .状态:正常,活动,打开
2 .键鼠都支持
3 .细节:

1 .控件处于活动状态,用户点击空间以外的位置
2 .控件是活动状态,但用户使用键盘将焦点移动到另一个小部件
3 .支持tab移动焦点
4 .出于可访问性方面的原因,所有尺寸都会由em值表示,用来确保用户在文本模式下使用浏览器缩放时组件的可缩放性.1em=16px

4 .活动状态的触发

1 .用户点击
2 .用户按下tab让控件获得了焦点
3 .控件呈现打开状态,然后用户点击控件

5 .打开状态的触发

1 .控件在非打开时被用户点击

6 .关闭状态的触发

1 .点击页面别的地方
2 .点击其中一个选项
3 .按esc快捷键

7 .值怎么改变

1 .用户在打开状态下点击一个选项
2 .控件在活动状态下用户按下键盘上方向键

8 .交互逻辑还需要搞这个:https://en.wikipedia.org/wiki/Usability_testing 用户可行性测试http://uxdesign.com/

成果

展示方面
   <style>
      .select{
        position: relative;
        /* 先创建一个上下文定位 */
        display: inline-block;
        /* 组件变成文本流,还能调整大小 */

        /* 美化部分 */
        font-size: 0.625em;
        box-sizing: border-box;

        /* 向下的箭头需要一些额外的空间 */
        padding: .1em 2.5em .2em .5em;
        width: 10em;
        border:.2em solid #000;
        border-radius: .4em;
        box-shadow: 0 .1em .2em rgba(0,0,0,.45);

        background:#F0F0F0;

      }

      .select:after{
        /* 向下的箭头 */
        content:"▼";
        position: absolute;
        z-index: 1;
        /* 防止箭头覆盖选项列表 */
        top: 0;
        right: 0;

        box-sizing:border-box;

        height: 100%;
        width: 2em;
        padding-top:.1em;

        border-left:.2em solid #000;
        border-radius: 0 .1em .1em 0;

        background-color: #000;
        color:#FFF;
        text-align: center;
      }

      /* 选中或者focus的样式 */
      .select .active,.select:focus{
        outline: none;
        box-shadow: 0 0 3px 1px #227755;
      }

      .select .value{
        display: inline-block;
        width: 100%;
        /* 确保item的宽度不会超过父元素的宽度 */
        overflow: hidden;
        vertical-align: top;
        white-space: nowrap;
        text-overflow: ellipsis;

        overflow: hidden;
      }

      /* 显示列表部分 */
      .select .list{
        position: absolute;
        top:100%;
        left:0;

        z-index: 2;
        list-style: none;
        margin: 0;
        padding: 0;
        box-sizing: border-box;
        min-width: 100%;
        max-height: 10em;
        overflow-y:auto;
        overflow-x: hidden;

        border:.2em solid #000;
        border-top-width:.1em;
        border-radius:0 0 .4em .4em;
        box-shadow: 0 .2em .4em rgba(0,0,0,.4);
        background:#f0f0f0;
      }

      .select .option{
        padding:.2em .3em;
      }

      .select .highlight{
        background-color: #000;
        color:#fff;
      }

      /* 列表隐藏的时候 */
      .select .list .hidden{
        max-height: 0;
        visibility: hidden;
      }
   </style>

<div class="select" tabindex="0">
      <!--用来显示当前选的值  -->
      <span class="value">Apple</span>
      <ul class="list">
        <li class="option">Apple</li>
        <li class="option">Orange</li>
        <li class="option">Banana</li>
      </ul>
  </div>

部分

1 .在浏览器中,js是一种不可靠技术

1 .用户关掉:可能性很小
2 .脚本没有加载,移动端,网络不好的地方非常常见
3 .脚本和第三方脚本冲突。用户使用的跟踪脚本和一些书签工具引发
4 .脚本和浏览器的拓展冲突
5 .用户老旧的浏览器

2 .在使用自定义部件前,还需要添加一个标准的select元素。向下兼容

缺陷

1 .如果有滚动的话,鼠标上下键移动到最底边,没有自动发生滚动。
2 .ant design虽然也会有联动,但是超出一屏也是有问题的

总结

1 .tabindex的使用,这里tabindex=-1可以保证原生组件永远不会获得焦点。而且还能保证当用户使用键盘和鼠标时,我们的自定义组件能够获得焦点
2 .语义化

真正的语义化

1 .虽然我们做出来的已经看起来是一个选择框了,但是从浏览器角度来看并不是,所以辅助技术并不能明白这是一个选择框。
2 .ARIA技术:是一组用来扩展HTML的属性集,可以让我们更好地描述角色,状态和属性,就像我们刚才设计的元素是他试图传递的原生元素一样
3 .role属性:接受一个值,定义了元素的用途。每一个role定义了自己的需求和行为,在这里面我们使用listbox这一个role
4 .window.aria.start()我还以为这是一个方法,直接调用浏览器的api就可以了,原来还有乾坤

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
   <style>
      .widget select,.no-widget .select{
        position: absolute;
        left: -5000em;
        /* 隐藏的操作都是这种left设置的很远,这是为啥呢,和其他不一样的区别是什么呢 */
        height: 0;
        overflow:hidden;
      }

      .select{
        position: relative;
        /* 先创建一个上下文定位 */
        display: inline-block;
        /* 组件变成文本流,还能调整大小 */

        /* 美化部分 */
        font-size: 0.625em;
        box-sizing: border-box;

        /* 向下的箭头需要一些额外的空间 */
        padding: .1em 2.5em .2em .5em;
        width: 10em;
        border:.2em solid #000;
        border-radius: .4em;
        box-shadow: 0 .1em .2em rgba(0,0,0,.45);

        background:#F0F0F0;
        user-select: none;

      }

      .select:after{
        /* 向下的箭头 */
        content:"▼";
        position: absolute;
        z-index: 1;
        /* 防止箭头覆盖选项列表 */
        top: 0;
        right: 0;

        box-sizing:border-box;

        height: 100%;
        width: 2em;
        padding-top:.1em;

        border-left:.2em solid #000;
        border-radius: 0 .1em .1em 0;

        background-color: #000;
        color:#FFF;
        text-align: center;
      }

      /* 选中或者focus的样式 */
      .select .active,.select:focus{
        outline: none;
        box-shadow: 0 0 3px 1px #227755;
      }

      .select .value{
        display: inline-block;
        width: 100%;
        /* 确保item的宽度不会超过父元素的宽度 */
        overflow: hidden;
        vertical-align: top;
        white-space: nowrap;
        text-overflow: ellipsis;

        overflow: hidden;
      }

      /* 显示列表部分 */
      .select .list{
        position: absolute;
        top:100%;
        left:0;

        z-index: 2;
        list-style: none;
        margin: 0;
        padding: 0;
        box-sizing: border-box;
        min-width: 100%;
        max-height: 10em;
        overflow-y:auto;
        overflow-x: hidden;

        border:.2em solid #000;
        border-top-width:.1em;
        border-radius:0 0 .4em .4em;
        box-shadow: 0 .2em .4em rgba(0,0,0,.4);
        background:#f0f0f0;
      }

      .select .option{
        padding:.2em .3em;
      }

      .select .highlight{
        background-color: #000;
        color:#fff;
      }

      /* 列表隐藏的时候 */
      .select .list.hidden{
        max-height: 0;
        visibility: hidden;
      }
   </style>
          
</head>
<body>
  <form action="" class="no-widget">
    <select name="myFruit">
      <option>Cherry</option>
      <option>Lemon</option>
      <option>Banana</option>
      <option>Strawberry</option>
      <option>Apple</option>
  </select>
  <!-- js没好之前先用这个 -->

  <div class="select" role='listbox'>
    <span class="value">Cherry</span>
    <ul class="list hidden" role="presentation">
      <li class="option">Cherry</li>
      <li class="option">Lemon</li>
      <li class="option">Banana</li>
      <li class="option">Strawberry</li>
      <li class="option">Apple</li>
    </ul>
  </div>
  </form>

  <script>
    function deactiveSelect(select){
          if(!select.classList.contains('active'))return

          let optList=select.querySelector('.list')
          optList.classList.add('hidden')
          optList.classList.remove('active')
    }

    function activeSelect(select,selectList){
          if(select.classList.contains('active'))return
          selectList.forEach(deactiveSelect)
          select.classList.add('active')
    }

    // 打开和收齐列表
    function toggleList(select){
        let list=select.querySelector('.list')
        list.classList.toggle('hidden')
    }

    // 高亮一个选项
    function highlightOption(select,option){
      let list=select.querySelectorAll('.option')
      list.forEach((other)=>{
        other.classList.remove('highlight')
      })

      option.classList.add('highlight')
    }

    //点击更新值的操作
    function updateValue(select,index){
          const nativeWidget=select.previousElementSibling
          const value=select.querySelector('.value')

          const optionList=select.querySelectorAll('.option')
          optionList.forEach((option)=>{
            option.setAttribute('aria-selected','false')
          })
          optionList[index].setAttribute('aria-selected','true')
          nativeWidget.selectedIndex=index
          value.innerHTML=optionList[index].innerHTML
          highlightOption(select,optionList[index])
    }

    function getIndex(select){
        let nativeWidget=select.previousElementSibling;
        return nativeWidget.selectedIndex
    }

    window.addEventListener('load',function(){
      let form=document.querySelector('form')
      form.classList.remove('no-widget')
      form.classList.add('widget')

      // 绑定js
       let selectList=document.querySelectorAll('.select')
       selectList.forEach((select)=>{
         const optionList=select.querySelectorAll('.option')
        //选到每一个list

         const selectedIndex=getIndex(select)

         select.tabIndex=0
        // 让自定义的节点可以获得焦点
         select.previousElementSibling.tabIndex=-1
        //  让原生组件取消焦点

         updateValue(select,selectedIndex)
         optionList.forEach((option,index)=>{
           option.addEventListener('mouseover',()=>{
             highlightOption(select,option)
           })

           option.addEventListener('click',(event)=>{
             updateValue(select,index)
           })

         })

         select.addEventListener('click',(event)=>{
            toggleList(select)
          })

         select.addEventListener('focus',()=>{
           activeSelect(select,selectList)
         })

         select.addEventListener('blur',()=>{
           deactiveSelect(select)
         })

         select.addEventListener('keyup',(event)=>{
          // 添加键盘事件
            let len=optionList.length;
            let index=getIndex(select)

            if(event.keyCode===40&&index<len-1){
              index++
            }

            if(event.keyCode===38&&index>0){
              index--
            }

            updateValue(select,index)
         })
       })

       
    })
  </script>
</body>
</html>
上一篇 下一篇

猜你喜欢

热点阅读