如何实现类似于 youtube 卡片的上下文菜单?

BPL*_*BPL 6 html javascript css

我正在学习一些 CSS,今天我发现自己想要实现一个类似的上下文菜单(当您点击 youtube 卡片上的垂直省略号时显示的菜单),但我的尝试没有成功很远:D。这是我所拥有的:

<!DOCTYPE html>
<html>

<head>
<style type="text/css">
.container {
    display: inline-block;
    position: relative;
}

.dropdown {
    position: absolute;
    right: 1rem;
    top: 1rem;
}

.dropdown-opener {
    cursor: pointer;
    user-select: none;
    position: absolute;
    right: 0;
}

.dropdown .dropdown-toggle,
.dropdown .dropdown-menu {
    display: none;
    style-type: none;
}

.dropdown .dropdown-toggle:checked+ul {
    display: block;
}

.style-scope .menu-renderer {
    --layout-inline_-_display: inline-flex;
    --icon-button-icon-height: 24px;
    --icon-button-icon-width: 24px;
    --spec-icon-active-other: #606060;
    --spec-icon-inactive: #909090;
    --spec-text-disabled: #909090;
    --spec-text-secondary: #606060;
    align-items: var(--layout-center-center_-_align-items);
    color: var(--menu-renderer-button-color, var(--spec-icon-inactive));
    cursor: pointer;
    display: var(--layout-inline_-_display);
    fill: var(--iron-icon-fill-color, currentcolor);
    width: var(--icon-button-icon-width, 100%);
    background: transparent;
}
</style>
</head>

<body>
<div class="container">
    <img alt="sample" src="https://via.placeholder.com/200x200">
    <nav class="dropdown layer--topright">
        <label class="dropdown-opener" for="menu-opener1">...</label>
        <input class="dropdown-toggle" id="menu-opener1" type="checkbox">
        <ul class="dropdown-menu">
            <li>Foo1</li>
            <li>Bar1</li>
            <li>Baz1</li>
        </ul>
    </nav>
</div>
<div class="container">
    <img alt="sample" src="https://via.placeholder.com/200x200">
    <nav class="dropdown layer--topright">
        <label class="dropdown-opener" for="menu-opener1">...</label>
        <input class="dropdown-toggle" id="menu-opener1" type="checkbox">
        <ul class="dropdown-menu">
            <li>Foo2</li>
            <li>Bar2</li>
            <li>Baz2</li>
        </ul>
    </nav>
</div>
<div class="container">
    <img alt="sample" src="https://via.placeholder.com/200x200">
    <nav class="dropdown layer--topright">
        <icon-button id="button" class="dropdown-opener dropdown-trigger style-scope menu-renderer">
            <button id="button" class="style-scope icon-button" aria-label="Action menu">
                <icon class="style-scope menu-renderer">
                    <svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope icon" style="pointer-events: none; display: block; width: 100%; height: 100%;">
                        <g class="style-scope icon">
                            <path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" class="style-scope icon"></path>
                        </g>
                    </svg>
                </icon>
            </button>
        </icon-button>
        <input class="dropdown-toggle" id="menu-opener2" type="checkbox">
        <ul class="dropdown-menu">
            <li>Foo3</li>
            <li>Bar3</li>
            <li>Baz3</li>
        </ul>
    </nav>
</div>
</body>

</html>
Run Code Online (Sandbox Code Playgroud)

如您所见,代码很脏且损坏……但是我的尝试存在重大问题,我不知道如何解决:

  1. 单击省略号按钮时出现错位的上下文菜单
  2. 显示项目符号的项目
  3. 与 youtube 的非常干净的项目相比,项目的样式很丑陋

因此,如果您能解释如何解决这些问题,以便从我的尝试中获得一些有用的东西,那就太棒了。

或者,如果我的方法完全错误,无法使用并准备扔进垃圾箱……您能解释一下实现我的目标的最佳方法吗?

提前致谢。

Ric*_*ard 9

问题 #1

