From d9feeda0fd883777d4d5f812337cd4ab078726d1 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 11:38:44 +0800 Subject: [PATCH] =?UTF-8?q?iter(g28):=20=E6=B5=AE=E5=B1=82=20Esc=20?= =?UTF-8?q?=E9=80=90=E5=B1=82=E5=85=B3=E9=97=AD=EF=BC=8C=E9=98=BB=E6=AD=A2?= =?UTF-8?q?=E5=86=92=E6=B3=A1=E8=BF=9E=E5=B8=A6=E5=85=B3=E5=A4=96=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/backlog.json | 8 ++++++++ .ua/index.json | 11 +++++++---- apps/demo/main.ts | 4 +++- llms-full.txt | 5 ++++- packages/skin/ui/src/image.ts | 2 +- packages/skin/ui/src/menu.ts | 1 + packages/skin/ui/src/popover.ts | 1 + packages/skin/ui/src/select.ts | 2 +- packages/skin/ui/src/tooltip.ts | 2 +- packages/skin/ui/ua.manifest.json | 9 ++++++--- 10 files changed, 33 insertions(+), 12 deletions(-) diff --git a/.ua/backlog.json b/.ua/backlog.json index cef333f..496f437 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -580,6 +580,14 @@ "acceptance": "modal/drawer 共用 overlay.ts 的 Esc 栈,后开先关;栈空自动摘全局监听;npm run check 全绿", "status": "done", "notes": "自补:既有组件可用性细节;去掉两处各自挂的 document keydown" + }, + { + "id": "g28", + "title": "Esc 逐层关闭:浮层阻止冒泡", + "motivation": "弹窗内的 select/menu/popover 展开时按 Esc,会先收下拉再冒泡到 document 的 Esc 栈,导致弹窗一起被关——一次 Esc 关两层", + "acceptance": "menu/select/popover/tooltip/image 的 Esc 处理各自 stopPropagation,只关本层;演示页在弹窗内放下拉可复现逐层关闭;npm run check 全绿", + "status": "open", + "notes": "自补:既有组件可用性细节" } ] } diff --git a/.ua/index.json b/.ua/index.json index b974af7..60c3515 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T03:37:32.721Z", + "generatedAt": "2026-10-26T03:38:39.607Z", "repoRoot": ".", "packages": [ { @@ -627,7 +627,8 @@ "准备 options 数组,value 是提交用值、label 是展示文案", "创建信号并传入 value;未传时组件内部自持状态", "点击触发器展开弹层,点选项写回信号并收起;点外部或按 Esc 也会收起", - "选中项在弹层中用淡色弱化标记,不抢视觉焦点" + "选中项在弹层中用淡色弱化标记,不抢视觉焦点", + "弹层展开时按 Esc 只收起下拉并回焦触发器;若下拉在弹窗/抽屉内,再按一次 Esc 才关外层(逐层关闭)" ] }, { @@ -673,7 +674,8 @@ "点触发器展开弹层,点条目收起并触发 onSelect;点外部或按 Esc 也会收起(Esc 后焦点回到触发器)", "条目要给快捷键提示时传 shortcut(如 '⌘K'):右侧灰字展示,组件不绑定按键,实际快捷键由调用方在全局监听", "键盘:Tab 聚焦触发器,↑↓ 展开并在可用条目间环形移动,Enter / Space 选中;disabled 项自动跳过", - "靠右对齐避免溢出时传 align: 'end';危险操作给条目加 danger: true" + "靠右对齐避免溢出时传 align: 'end';危险操作给条目加 danger: true", + "展开时按 Esc 只收起菜单并回焦触发器,不会连带关闭所在弹窗(逐层关闭)" ] }, { @@ -1108,7 +1110,8 @@ "点击触发器开合;点弹层外部或按 Esc 收起(Esc 后焦点回触发器)", "content 里可以放 button / checkbox 等交互元素(tooltip 只能纯文本),点击弹层内部不会误关", "hover 场景传 trigger: 'hover' + delay 控制多快出场;placement: 'top' 让弹层往上翻", - "需要感知开合做副作用(如懒加载数据)时用 onOpenChange" + "需要感知开合做副作用(如懒加载数据)时用 onOpenChange", + "展开时按 Esc 只收起气泡并回焦触发器,不会连带关闭所在弹窗(逐层关闭)" ] }, { diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 42b7ac8..267f630 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -132,6 +132,7 @@ setInterval(() => percent((percent() + 5) % 105), 400); // 自动推进演示进 // —— 标签页 / 弹窗状态 —— const activeTab = signal('a'); const modalOpen = signal(false); +const modalPick = signal('a'); // 弹窗内下拉的取值,用于演示 Esc 逐层关闭 // —— 表单校验状态集中声明 —— const username = signal(''); @@ -384,7 +385,7 @@ const content = h( section( 'sec-tabs-modal', '标签页与弹窗', - 'tabs 滑动指示条 + 方向键切换(←→),禁用页签置灰;modal 支持 Esc / 遮罩 / × 关闭。', + 'tabs 滑动指示条 + 方向键切换(←→),禁用页签置灰;modal 支持 Esc / 遮罩 / × 关闭,弹窗内的下拉展开时 Esc 先收下拉、再按才关弹窗(逐层关闭)。', tabs({ tabs: [{ value: 'a', label: '概览' }, { value: 'b', label: '明细' }, { value: 'c', label: '设置' }, { value: 'd', label: '已停用', disabled: true }], active: activeTab }), h('span', { class: 'out' }, () => `当前页签:${activeTab()}`), button({ label: '打开弹窗', onClick: () => modalOpen(true) }), @@ -403,6 +404,7 @@ const content = h( }, h('p', null, '这是一个由信号驱动的弹窗组件(size: lg + footer 操作区)。'), h('div', { class: 'row' }, badge({ label: 'modal', variant: 'success' }), badge({ label: 'footer', variant: 'default' })), + select({ options: [{ value: 'a', label: '优先处理' }, { value: 'b', label: '排队等待' }, { value: 'c', label: '转交他人' }], value: modalPick }), ), ), section( diff --git a/llms-full.txt b/llms-full.txt index 8fd7a0c..8fb0c25 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T03:37:32.721Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T03:38:39.607Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -274,6 +274,7 @@ const city = signal(''); select({ options: [{ value: 'bj', label: '北京' }], v 2. 创建信号并传入 value;未传时组件内部自持状态 3. 点击触发器展开弹层,点选项写回信号并收起;点外部或按 Esc 也会收起 4. 选中项在弹层中用淡色弱化标记,不抢视觉焦点 +5. 弹层展开时按 Esc 只收起下拉并回焦触发器;若下拉在弹窗/抽屉内,再按一次 Esc 才关外层(逐层关闭) 现场演示:apps/docs/demo.html#sec-form @@ -301,6 +302,7 @@ menu({ label: '操作', items: [{ value: 'edit', label: '编辑' }, { value: 'de 4. 条目要给快捷键提示时传 shortcut(如 '⌘K'):右侧灰字展示,组件不绑定按键,实际快捷键由调用方在全局监听 5. 键盘:Tab 聚焦触发器,↑↓ 展开并在可用条目间环形移动,Enter / Space 选中;disabled 项自动跳过 6. 靠右对齐避免溢出时传 align: 'end';危险操作给条目加 danger: true +7. 展开时按 Esc 只收起菜单并回焦触发器,不会连带关闭所在弹窗(逐层关闭) 现场演示:apps/docs/demo.html#sec-menu @@ -591,6 +593,7 @@ popover({ title: '筛选条件', content: h('div', null, '任意可交互内容' 3. content 里可以放 button / checkbox 等交互元素(tooltip 只能纯文本),点击弹层内部不会误关 4. hover 场景传 trigger: 'hover' + delay 控制多快出场;placement: 'top' 让弹层往上翻 5. 需要感知开合做副作用(如懒加载数据)时用 onOpenChange +6. 展开时按 Esc 只收起气泡并回焦触发器,不会连带关闭所在弹窗(逐层关闭) 现场演示:apps/docs/demo.html#sec-popover diff --git a/packages/skin/ui/src/image.ts b/packages/skin/ui/src/image.ts index f40fdf1..a6a0f97 100644 --- a/packages/skin/ui/src/image.ts +++ b/packages/skin/ui/src/image.ts @@ -35,7 +35,7 @@ function openViewer(src: string, alt?: string): void { document.removeEventListener('keydown', onKey); prev?.focus?.(); }; - const onKey = (e: KeyboardEvent) => e.key === 'Escape' && close(); + const onKey = (e: KeyboardEvent) => { if (e.key !== 'Escape') return; e.stopPropagation(); close(); }; // 预览层 Esc 自关,不连带关外层 mask.addEventListener('click', close); document.addEventListener('keydown', onKey); document.body.appendChild(mask); diff --git a/packages/skin/ui/src/menu.ts b/packages/skin/ui/src/menu.ts index 45cce89..0618b37 100644 --- a/packages/skin/ui/src/menu.ts +++ b/packages/skin/ui/src/menu.ts @@ -73,6 +73,7 @@ export function menu(props: MenuProps): HTMLElement { root.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === 'Escape') { + e.stopPropagation(); // 只关本层:不再冒泡去关外层的弹窗/抽屉 open(false); trigger.focus(); // 收起后焦点回到触发器,键盘流不断 return; diff --git a/packages/skin/ui/src/popover.ts b/packages/skin/ui/src/popover.ts index e53c14c..fa52529 100644 --- a/packages/skin/ui/src/popover.ts +++ b/packages/skin/ui/src/popover.ts @@ -53,6 +53,7 @@ export function popover(props: PopoverProps, trigger: HTMLElement): HTMLElement } root.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key !== 'Escape' || !open()) return; // 卫语句:未展开时 Esc 不处理 + e.stopPropagation(); // 只关本层,避免连带关闭外层弹窗 setOpen(false); trigger.focus(); // 收起后焦点回触发器 }); diff --git a/packages/skin/ui/src/select.ts b/packages/skin/ui/src/select.ts index 330379d..57542b9 100644 --- a/packages/skin/ui/src/select.ts +++ b/packages/skin/ui/src/select.ts @@ -65,7 +65,7 @@ export function select(props: SelectProps): HTMLElement { }; root.addEventListener('keydown', (e: KeyboardEvent) => { - if (e.key === 'Escape') { open(false); trigger.focus(); return; } // Esc 关闭并回焦触发器 + if (e.key === 'Escape') { e.stopPropagation(); open(false); trigger.focus(); return; } // Esc 只关本层并回焦触发器 if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); // 阻止页面滚动 if (!open()) { open(true); options[0]?.focus(); return; } // 未展开:展开并聚焦首个选项 diff --git a/packages/skin/ui/src/tooltip.ts b/packages/skin/ui/src/tooltip.ts index 0919c28..159e3a3 100644 --- a/packages/skin/ui/src/tooltip.ts +++ b/packages/skin/ui/src/tooltip.ts @@ -49,7 +49,7 @@ export function tooltip(props: TooltipProps, trigger: HTMLElement): HTMLElement root.addEventListener('focusout', hide); // 失焦 effect(() => { if (open()) place(flip(trigger, bubble, props.placement ?? 'top')); }); // 每次出场重算方位:空间不足自动翻转 root.addEventListener('keydown', (e: KeyboardEvent) => { - if (e.key === 'Escape') hide(); // Esc 立即收起,不必等延迟 + if (e.key === 'Escape') { e.stopPropagation(); hide(); } // Esc 立即收起且不影响外层弹层 }); return root; } diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index b529bd3..32c7a7e 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -498,7 +498,8 @@ "准备 options 数组,value 是提交用值、label 是展示文案", "创建信号并传入 value;未传时组件内部自持状态", "点击触发器展开弹层,点选项写回信号并收起;点外部或按 Esc 也会收起", - "选中项在弹层中用淡色弱化标记,不抢视觉焦点" + "选中项在弹层中用淡色弱化标记,不抢视觉焦点", + "弹层展开时按 Esc 只收起下拉并回焦触发器;若下拉在弹窗/抽屉内,再按一次 Esc 才关外层(逐层关闭)" ] }, { @@ -544,7 +545,8 @@ "点触发器展开弹层,点条目收起并触发 onSelect;点外部或按 Esc 也会收起(Esc 后焦点回到触发器)", "条目要给快捷键提示时传 shortcut(如 '⌘K'):右侧灰字展示,组件不绑定按键,实际快捷键由调用方在全局监听", "键盘:Tab 聚焦触发器,↑↓ 展开并在可用条目间环形移动,Enter / Space 选中;disabled 项自动跳过", - "靠右对齐避免溢出时传 align: 'end';危险操作给条目加 danger: true" + "靠右对齐避免溢出时传 align: 'end';危险操作给条目加 danger: true", + "展开时按 Esc 只收起菜单并回焦触发器,不会连带关闭所在弹窗(逐层关闭)" ] }, { @@ -979,7 +981,8 @@ "点击触发器开合;点弹层外部或按 Esc 收起(Esc 后焦点回触发器)", "content 里可以放 button / checkbox 等交互元素(tooltip 只能纯文本),点击弹层内部不会误关", "hover 场景传 trigger: 'hover' + delay 控制多快出场;placement: 'top' 让弹层往上翻", - "需要感知开合做副作用(如懒加载数据)时用 onOpenChange" + "需要感知开合做副作用(如懒加载数据)时用 onOpenChange", + "展开时按 Esc 只收起气泡并回焦触发器,不会连带关闭所在弹窗(逐层关闭)" ] }, {