带有“更多”按钮的容器自适应选项卡

Avatar of Osvaldas Valutis
Osvaldas Valutis

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

或者优先级导航模式,或者逐步折叠的导航菜单。我们至少可以用三种方式命名它。

对于选项卡和菜单,有多种 UX 解决方案,它们各自都具有其他解决方案无法比拟的优势,您只需为要解决的案例选择最佳解决方案即可。在设计与开发机构 Kollegorna,我们正在就最适合客户网站选项卡的 UX 技术进行辩论……

我们一致认为它应该是 单行,因为选项卡项的数量未知,并且将我们的选择范围缩小到两种:水平滚动和带有“更多”按钮的自适应。首先,前者的问题在于,水平滚动作为一个功能并不总是对用户直观(尤其是对于选项卡等狭窄元素),而还有什么比按钮(“更多”)更直观呢?其次,使用鼠标控制设备进行水平滚动并不是一件很舒服的事情,因此我们可能需要使用额外的箭头按钮使我们的 UI 更复杂。综合考虑,我们最终选择了后一种选择

规划

这里的主要问题是是否可以在没有 JavaScript 的情况下实现?部分可以,但是它附带的限制可能使其仅适合概念博物馆,而不是现实生活中的场景(无论如何,Kenan 做得非常好)。尽管如此,对 JS 的依赖并不意味着如果出于某种原因该技术不可用,我们就无法使其可用。渐进增强和优雅降级才能获胜!

由于选项卡项的数量不确定或不稳定,我们将使用 Flexbox,它可以确保项在容器元素中均匀分布,而无需设置宽度。

初始原型

在视觉上和技术上都有两个列表:一个用于适合容器的项,另一个用于不适合的项。由于我们将依赖 JavaScript,因此使用单个列表作为初始标记是完全可以的(我们将使用 JS 复制它)

<nav class="tabs">
  <ul class="-primary">
    <li><a href="...">Falkenberg</a></li>
    <li><a href="...">Braga</a></li>
    <!-- ... -->
  </ul>
</nav>

通过一点基于 flex 的 CSS,事情开始变得严肃起来。我将在这里跳过示例中的装饰性 CSS 属性,只放置真正重要的内容

.tabs .-primary {
  display: flex;
}
.tabs .-primary > li {
  flex-grow: 1;
}

这是我们已经拥有的

Container-Adapting Tabs With More Button

优雅降级

现在,在用 JavaScript 渐进地增强它之前,让我们确保如果没有 JS 可用,它会优雅地降级。JS 缺失的原因有很多:它仍在加载,它存在致命错误,它未能通过网络传输。