单击溢出菜单按钮时错位的上下文菜单。上下文菜单实际上并没有放错位置。元素中的for属性<label>引用了错误的上下文菜单。例如,for你的价值<label>元素在第一.container具有的价值menu-opener1,但for在价值<label>的元素第二个 .container具有完全相同的值。单击任一标签都会打开第一个容器中的下拉菜单,因为这两个标签都会导致选中第一个容器上的隐藏复选框。

我们可以做什么?只需更改该id值,以便每个下拉菜单都有一个唯一的 id 值。然后,将该 id 值用于元素for内的值<label>


问题#2

要将li元素的公告隐藏在 a 中ul,您必须list-style-type: none在 CSS上使用而不是 style-type: none.


问题 #3

这是一个非常主观的问题。一种设计在一个人看来很干净,但在其他人看来却是不干净的。尽管如此,我试图实现我想看到的。以下是您可以更改以改进设计方面的一些内容。

  • 更改您的字体系列,使其适合您的主题。我在这里选择了 sans-serif 字体系列。
  • 为上下文菜单添加背景。在这里,我选择了白色。
  • 在每个li元素之间添加空格。在这里,我使用了line-height. 您还可以在每个元素上使用padding或。marginli
  • 添加一个box-shadow以显示高程。Google 的 Material Design 建议使用这种技术来显示元素的 z 位置更高。
  • 允许上下文菜单从可见变为不可见,反之亦然。这意味着避免使用不可转换的 CSS 属性(例如visibilitydisplay)。在这里,我选择了使用transform: scaleopacity过渡。

其他问题

从语义上讲,您的 HTML 标记不正确。

  • <nav>(导航)元素用于页面间导航。在这里,您可能应该使用<menu>element 代替。但是,由于它仍处于实验阶段,我选择使用<section>.
  • 您正在使用一些不存在的 HTML 标签(例如<icon-button><icon>)。尝试在此处咨询有效的 HTML 标签在此处咨询有效的 SVG 标签
  • 最后.container一项有一个按钮,可以检查隐藏的复选框。但是,<button>元素不适用于<label>element。所以,试着让<label>元素在视觉上看起来像一个按钮。您可以使用:active:hoverCSS 伪选择器分别在按下和悬停按钮时更改按钮样式。此外,这减少了嵌套。
  • 当可以使用 CSS 进行样式设置时,尽量避免使用内联样式(例如您的内联 SVG 样式)
  • 个人喜好。大多数框架使用.container包含整个页面,所以我选择使用类名.box而不是.container.

这是可运行的代码段。

* {
  font-family: Helvetica;
  box-sizing: border-box;
}

.box {
  display: inline-block;
  position: relative;
}

.dropdown {
  position: absolute;
  right: 0;
  top: 8px;
}

.dropdown-opener {
  cursor: pointer;
  user-select: none;
  position: absolute;
  width: 24px;
  height: 24px;
  background: url('https://i.imgur.com/Qt3Qwgp.png');
  background-repeat: no-repeat;
  background-position: right;
  right: 0;
}

.dropdown .dropdown-toggle {
  display: none;
}

.dropdown .dropdown-menu {
  list-style-type: none;
  transform: scale(0);
  opacity: 0;
  transition:
    transform 0.25s ease,
    opacity 0.25s ease;
  
  position: absolute;
  top: 1.5em;
  right: 10px;
  line-height: 1.75em;
  background: white;
  box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.5);
  border-radius: 5px;
  padding: 20px;
  margin: 0;
  transform-origin: top right;
}

.dropdown .dropdown-toggle:checked + ul {
  transform: scale(1);
  opacity: 1;
}

.dropdown-opener-button {
  position: absolute;
  width: 24px;
  height: 24px;
  right: 8px;
  top: 0;
}

.icon-button {
  padding: 0;
  border: 0;
  border-radius: 5px;
  cursor: pointer;
  transition: 
    box-shadow .25s ease,
    background .25s ease,
    transform .25s ease;
  background: #ffffffdd;
}

