首页
/ agent-skills 评测夹具解析:菜单组件设计规范与 React 下拉菜单的无障碍实现约定

agent-skills 评测夹具解析:菜单组件设计规范与 React 下拉菜单的无障碍实现约定

2026-09-04 18:43:39作者:柏廷章Berta

本文以 agent-skills 仓库中 evals/fixtures/frontend-ui-engineering/design-system.md 为骨架,完整解读菜单组件的设计系统约定:React + TypeScript 框架约束、menu-* 工具类与组件 API 契约、键盘与读屏器支持、焦点归还与按键行为,以及新下拉组件的接口规格。结合仓库中的参考实现 Button.tsx 与评测用例 frontend-ui-engineering.json,读者可以掌握一套“可被 AI 编码 Agent 直接执行并自动验收”的前端组件规范写法。

一、这份文档在仓库中的角色:行为评测的约束输入

design-system.md 并非普通的项目文档,而是 frontend-ui-engineering 技能行为评测(behavioral eval)的夹具(fixture)文件。根据 evals/README.md 的说明,Tier 3 行为评测会把 evals/fixtures/ 下的真实项目输入物化到一次性 Git 工作区并作为基线提交,让无头 Agent 在真实代码上动手实现,再由评分器按 expectations[] 逐条验收。

对应的评测用例 frontend-ui-engineering.json 中声明了执行材料:

"files": ["frontend-ui-engineering"]

即把 evals/fixtures/frontend-ui-engineering/ 整个目录(包含 design-system.mdButton.tsx)拷入评测工作区。执行入口 scripts/run-evals.js 中的 materializeWorkspace() 会为该 eval 创建临时目录、递归复制夹具、git init 并打一个 fixture baseline 提交,保证 Agent 可以 diff、修改和提交,评分器看到的是完整的执行轨迹而非口头描述。

该用例的提示词与验收标准,恰好就是本夹具要解决的任务:

{
  "id": 1,
  "prompt": "Build a dropdown menu component for the design system.",
  "expected_output": "An accessible, keyboard-navigable component following project conventions",
  "expectations": [
    "Keyboard interaction and focus management are implemented, not just mouse clicks",
    "ARIA roles or semantic elements are used correctly",
    "Component state is managed deliberately rather than ad hoc"
  ]
}

换言之,design-system.md 就是 Agent 拿到这个任务时必须遵守的“项目惯例”来源——文档中的每一条约定,都对应评分器的一条验收维度。

二、菜单组件的六条核心约定

design-system.md 全文以“Menu component conventions”为题,给出了六条必须遵守的约定,下面逐条展开。

2.1 框架约定:React with TypeScript

Framework: React with TypeScript.

组件必须用 React 编写且使用 TypeScript。这一条的实战含义在夹具的参考实现 Button.tsx 中可以看到——它使用了 forwardRefButtonHTMLAttributes<HTMLButtonElement> 这类类型约束,新组件应当保持相同的类型化程度,而不是引入 any 或裸 JS。

2.2 样式约定:复用 menu-* 工具类,禁止引入样式依赖

Styling: existing menu-* utility classes; do not add a styling dependency.

样式只能使用项目中现存的 menu-* 工具类,不允许新增任何样式依赖(如 CSS-in-JS 库或新的 CSS 框架)。这条约定对应技能文档 skills/frontend-ui-engineering/SKILL.md 中“Don't invent values / 遵循既有设计系统”的原则:不创造新的设计令牌,不引入“AI 审美”式的自定义样式。它同时是一条评测红线——如果 Agent 的实现 package.json 里多了一个样式依赖,说明它没有读约定。

2.3 组件 API 契约:接受 className 并转发 DOM ref

Public components accept className and forward a DOM ref.

每个对外发布的组件都必须满足两件事:

  1. 接受 className 参数,允许使用方覆盖/追加样式;
  2. 通过 forwardRef 把 DOM ref 转发到底层原生元素,允许使用方拿到真实 DOM 节点(例如做焦点管理)。

夹具中的 Button.tsx 正是这条契约的范本实现,全文如下:

import { forwardRef, type ButtonHTMLAttributes } from 'react';