.tabs:not(.--jsfied) {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

一旦 JavaScript 出现,--jsfied 类名就会添加到容器元素中,该类名会抵消上面的 CSS

container.classList.add('--jsfied')

事实证明,我之前提到的水平滚动策略在这里可能会派上用场!当没有足够的菜单项空间时,溢出内容会在容器内被裁剪,并且会显示滚动条。这比空空间或损坏的东西好得多,不是吗?

Container-Adapting Tabs With More Button

缺失部分

首先,让我们插入缺失的 DOM 部分

  • 次级(下拉)列表,它是主列表的副本;
  • “更多”按钮。
const container = document.querySelector('.tabs')
const primary = container.querySelector('.-primary')
const primaryItems = container.querySelectorAll('.-primary > li:not(.-more)')
container.classList.add('--jsfied')

// insert "more" button and duplicate the list

primary.insertAdjacentHTML('beforeend', `
  <li class="-more">
    <button type="button" aria-haspopup="true" aria-expanded="false">
      More &darr;
    </button>
    <ul class="-secondary">
      ${primary.innerHTML}
    </ul>
  </li>
`)
const secondary = container.querySelector('.-secondary')
const secondaryItems = secondary.querySelectorAll('li')
const allItems = container.querySelectorAll('li')
const moreLi = primary.querySelector('.-more')
const moreBtn = moreLi.querySelector('button')
moreBtn.addEventListener('click', (e) => {
  e.preventDefault()
  container.classList.toggle('--show-secondary')
  moreBtn.setAttribute('aria-expanded', container.classList.contains('--show-secondary'))
})

在这里,我们将次级列表嵌套到列表中,并使用一些aria-*属性。我们希望我们的导航菜单是可访问的,对吧?

还有一个附加到“更多”按钮上的事件处理程序,它会切换容器元素上的--show-secondary类名。我们将使用它来显示和隐藏次级列表。现在让我们对新部分进行样式设置。您可能希望在视觉上突出显示“更多”按钮。

.tabs {
  position: relative;
}
.tabs .-secondary {
  display: none;
  position: absolute;
  top: 100%;
  right: 0;
}
.tabs.--show-secondary .-secondary {
  display: block;
}

这是我们所处的状态

Container-Adapting Tabs With More Button

显然,我们需要一些隐藏和显示选项卡的代码……

隐藏和显示列表中的选项卡

由于 Flexbox,选项卡项永远不会换行,并且会缩小到其可能的最小宽度。这意味着我们可以逐个遍历每个项,将它们的宽度加起来,将其与.tabs元素的宽度进行比较,并相应地切换特定选项卡的可见性。为此,我们将创建一个名为doAdapt的函数,并将本节中下面的代码包装到该函数中。

首先,我们应该在视觉上显示所有项

allItems.forEach((item) => {
  item.classList.remove('--hidden')
})

顺便说一下,.--hidden按您预期的方式工作

.tabs .--hidden {
  display: none;
}

数学时间!如果您期待一些高级数学,我将不得不让您失望。因此,如前所述,我们通过在stopWidth变量下将每个选项卡的宽度加起来来遍历每个选项卡。我们还会检查该项是否适合容器,如果不适合,则隐藏该项,并将它的索引保存以备后用。

let stopWidth = moreBtn.offsetWidth
let hiddenItems = []
const primaryWidth = primary.offsetWidth
primaryItems.forEach((item, i) => {
  if(primaryWidth >= stopWidth + item.offsetWidth) {
    stopWidth += item.offsetWidth
  } else {
    item.classList.add('--hidden')
    hiddenItems.push(i)
  }
})

此后,我们需要隐藏次级列表中与列表中仍可见的项等效的项。以及在没有隐藏任何选项卡的情况下隐藏“更多”按钮。

if(!hiddenItems.length) {
  moreLi.classList.add('--hidden')
  container.classList.remove('--show-secondary')
  moreBtn.setAttribute('aria-expanded', false)
}
else {  
  secondaryItems.forEach((item, i) => {
    if(!hiddenItems.includes(i)) {
      item.classList.add('--hidden')
    }
  })
}

最后,确保doAdapt函数在正确的时间执行

doAdapt() // adapt immediately on load
window.addEventListener('resize', doAdapt) // adapt on window resize

理想情况下,调整大小事件处理程序应该防抖,以防止不必要的计算。

女士们,先生们,这是结果(玩玩调整演示窗口的大小)

查看 CodePen 上 Osvaldas ( @osvaldas ) 的 带有“更多”按钮的容器自适应选项卡

我可能在这里结束我的文章,但我们可以更进一步,使其变得更好,还有一些需要注意的事项……

增强功能

它已在上面的演示中实现,但我们没有概述一个小细节,它可以改善我们的选项卡小部件的 UX。它会自动隐藏下拉列表,如果用户点击列表外部的任何位置。为此,我们可以绑定一个全局点击监听器,并检查被点击的元素或其任何父元素是否为次级列表或“更多”按钮。如果不是,则下拉列表将被关闭。

document.addEventListener('click', (e) => {
  let el = e.target
  while(el) {
    if(el === secondary || el === moreBtn) {
      return;
    }
    el = el.parentNode
  }
  container.classList.remove('--show-secondary')
  moreBtn.setAttribute('aria-expanded', false)
})

边缘情况

长选项卡标题

您可能想知道该小部件在长选项卡标题的情况下如何表现。好吧,您至少有两个选择……

  1. 让标题换行,这就是它们默认的行为(您也可以使用word-wrap: break-word启用自动换行)
Container-Adapting Tabs With More Button
  1. 或者,您可以使用white-space: nowrap列表中禁用所有类型的换行。该脚本足够灵活,可以通过避开较短的兄弟节点,将过长的项放到下拉菜单中(在下拉菜单中,标题可以自由换行)
Container-Adapting Tabs With More Button

多个标签

即使辅助列表是position: absolute,也不影响文档的高度。 只要容器元素或其父元素不是position: fixed,文档就会适应,底部项目可以通过向下滚动页面访问。

需要注意的一件事

如果标签在语义上是按钮而不是锚点,那么事情可能会变得棘手,这意味着它们的点击响应由 JavaScript 决定,例如:动态标签。 这里的问题是标签按钮事件处理程序不会与标记一起复制。 我看到至少两种解决方法

  • 在自适应标签代码之后放置动态事件处理程序附件;
  • 改用事件委托方法(想想 jQuery 的live())。

不幸的是,事件数量很大:您的标签很可能有一个选中状态,它以视觉方式指示当前标签,因此同时管理这些状态也很重要。 否则,翻转平板电脑,你就迷路了。

浏览器兼容性

即使我在示例和演示中使用了 ES6 语法,也应该由编译器(例如 Babel)将其转换为 ES5,以显着扩大浏览器支持范围(包括 IE9)。

您还可以使用 较旧的版本和语法(一直到 IE10)扩展 Flexbox 实现。 如果您还需要支持非 Flexbox 浏览器,您可以始终使用 CSS @supports 进行特性检测,逐步应用该技术,并依赖于旧浏览器的水平滚动。

祝您标签愉快!