.icon-button:hover {
  box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.35);
}

.icon-button:active {
  background: #ffffff77;
  transform: scale(0.9);
}

.icon-button ~ .dropdown-menu {
  top: 1.75em;
}
Run Code Online (Sandbox Code Playgroud)
<div class="box">
  <img alt="sample" src="https://via.placeholder.com/200x200">
  <section class="dropdown layer--topright">
    <label class="dropdown-opener" for="menu-opener1"></label>
    <input class="dropdown-toggle" id="menu-opener1" type="checkbox">
    <ul class="dropdown-menu">
      <li>Foo1</li>
      <li>Bar1</li>
      <li>Baz1</li>
    </ul>
  </section>
</div>
<div class="box">
  <img alt="sample" src="https://via.placeholder.com/200x200">
  <section class="dropdown layer--topright">
    <label class="dropdown-opener" for="menu-opener2"></label>
    <input class="dropdown-toggle" id="menu-opener2" type="checkbox">
    <ul class="dropdown-menu">
      <li>Foo2</li>
      <li>Bar2</li>
      <li>Baz2</li>
    </ul>
  </section>
</div>
<div class="box">
  <img alt="sample" src="https://via.placeholder.com/200x200">
  <section class="dropdown layer--topright">
    <label class="dropdown-opener-button icon-button" for="menu-opener3">
      <svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope icon">
        <g class="style-scope icon">
          <path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" class="style-scope icon"></path>
        </g>
      </svg>
    </label>
    <input class="dropdown-toggle" id="menu-opener3" type="checkbox">
    <ul class="dropdown-menu">
      <li>Foo3</li>
      <li>Bar3</li>
      <li>Baz3</li>
    </ul>
  </section>
</div>
Run Code Online (Sandbox Code Playgroud)


更新

对 OP 的含义没有清晰的理解,即在用鼠标悬停时使菜单项更改状态的难度多大,我决定在悬停时创建一个效果,并在单击时创建一个不同的效果(涟漪)该<li>元素。我推荐阅读这篇关于创建涟漪效应的文章

此外,根据要求,添加了在菜单框外单击时隐藏菜单的功能。这是可运行的代码段。

// Closing menu on outside click
const outsideClickListener = event => {
  let checkedToggle = document.querySelector('.dropdown-toggle:checked')
  let openedMenu = document.querySelector('.dropdown-toggle:checked + .dropdown-menu')
  
  // If click is performed on checkbox (through label), do nothing
  if (event.target.classList.contains('dropdown-toggle')) {
    return
  }
  
  // If click is performed on label, uncheck all other dropdown-toggle
  if (event.target.classList.contains('dropdown-opener') ||
      event.target.classList.contains('dropdown-opener-button')) {
    let forId = event.target.getAttribute('for')
    document.querySelectorAll('.dropdown-toggle').forEach(toggle => {
      if (forId !== toggle.getAttribute('id'))
        toggle.checked = false
    })
    return
  }
  
  // If click is performed outside opened menu
  if (openedMenu && !openedMenu.contains(event.target)) {
    checkedToggle.checked = false
  }
}
document.addEventListener('click', outsideClickListener)

// Ripple effect on li elements
const createRipple = event => {
  let li = event.target
  let liBox = li.getBoundingClientRect()
  let x = event.pageX - liBox.left
  let y = event.pageY - liBox.top
  let animDuration = 350
  let animationStart, animationFrame
  
  let animationStep = timestamp => {
    if (!animationStart) animationStart = timestamp
    let frame = timestamp - animationStart
    if (frame < animDuration) {
      let easing = (frame / animDuration) * (2 - (frame / animDuration))
      let circle = `circle at ${x}px ${y}px`
      let color = `rgba(0, 0, 0, ${0.2 * (1 - easing)})`
      let stop = `${100 * easing}%`
      li.style.backgroundImage = `radial-gradient(${circle}, ${color} ${stop}, transparent ${stop})`
      animationFrame = window.requestAnimationFrame(animationStep)
    }
    else {
      li.style.backgroundImage = ''
      window.cancelAnimationFrame(animationStep)
    }
  }
  
  animationFrame = window.requestAnimationFrame(animationStep)
}
const listItems = document.querySelectorAll('li')
listItems.forEach(li => {
  li.addEventListener('click', createRipple)
})
Run Code Online (Sandbox Code Playgroud)
* {
  font-family: Helvetica;
  box-sizing: border-box;
}

