女王控的博客

focus-within伪类及其交互应用

了解 CSS :focus-within 伪类

CSS :focus-within 伪类选择器和 IE8 就开始支持的:focus 可以说是近亲,区别在于:focus 表示当前元素处于 focus 状态时候干嘛干嘛,而:focus-within 表示当前元素或者当前元素的子元素处于 focus 状态时候干嘛干嘛。

举个例子:

css 复制代码
form:focus {
  outline: solid;
}

表示仅仅当<form>处于 focus 状态时候,<form>元素 outline 出现。换句话说,子元素 focus,可以让任意父级元素样式发生变化。

这个新特性的支持,未来势必会带来更加丰富的交互形式,以及特定场景简化我们实现。

CSS :focus-within 伪类实际应用举例

表单输入勿扰模式

当我们表单输入,选择或者进行交互时候,页面上表单以为其他内容全部都不可见。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    html,
    body {
      background-color: white;
      margin: 0;
    }

    form {
      transition: outline .2s;
      outline: 2000px solid hsla(0,0%,100%,0);
      position: relative;
      z-index: 1;
    }

    form:focus-within {
      outline: 2000px solid hsla(0,0%,100%,1);
    }
  </style>
</head>
<body>
  购物订单详情
  <form class="p20 bgwh">
    <h6 class="f14 b">完成您的购物订单 <i class="icon icon_title_inf vn ui-tips" title="星号为必填"></i></h6>
    <div class="clearfix mt15">
      <label class="left">邮箱<span class="red abs">*</span></label>
      <div class="cell">
        <input type="email" class="ui-input" name="email" required="">
      </div>
    </div>
    <div class="clearfix mt15">
      <label class="left">手机号<span class="red abs">*</span></label>
      <div class="cell">
        <input type="tel" class="ui-input" name="tel" required="">
      </div>
    </div>
    <div class="clearfix mt15">
      <span class="left">您的款式<span class="red abs">*</span></span>
      <div class="cell">
        <input type="radio" id="radioMale" name="type" value="488"><label class="ui-radio" for="radioMale"></label><label class="mr20 ml5" for="radioMale">男款</label>
        <input type="radio" id="radioFemale" name="type" value="388" required=""><label class="ui-radio" for="radioFemale"></label><label for="radioFemale" class="ml5">女款</label>
        <p id="radioMaleP" class="dn mt10">男款每件488元</p>
        <p id="radioFemaleP" class="dn mt10">女款每件388元</p>
      </div>
    </div>
    <div class="clearfix mt15">
      <label class="left" for="number">购买数量<span class="red abs">*</span></label>
      <div class="cell">
        <input type="number" id="number" class="ui-input" min="1" max="10" size="5" value="1" required="" placeholder="直接输入数值">
      </div>
    </div>
    <div class="clearfix mt15">
      <label class="left">所在城市<span class="red abs">*</span></label>
      <div class="cell">
        <select id="citySelect" required="">
          <option value="">请选择</option>
          <option value="10">深圳</option>
          <option value="5">上海</option>
          <option value="7">北京</option>
          <option value="8">成都</option>
          <option value="6">武汉</option>
        </select>
        <p class="mt10 dn">运费: <strong id="transPrice" class="red">0</strong></p>
      </div>
    </div>
    <div class="clearfix mt15">
      <label class="left">给商家留言</label>
      <div class="cell">
        <div class="ui-textarea"><textarea name="message" rows="4" maxlength="200" placeholder="少于200字符"></textarea></div>
      </div>
    </div>
    <div class="clearfix mt15">
      <span class="left">&nbsp;</span>
      <div class="cell">
        <input type="checkbox" id="checkInsur" name="insurance" value="0.4"><label class="ui-checkbox" for="checkInsur"></label><label class="ml5" for="checkInsur">使用运费险0.4元</label>
      </div>
    </div>
    <p class="pay">您总共需要支付 <strong id="total" class="red">0</strong></p>
    <div class="clearfix mt15">
      <span class="left">是否货到付款</span>
      <div class="cell">
        <input type="checkbox" id="switch" name="pay"><label class="ui-switch" for="switch"></label>
      </div>
    </div>
    <div class="clearfix mt15">
      <label class="left" for="achieveDate">希望到货日期</label>
      <div class="cell">
        <span class="ui-input ui-date-input"><input type="date" id="achieveDate" name="achieve" value="2015-12-27" min="2015-12-27"></span>
        <span class="gray ml20">默认尽快发货</span>
      </div>
    </div>
    <div class="clearfix mt20">
      <span class="left">&nbsp;</span>
      <div class="cell">
        <input type="submit" id="validateSubmit" class="ui-button ui-button-primary" value="提交">
      </div>
    </div>
  </form>
