☰
Oat UI Dropdown 组件完全指南:基于原生 Popover API 的无依赖菜单与弹层
2026/10/12 1:51:55 网站建设 项目流程

【免费下载链接】oat

Ultra-lightweight, zero dependency, semantic HTML, CSS, JS UI library. ~10KB min+gz.

项目地址:https://gitcode.com/gh_mirrors/oat5/oat
点击查看免费下载

Oat(oat)是一套超轻量、零依赖的语义化 HTML/CSS/JS UI 库,而<ot-dropdown>是其中少数由 Web Component 驱动的交互组件之一。它完全建立在浏览器原生 Popover API 之上,用最少的标记实现菜单(Menu)与弹层(Popover)两种形态,并自带视口翻转定位、键盘导航与 ARIA 状态管理。读完本文,你将掌握在 Oat 中编写可用的下拉菜单与确认弹层的完整写法,理解其底层定位、焦点与事件机制的源码实现,并能将其与按钮组等组件组合成 Split Button 等实战控件。

组件概览:语义标记 + 原生 Popover API

<ot-dropdown>的设计思路非常直接:触发器(trigger)与目标(target)之间的开合关系完全交给浏览器原生的 Popover API,Web Component 只负责定位、键盘导航和可访问性状态。

官方文档(dropdown.md)给出了三条核心规则:

  1. 用<ot-dropdown>包裹整个下拉区域;
  2. 在触发器元素上声明popovertarget,在目标元素上声明popover;
  3. 如果下拉内容是一个<menu>,其中的每一项使用role="menuitem"。

也就是说,你写的仍然是标准 HTML——popovertarget、popover、role="menuitem"都是原生属性,组件本身不引入任何自定义配置项或命令式 API。

在使用组件前,需要先引入 Oat 的基础文件。按 usage.md 的说明,你可以通过 CDN 直接引入:

<link rel="stylesheet" href="https://unpkg.com/@knadh/oat/oat.min.css"> <script src="https://unpkg.com/@knadh/oat/oat.min.js" defer></script>

也可以按需只引入 Dropdown 所依赖的最小文件集(参见 customizing.md 的“Picking and choosing”):

  • 00-base.css(基础样式层,其中包含对[popover]顶层元素的字体与颜色声明)
  • 01-theme.css(全部 CSS 变量)
  • base.js(OtBase基类)
  • dropdown.js(本组件实现,或在完整入口 index.js 中一并引入)

组件在 dropdown.js 末尾通过customElements.define('ot-dropdown', OtDropdown)注册,引入后即可直接使用。

基础用法:菜单型 Dropdown

官方文档提供了第一个完整示例——一个包含常规操作、危险操作与链接项的“Options”菜单。下面直接继承并展开这段代码:

<ot-dropdown> <button popovertarget="demo-menu" class="outline"> Options <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg> </button> <menu popover id="demo-menu"> <button role="menuitem" class="ghost">Profile</button> <button role="menuitem" class="ghost" popovertarget="demo-menu" popovertargetaction="hide">Click to close</button> <button role="menuitem" class="ghost">Help</button> <a href="#" role="menuitem" class="unstyled">Link</a> <hr> <button role="menuitem" class="ghost">Logout</button> <button role="menuitem"><ot-dropdown> <button popovertarget="demo-confirm" class="outline"> Confirm </button> <article class="card" popover id="demo-confirm"> <header> <h4>Are you sure?</h4> <p>This action cannot be undone.</p> </header> <br /> <footer> <button class="outline small" popovertarget="demo-confirm">Cancel</button> <button>init() { this.#menu = this.querySelector('[popover]'); this.#trigger = this.querySelector('[popovertarget]'); if (!this.#menu || !this.#trigger) return; this.#menu.addEventListener('toggle', this); this.#menu.addEventListener('keydown', this); ... }

init()的调用时机由基类 base.js 保证:connectedCallback()会等待DOMContentLoaded(若文档仍在加载)后再执行且只执行一次#setup()→init()。事件监听全部以this(元素本身)作为处理器传入,OtBase.handleEvent()会把事件按类型分发到onclick、onkeydown、ontoggle等以on${event.type}命名的实例方法上——这是整个 Oat 组件体系的统一事件模式。

手动定位与视口翻转

popover定位行为类似position: fixed,相对视口而言,因此组件必须在打开时手动计算坐标,源码注释直言这一点:

this.#position = () => { const r = this.#trigger.getBoundingClientRect(); const m = this.#menu.getBoundingClientRect(); // Flip if menu overflows viewport. this.#menu.style.top = `${r.bottom + m.height > window.innerHeight ? r.top - m.height : r.bottom}px`; this.#menu.style.left = `${r.left + m.width > window.innerWidth ? r.right - m.width : r.left}px`; };

定位逻辑非常朴素但有效:

  • 默认把菜单锚定在触发器下方(top = r.bottom)、左侧对齐(left = r.left);
  • 若下方空间不足(r.bottom + m.height > window.innerHeight),翻转到触发器上方(top = r.top - m.height);
  • 若右侧溢出视口,则改为右对齐(left = r.right - m.width)。

作为配套,dropdown.css 把[popover]声明为position: fixed; margin: 0; min-width: 12rem;,确保计算坐标时不受默认边距干扰。

开合状态、焦点与滚动监听

ontoggle(e)是组件的状态中枢,根据toggle事件的newState分流:

ontoggle(e) { if (e.newState === 'open') { this.#position(); window.addEventListener('scroll', this.#position, true); window.addEventListener('resize', this.#position); this.#items = [...this.querySelectorAll('[role="menuitem"]')]; this.#items[0]?.focus(); this.#trigger.ariaExpanded = 'true'; } else { this.cleanup(); this.#items = null; this.#trigger.ariaExpanded = 'false'; this.#trigger.focus(); } }

打开时:

  • 立即计算并应用位置;
  • 在 window 上注册scroll(捕获阶段,true,以便捕获任意容器内的滚动)与resize监听,菜单保持跟随触发器;
  • 收集所有[role="menuitem"]并聚焦第一项;
  • 把触发器的aria-expanded置为"true",向辅助技术暴露展开状态。

关闭时:移除滚动/缩放监听(cleanup(),同时该方法在元素被移出 DOM 时由OtBase.disconnectedCallback()兜底调用)、清空菜单项集合、aria-expanded置为"false",并把焦点归还给触发器按钮——这保证了键盘用户的操作流不被打断。

键盘循环导航

onkeydown(e)先过滤非菜单项的按键,再把事件交给基类的keyNav计算下一个目标索引:

onkeydown(e) { if (!e.target.matches('[role="menuitem"]')) return; const idx = this.#items.indexOf(e.target); const next = this.keyNav(e, idx, this.#items.length, 'ArrowUp', 'ArrowDown', true); if (next >= 0) this.#items[next].focus(); }

keyNav(base.js)实现了标准的“roving”循环导航:

  • ArrowDown→ 索引(idx + 1) % len;
  • ArrowUp→ 索引(idx - 1 + len) % len(支持环绕);
  • Home/End→ 跳到首项 / 末项(homeEnd = true时启用);
  • 命中任一按键都会preventDefault(),避免页面滚动。

这套模式与 tabs.js 的ArrowLeft/ArrowRight导航同源,是整个 Oat 键盘可访问性的通用底座。

样式与主题:dropdown.css 的动画细节

dropdown.css 位于@layer components层,样式全部基于主题 CSS 变量,改动外观无需碰组件源码:

ot-dropdown { [popover] { position: fixed; margin: 0; min-width: 12rem; background-color: var(--background); border: 1px solid var(--border); border-radius: var(--radius-medium); box-shadow: var(--shadow-small); opacity: 0; transform: translateY(-4px); transition: opacity 150ms ease-out, transform 150ms ease-out, display 150ms allow-discrete, overlay 150ms allow-discrete; &:popover-open { opacity: 1; transform: translateY(0); } @starting-style { &:popover-open { opacity: 0; transform: translateY(-4px); } } ... } }

值得注意的现代 CSS 特性:

  • @starting-style:为进入动画定义起始状态,使菜单打开时从透明、上移 4px 平滑过渡到不透明、归位;
  • allow-discrete:允许display与overlay(顶层渲染)这类离散属性参与过渡,配合:popover-open伪类实现真正可动画的开合;
  • prefers-reduced-motion:全局动画缩减由 animations.css 统一处理,会把过渡时长压到 0.01ms,尊重用户的系统设置。

菜单项样式同样集中在组件层:

[role="menuitem"] { display: flex; width: 100%; padding: var(--space-2) var(--space-3); justify-content: start; cursor: pointer; &:hover, &:focus { background-color: var(--accent); outline: none; } }

菜单项全宽、左对齐,悬停/聚焦时以--accent高亮;outline: none由组件接管视觉焦点反馈(配合00-base.css中:focus-visible的--ring描边规则)。

若要调整下拉的圆角、阴影、间距或高亮色,只需在引入 Oat 之后覆盖对应变量,例如:

:root { --radius-medium: 0.5rem; --shadow-small: 0 4px 12px rgb(0 0 0 / 0.08); --accent: #eef2ff; }

完整变量清单见 customizing.md 与 01-theme.css。

实战组合:Split Button 与真实站点用法

<ot-dropdown>的价值更多体现在与其他 Oat 组件的组合上。

Split Button(分割按钮)

recipes.md 给出了一个典型配方:用menu.buttons(Oat 的连接按钮组,见 button.css)承载主动作,用ot-dropdown承载次级动作:

<ot-dropdown> <menu class="buttons"> <li><button class="outline">Save</button></li> <li> <button class="outline" popovertarget="save-actions" aria-label="More save actions"> More <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg> </button> </li> </menu> <menu popover id="save-actions"> <button role="menuitem" class="ghost">Save draft</button> <button role="menuitem" class="ghost">Save and publish</button> <button role="menuitem" class="ghost">Duplicate</button> </menu> </ot-dropdown>

要点:popovertarget声明在menu.buttons内部的按钮上,而ot-dropdown的querySelector('[popovertarget]')会命中它;menu同时复用“连接按钮组”与“下拉菜单容器”两种角色,标记量极小。

官方文档站自身的导航菜单

base.html 中,Oat 文档站的顶部导航“More”就是一个真实的<ot-dropdown>实例——包含指向 Usage、Customizing、Other libs、Recipes、Extensions 的链接项,并用<hr>分组。这证明组件可以直接服务于站点级导航,是经过实际使用的模式。

无障碍与兼容性注意

  • 语义与状态:role="menuitem"、触发器aria-expanded的动态同步、打开时聚焦首项、关闭后焦点归还触发器,构成了完整的菜单式 ARIA 交互(dropdown.js)。
  • 原生能力优先:开合用popover/popovertarget/popovertargetaction原生属性,关闭由浏览器 light-dismiss(点击外部、Esc)兜底,代码量被压到极低。
  • 浏览器前提:组件依赖浏览器原生 Popover API([popover]属性、toggle事件),因此需要较新的浏览器版本。Oat 在 base.js 中为 Safari 的commandfor/command提供了事件委托式 polyfill,并在 tooltip.js 中用'showPopover' in HTMLElement.prototype做特性检测;而dropdown.js未做特性检测,直接假定 Popover API 可用——部署前请确认目标环境支持(主流 Chromium 与 Firefox 已支持,Safari 支持情况请以目标版本实测为准)。
  • 嵌套定位:由于定位基于getBoundingClientRect()且监听scroll/resize,当触发器位于可滚动容器内时,菜单会在捕获阶段随滚动实时重定位,一般无需额外处理。

小结

<ot-dropdown>是 Oat“少即是多”理念的典型代表:交互状态交给原生 Popover API,Web Component 只做三件事——视口内翻转定位、键盘循环导航、ARIA 状态管理,全部逻辑集中在 dropdown.js 七十余行之内。菜单与确认弹层两种形态共用同一组件,配合按钮变体(outline/ghost/danger)与主题变量即可快速产出可用、可访问、零依赖的下拉交互。

【免费下载链接】oat

Ultra-lightweight, zero dependency, semantic HTML, CSS, JS UI library. ~10KB min+gz.

项目地址:https://gitcode.com/gh_mirrors/oat5/oat
点击查看免费下载
上一篇:零代码实现专业级图像修复:Resynthesizer插件跨平台安装指南
下一篇:SDR++完全指南:开源软件定义无线电的极致体验

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询