.box {
  display: inline-block;
  position: relative;
}

.dropdown {
  position: absolute;
  right: 0;
  top: 8px;
}

.dropdown-opener {
  cursor: pointer;
  user-select: none;
  position: absolute;
  width: 24px;
  height: 24px;
  background: url('https://i.imgur.com/Qt3Qwgp.png');
  background-repeat: no-repeat;
  background-position: right;
  right: 0;
}

.dropdown .dropdown-toggle {
  display: none;
}

.dropdown .dropdown-menu {
  list-style-type: none;
  transform: scale(0);
  opacity: 0;
  transition:
    transform 0.25s ease,
    opacity 0.25s ease;
  
  position: absolute;
  top: 1.5em;
  right: 10px;
  background: white;
  box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.5);
  border-radius: 5px;
  margin: 0;
  transform-origin: top right;
  padding: 7.5px 0 7.5px 0;
}

.dropdown .dropdown-toggle:checked + ul {
  transform: scale(1);
  opacity: 1;
}

.dropdown-menu li {
  padding: 7.5px;
  padding-left: 25px;
  cursor: pointer;
  transition: background .15s ease;
}

.dropdown-menu li:hover {
  background: #00000012;
}

.dropdown-opener-button {
  position: absolute;
  width: 24px;
  height: 24px;
  right: 8px;
  top: 0;
}

.dropdown-opener-button svg {
  pointer-events: none;
}

.icon-button {
  padding: 0;
  border: 0;
  border-radius: 5px;
  cursor: pointer;
  transition: 
    box-shadow .25s ease,
    background .25s ease,
    transform .25s ease;
  background: #ffffffdd;
}

.icon-button:hover {
  box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.35);
}

.icon-button:active {
  background: #ffffffaa;
  box-shadow: 0px 0px 4px 0px rgba(0,0,0,0.35);
  transform: scale(0.9);
}

.icon-button ~ .dropdown-menu {
  top: 1.75em;
}
Run Code Online (Sandbox Code Playgroud)
<div class="box">
  <img alt="sample" src="https://via.placeholder.com/200x200">
  <section class="dropdown layer--topright">
    <label class="dropdown-opener" for="menu-opener1"></label>
    <input class="dropdown-toggle" id="menu-opener1" type="checkbox">
    <ul class="dropdown-menu">
      <li>Foo1</li>
      <li>Bar1</li>
      <li>Baz1</li>
    </ul>
  </section>
</div>
<div class="box">
  <img alt="sample" src="https://via.placeholder.com/200x200">
  <section class="dropdown layer--topright">
    <label class="dropdown-opener" for="menu-opener2"></label>
    <input class="dropdown-toggle" id="menu-opener2" type="checkbox">
    <ul class="dropdown-menu">
      <li>Foo2</li>
      <li>Bar2</li>
      <li>Baz2</li>
    </ul>
  </section>
</div>
<div class="box">
  <img alt="sample" src="https://via.placeholder.com/200x200">
  <section class="dropdown layer--topright">
    <label class="dropdown-opener-button icon-button" for="menu-opener3">
      <svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope icon">
        <g class="style-scope icon">
          <path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" class="style-scope icon"></path>
        </g>
      </svg>
    </label>
    <input class="dropdown-toggle" id="menu-opener3" type="checkbox">
    <ul class="dropdown-menu">
      <li>Foo3</li>
      <li>Bar3</li>
      <li>Baz3</li>
    </ul>
  </section>
</div>
Run Code Online (Sandbox Code Playgroud)