</body>
</html>

本案例是真实的勿扰模式效果可以放心大胆在实际项目中使用,因为这是一个体验增强的交互,就算浏览器不支持,对原本功能也不会有任何影响。

带计数文本域的 focus 高亮

对于带计数的组件化的多行文本域,计数的数值通常是设计在文本域的右下角,这样能够适应各种复杂的场景。如下截图:

2020 01 14 17 56 25

然而这种设计对我们的布局实现带来的挑战。

我们通常想到的方法是,计数元素浮在下面的 textarea 元素上。然而这种实现存在有致命的不足,那就是输入框的内容有可能和我们的计数重叠,以及出现的滚动条和技术文本重叠,如下截图所示:

2020 01 14 17 57 04

因此,我们通常做法就是:边框使用父级<div>元素模拟,文本域元素和技术元素上下依次排列(非重叠),文本域原本的边框去除。

假设 HTML 如下:

html 复制代码
<div class="textarea-x">
  <textarea></textarea>
  <label class="textarea-count">0/250</label>
</div>

则核心 CSS 这样:

css 复制代码
/* 父级div元素模拟边框 */
.textarea-x {
  border: 1px solid #d0d0d5;
  border-radius: 4px;
  background-color: #fff;
}

/* 文本域原本的边框去除 */
.textarea-x > textarea {
  border: 0;
  background: none;
}

然而上面的实现有个非常严重的不足,那就是<textarea>元素 :focus 时候,边框无法高亮,因为 CSS 中没有父选择器!

因此,实际开发的时候,我们会使用相邻兄弟选择器,以及新建一个兄弟元素来模拟边框。

HTML 结构如下:

html 复制代码
<div class="textarea-x">
  <textarea></textarea>
  <div class="textarea"></div>
  <label class="textarea-count">0/250</label>
</div>

原理如下图示意:

2020 01 14 18 00 12

css 复制代码
.textarea-x {
  position: relative;
  z-index: 0;
}

.textarea-x > textarea {
  border: 0;
  background: none;
  outline: 0;
  resize: none;
  position: relative;
}

.textarea-x > .textarea {
  position: absolute;
  border: 1px solid #d0d0d5;
  border-radius: 4px;
  background-color: #fff;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: -1;
}

.textarea-x > :focus + .textarea {
  border-color: #00a5e0;
}

由于.textarea 元素和原生的<textarea>元素是相邻兄弟关系,因此我们可以借助相邻兄弟选择器,让<textarea>元素 focus 时候后面用来模拟边框的 .textarea 元素高亮。也就是这么一段 CSS 代码:

css 复制代码
.textarea-x > :focus + .textarea {
  border-color: #00a5e0;
}

这种实现兼容 IE7+ 浏览器。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    html,
    body {
      background-color: white;
      margin: 0;
    }

    .textarea-x {
      max-width: 300px;
      padding: 1px 1px 24px 0;
      border-radius: 4px;
      -ms-box-sizing: border-box;
      box-sizing: border-box;
      overflow: hidden;
      position: relative;
      z-index: 0;
    }

    .textarea-count {
      position: absolute;
      font-size: 12px;
      line-height: 16px;
      bottom: 5px; right: 10px;
      color: #999;
    }

    .textarea-x > textarea {
      display: block;
      width: 100%;
      padding: 7px 9px;
      border: 0;
      background: none;
      -ms-box-sizing: border-box;
      box-sizing: border-box;
      outline: 0;
      resize: none;
      position: relative;
    }

    .textarea-x > .textarea {
      position: absolute;
      border: 1px solid #d0d0d5;
      border-radius: 4px;
      background-color: #fff;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      transition: border-color .2s;
      z-index: -1;
    }

    .textarea-x > :focus + .textarea {
      border-color: #00a5e0;
    }
  </style>
</head>
<body>
  <div class="textarea-x">
    <textarea id="forFocus" maxlength="250" rows="5"></textarea>
    <div class="textarea"></div>
    <label for="forFocus" class="textarea-count">0/250</label>
  </div>
</body>
</html>