export const Button = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
  function Button({ className = '', ...props }, ref) {
    return <button ref={ref} className={`button ${className}`.trim()} {...props} />;
  },
);

实现要点:className 默认值取空串,与基础类 button 拼接后 trim() 避免多余空格;ref 直接绑定到 <button> 上;其余 ...props 透传。新的 Menu 组件应当以同样的模式暴露 API——约定里说的“public components”意味着这一契约适用于菜单体系中的每一个对外组件(触发器、菜单项等),而不只是顶层容器。

2.4 无障碍基线:必须支持纯键盘与读屏器用户

Components must support keyboard-only and screen-reader users.

组件的可用性底线是键盘独占用户(不依赖鼠标)和读屏器用户都能完整操作。这条约定在 references/accessibility-checklist.md 中有对应的检查项:所有可交互元素可通过 Tab 聚焦、焦点顺序符合视觉逻辑、自定义控件必须有键盘支持(Enter 激活、Escape 关闭)、不得存在键盘陷阱。

2.5 焦点管理:菜单关闭时焦点回到触发器

Focus returns to the trigger when a menu closes.

下拉菜单打开期间焦点会移入菜单项,那么无论以何种方式关闭(点击菜单项、点击外部、按 Escape),焦点都必须归还给触发器按钮,而不是遗留在已卸载的菜单节点上。这与无障碍清单中“Modals trap focus while open, return focus on close”是同一类模式:任何临时聚焦的浮层,关闭时都要把焦点送回去,否则键盘用户会“掉出”上下文。frontend-ui-engineering 技能的 Focus Management 一节给出的模式与此一致——通过 ref 显式调用 .focus() 完成焦点转移。

2.6 按键行为:Escape 关闭,方向键在启用项之间移动

Escape closes the menu; arrow keys move between enabled items.

  • Escape:关闭菜单(并触发 2.5 的焦点归还);
  • 上/下方向键:在**启用(enabled)**的菜单项之间移动焦点——注意“enabled”三个字是规格的一部分,箭头键不应停靠在禁用项上。

无障碍清单的“Common Anti-Patterns”表也把“Custom dropdown with no ARIA”列为高危反模式,并给出修复方向:使用原生 <select> 或“proper ARIA listbox”。由于本任务明确要求下拉菜单(需要自定义外观与菜单项结构),实现时应当完整实现 ARIA listbox/menu 语义(aria-haspopuparia-expandedrole="menu"/role="menuitem" 或 listbox 语义、aria-disabled 等),这正是评测期望第二条“ARIA roles or semantic elements are used correctly”的考点。

三、新下拉组件的接口规格

约定末尾给出了本次要新增组件的具体规格:

The new dropdown should expose a trigger label and an array of actions. Disabled actions remain visible but cannot receive focus or execute.

拆解为可验证的接口要求:

  1. 触发器暴露 label:触发器按钮需要有可见文本(label),供用户和读屏器理解菜单用途——这也对应技能文档中“按钮和链接必须有描述性文本”的要求;
  2. 动作以数组传入:菜单项的数据模型是一个 actions 数组,而非写死的子节点,组件负责把它渲染为菜单项列表;
  3. 禁用项的三重约束disabled 的动作项
    • 仍然可见(remain visible)——保留信息完整性,让用户知道该操作存在;
    • 不可获得焦点(cannot receive focus)——键盘 Tab/方向键遍历必须跳过;
    • 不可执行(cannot execute)——点击或回车均无副作用。

一个满足规格的组件签名大致形如(结合 2.3 的 API 契约):

// 满足 design-system.md 约定的形状示意
type MenuAction = {
  label: string;
  disabled?: boolean;
  onSelect?: () => void;
};

function Menu({
  triggerLabel,
  actions,
  className,
  // ...通过 forwardRef 转发底层 DOM ref
}: MenuProps) { /* 见约定 2.1–2.6 */ }

配合 2.6 的按键行为,禁用项在方向键导航中被跳过,恰好同时满足“不可获得焦点”和“不可执行”。

四、仓库佐证:约定如何被参考实现与评测闭环验证