但是,现在有了 CSS :focus-within 伪类选择器,我们的事情就简单多了。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    html,
    body {
      background-color: white;
      margin: 0;
    }

    .textarea-x {
      max-width: 298px;
      border: 1px solid #d0d0d5;
      border-radius: 4px;
      background-color: #fff;
      padding-bottom: 24px;
      transition: border-color .2s;
      overflow: hidden;
      position: relative;
    }

    .textarea-count {
      position: absolute;
      font-size: 12px;
      line-height: 16px;
      bottom: 5px; right: 10px;
      color: #999;
    }

    .textarea-x > textarea {
      display: block;
      width: 100%;
      padding: 7px 9px;
      border: 0;
      background: none;
      -ms-box-sizing: border-box;
      box-sizing: border-box;
      outline: 0;
      resize: none;
    }

    .textarea-x:focus-within {
      border-color: #00a5e0;
    }
  </style>
</head>
<body>
  <div class="textarea-x">
    <textarea id="forFocus" maxlength="250" rows="5"></textarea>
    <label for="forFocus" class="textarea-count">0/250</label>
  </div>
</body>
</html>

更多场景

:focus-within 伪类原本设计的作用是原生表单元素 focus 时候,祖先<form>元素可以也有状态变化。

但是在我看来,:focus-within 功能之强悍,远远不是仅仅和祖先<form>元素玩过家家这么简单。

理论上,只要页面上任意一个元素 focus,通过:focus-within 就能对页面上任意的元素进行样式控制。

例如:

css 复制代码
html:focus-within xxx {
}
  1. 相邻选择器,加号,或者弯弯都只能选择后面的元素,但是有了:focus-within,我们就可以对前面元素进行控制,虽然只能在:focus 行为场景下。例如请重复输入密码时候,让上面的输入密码框也高亮。

  2. 基于纯 CSS 和 focus 行为实现下拉列表的交互成为了可能。只要把按钮和下拉元素放在一个容器中,就可以了。

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
      <style>
        html,
        body {
          background-color: white;
          margin: 0;
        }
    
        .bar {
          background-color: #e3e4e5;
          color: #888;
          padding-left: 40px;
        }
    
        .summary {
          display: inline-block;
          padding: 5px 28px;
          text-indent: -15px;
          -webkit-user-select: none;
          -moz-user-select: none;
          -ms-user-select: none;
          user-select: none;
          outline: 0;
          position: relative;
          z-index: 1;
        }
    
        .summary::after {
          content: '';
          position: absolute;
          width: 12px; height: 12px;
          margin: 4px 0 0 .5ch;
          background: url(./arrow-on.svg) no-repeat;
          background-size: 100% 100%;
          transition: transform .2s;
        }
    
        .details:focus-within .summary,
        .summary:hover {
          background-color: #fff;
          box-shadow: inset 1px 0 #ddd, inset -1px 0 #ddd;
        }
    
        .details:focus-within .summary::after {
          transform: rotate(180deg);
        }
    
        .box {
          display: none;
          position: absolute;
          border: 1px solid #ddd;
          background-color: #fff;
          min-width: 100px;
          padding: 5px 0;
          margin-top: -1px;
        }
    
        .details:focus-within .box {
          display: block;
        }
    
        .box a {
          display: block;
          padding: 5px 10px;
          transition: background-color .2s, color .2s;
          color: inherit;
        }
    
        .box a:hover {
          background-color: #f5f5f5;
        }
    
        .box a:active {
          background-color: #f0f0f0;
          color: #555;
        }
    
        .box sup {
          position: absolute;
          color: #cd0000;
          font-size: 12px;
          margin-top: -.25em;
        }
    
        a {
          color: inherit;
          text-decoration: none;
        }
      </style>
    </head>
    <body>
      <div class="bar">
        <div class="details">
          <a href="javascript:" class="summary" tabindex="0">我的消息</a>
          <div class="box">
            <a href="javascript:">我的回答<sup>12</sup></a>
            <a href="javascript:">我的私信</a>
            <a href="javascript:">未评价订单<sup>2</sup></a>
            <a href="javascript:">我的关注</a>
          </div>
        </div>
      </div>
      <p>这里放一段文字表明上面的是悬浮效果。</p>
    </body>
    </html>

评论

阅读上一篇

使用canvas在前端实现图片水印合成
2020-01-16 11:11:39

阅读下一篇

SVG/Canvas中nonzero和evenodd填充规则
2020-01-14 16:48:52
0%