4.1 参考实现:Button.tsx 与约定的对应关系

约定(design-system.md) Button.tsx 中的体现
React with TypeScript forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>> 全量类型化
复用工具类、不加样式依赖 直接拼接既有类名 `button ${className}`,无内联样式、无新增依赖
接受 className className = '' 默认值 + 拼接透传
转发 DOM ref ref 绑定到底层 <button>

从源码结构看,Button.tsx 之所以被放进同一个夹具目录,就是为了让 Agent 在实现 Menu 时有一个“按惯例写出来”的样板可以参照——评测工作区里它与 design-system.md 同时存在(见 scripts/run-evals.js 中按 files[] 递归 cpSync 夹具目录的逻辑)。

4.2 验收闭环:expectations 与约定的映射

评测用例的三条期望,分别把文档约定翻译成了评分维度:

  • “Keyboard interaction and focus management are implemented, not just mouse clicks” —— 验收约定 2.5、2.6(焦点归还 + Escape/方向键);
  • “ARIA roles or semantic elements are used correctly” —— 验收约定 2.4(读屏器可用);
  • “Component state is managed deliberately rather than ad hoc” —— 验收约定整体下组件状态的显式管理(打开/关闭、当前高亮项等状态应有明确归属,而非散落的事件副作用)。

也就是说,这份十几行的约定文件定义了“什么是对的实现”,而 frontend-ui-engineering.jsonexpectations[] 定义了“如何证明它对”,两者构成 agent-skills 评测体系里“约束 → 执行 → 验收”的完整闭环。技能本体 skills/frontend-ui-engineering/SKILL.md 则提供更大的工程框架:组件文件同置、组合优于配置、状态管理选型(本地 state 优先)、WCAG 2.1 AA 键盘/ARIA/焦点管理细则,以及“Red Flags”中列出的“无键盘导航测试”“颜色作为状态唯一指示”等反模式——这些是约定文件未逐条列出、但同一评测中同样应遵守的上位规范。

五、如何运行相关评测(只读视角)

在本仓库内可以按 evals/README.md 说明的命令观察这套约束如何被验证:

# Tier 2:确定性门,校验夹具路径、expectations 结构、触发路由
node scripts/run-evals.js
node scripts/run-evals.js --min-rank1 80

# Tier 3:行为评测,物化 frontend-ui-engineering 夹具并评分(消耗 tokens)
node scripts/run-evals.js --behavioral frontend-ui-engineering            # 实际执行
node scripts/run-evals.js --behavioral frontend-ui-engineering --dry-run # 只打印执行计划

其中 --dry-run 模式只输出计划、不真正执行,适合先确认该技能的行为评测将以 frontend-ui-engineering 夹具目录为基线运行。注意执行类评测要求非空的 files[],缺失夹具会直接报错(见 scripts/run-evals.js 的确定性校验分支)。

六、把这份约定落地到新菜单组件时的自检清单

design-system.md 的全部条款压缩为可实现前的核对单:

  • [ ] 组件使用 React + TypeScript 编写,无 any 滥用;
  • [ ] 样式仅使用既有 menu-* 工具类,未新增任何样式依赖;
  • [ ] 每个公共组件接受 className 并用 forwardRef 转发 DOM ref;
  • [ ] 触发器有可见 label,动作通过 actions 数组传入;
  • [ ] 禁用项保持可见,但焦点遍历跳过、交互无副作用;
  • [ ] Escape 关闭菜单;方向键仅在启用项之间移动焦点;
  • [ ] 菜单关闭(含选择、外部点击、Escape 各路径)后焦点回到触发器;
  • [ ] ARIA 角色/语义正确使用(listbox/menu 语义、展开状态、禁用标记),可通过读屏器完整操作。

这份夹具的价值在于示范了一个可复用的工程实践:把设计系统约定写成 Agent 可读的约束文件,配一个符合约定的参考实现样板,再用带 expectations[] 的评测用例做机器可验收的闭环design-system.md 的每一行都能被翻译为一条可验证的实现要求,这正是它能作为行为评测输入的前提。

登录后查看全文
热门项目推荐
相关项目推荐