iter(g116): 抽屉支持非模态模式
This commit is contained in:
@@ -1284,6 +1284,14 @@
|
|||||||
"acceptance": "新增 calendar(props)(< 200 行):value/month 信号、marker 每日标记(文案同时进 aria-label)、onSelect / onMonthChange;月初留白与天数计算抽到 dateKit.ts,datePicker 改为共用同一份(消除两处日期工具);组件 59→60,types/index/css/manifest/docs/README/AGENTS/ARCHITECTURE/demo 同步;npm run check 全绿",
|
"acceptance": "新增 calendar(props)(< 200 行):value/month 信号、marker 每日标记(文案同时进 aria-label)、onSelect / onMonthChange;月初留白与天数计算抽到 dateKit.ts,datePicker 改为共用同一份(消除两处日期工具);组件 59→60,types/index/css/manifest/docs/README/AGENTS/ARCHITECTURE/demo 同步;npm run check 全绿",
|
||||||
"notes": "自补:组件补充 + 派生语义单一来源(日期工具下沉 dateKit)",
|
"notes": "自补:组件补充 + 派生语义单一来源(日期工具下沉 dateKit)",
|
||||||
"status": "done"
|
"status": "done"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g116",
|
||||||
|
"title": "抽屉支持非模态模式",
|
||||||
|
"motivation": "「一边看列表一边改右侧详情」是后台最常见的交互,但 drawer 只有模态一档:打开就盖一层遮罩、锁住背景滚动、把焦点困在面板里,主视图彻底不可用;调用方只能拿 fixed 的 div 自己拼一个假抽屉",
|
||||||
|
"acceptance": "DrawerProps 新增 modal(缺省 true):false 时遮罩透明且 pointer-events: none、面板自身可交互并带阴影、不锁背景滚动、不启用 Tab 焦点陷阱、aria-modal=false、点外部不算关闭(× 与 Esc 仍可用);文档参数表 / 手把手与演示(非模态检查器)同步;npm run check 全绿",
|
||||||
|
"notes": "自补:组件可用性;沿用现有遮罩节点加一个 is-plain 分支,不新增 DOM 结构",
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T06:08:45.187Z",
|
"generatedAt": "2026-10-26T06:10:04.137Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -1692,6 +1692,11 @@
|
|||||||
"t": "boolean",
|
"t": "boolean",
|
||||||
"d": "点遮罩是否关闭抽屉;缺省 true(× 与 Esc 始终可用)"
|
"d": "点遮罩是否关闭抽屉;缺省 true(× 与 Esc 始终可用)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"n": "modal",
|
||||||
|
"t": "boolean",
|
||||||
|
"d": "是否模态:false 时无遮罩、不锁背景滚动、不困焦点,页面仍可操作(检查器 / 详情面板);缺省 true"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"n": "onClose",
|
"n": "onClose",
|
||||||
"t": "() => void",
|
"t": "() => void",
|
||||||
@@ -1710,7 +1715,8 @@
|
|||||||
"打开后从侧边滑出:placement: 'right'(默认)或 'left',width 控制宽度(窄屏自动收到 92vw)",
|
"打开后从侧边滑出:placement: 'right'(默认)或 'left',width 控制宽度(窄屏自动收到 92vw)",
|
||||||
"点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素,Esc 只关最上层弹层",
|
"点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素,Esc 只关最上层弹层",
|
||||||
"底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑",
|
"底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑",
|
||||||
"面板内容与 modal 一样接受任意 DOM 与响应式函数"
|
"面板内容与 modal 一样接受任意 DOM 与响应式函数",
|
||||||
|
"要「一边看一边改」就把 modal 设成 false:遮罩透明且不吃点击、背景滚动不锁、Tab 不被困在面板里,× 与 Esc 仍能关"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ const tablePage = signal(1);
|
|||||||
|
|
||||||
// —— 抽屉开关 ——
|
// —— 抽屉开关 ——
|
||||||
const drawerOpen = signal(false);
|
const drawerOpen = signal(false);
|
||||||
|
const inspectorOpen = signal(false); // 非模态抽屉:无遮罩,页面仍可操作
|
||||||
|
|
||||||
// —— 标签列表:关闭即写回信号,容器按信号重渲染 ——
|
// —— 标签列表:关闭即写回信号,容器按信号重渲染 ——
|
||||||
const tags = signal(['设计', '前端', '组件库']);
|
const tags = signal(['设计', '前端', '组件库']);
|
||||||
@@ -304,6 +305,7 @@ const content = h(
|
|||||||
h('div', { class: 'row' },
|
h('div', { class: 'row' },
|
||||||
button({ label: '从右侧打开', onClick: () => drawerOpen(true) }),
|
button({ label: '从右侧打开', onClick: () => drawerOpen(true) }),
|
||||||
button({ label: '从底部打开', variant: 'ghost', onClick: () => sheetOpen(true) }),
|
button({ label: '从底部打开', variant: 'ghost', onClick: () => sheetOpen(true) }),
|
||||||
|
button({ label: '非模态打开', variant: 'ghost', onClick: () => inspectorOpen(true) }),
|
||||||
),
|
),
|
||||||
drawer(
|
drawer(
|
||||||
{
|
{
|
||||||
@@ -320,6 +322,17 @@ const content = h(
|
|||||||
{ title: '快捷操作', open: sheetOpen, placement: 'bottom', width: 220, onClose: () => sheetOpen(false) },
|
{ title: '快捷操作', open: sheetOpen, placement: 'bottom', width: 220, onClose: () => sheetOpen(false) },
|
||||||
h('p', null, '从底部滑出的操作面板:适合移动端底部弹层,宽度自动铺满、高度由 width 指定。'),
|
h('p', null, '从底部滑出的操作面板:适合移动端底部弹层,宽度自动铺满、高度由 width 指定。'),
|
||||||
),
|
),
|
||||||
|
drawer(
|
||||||
|
{
|
||||||
|
title: '非模态检查器',
|
||||||
|
open: inspectorOpen,
|
||||||
|
modal: false,
|
||||||
|
width: 300,
|
||||||
|
onClose: () => inspectorOpen(false),
|
||||||
|
footer: h('div', { class: 'row' }, button({ label: '关闭', variant: 'ghost', onClick: () => inspectorOpen(false) })),
|
||||||
|
},
|
||||||
|
h('p', null, 'modal: false —— 没有遮罩、不锁背景滚动、不困焦点:左边页面照常能点能滚,适合「一边看一边改」的检查器 / 详情面板。'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
section(
|
section(
|
||||||
'sec-tag',
|
'sec-tag',
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-26T06:08:45.187Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-26T06:10:04.137Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -884,6 +884,7 @@ const open = signal(false); modal({ title: '确认操作', open, onClose: () =>
|
|||||||
- width(默认 320):number —— 面板尺寸(像素):左右滑出为宽度,上下滑出为高度;缺省 320
|
- width(默认 320):number —— 面板尺寸(像素):左右滑出为宽度,上下滑出为高度;缺省 320
|
||||||
- footer:unknown —— 底部操作区内容;缺省不渲染
|
- footer:unknown —— 底部操作区内容;缺省不渲染
|
||||||
- closeOnOverlay:boolean —— 点遮罩是否关闭抽屉;缺省 true(× 与 Esc 始终可用)
|
- closeOnOverlay:boolean —— 点遮罩是否关闭抽屉;缺省 true(× 与 Esc 始终可用)
|
||||||
|
- modal:boolean —— 是否模态:false 时无遮罩、不锁背景滚动、不困焦点,页面仍可操作(检查器 / 详情面板);缺省 true
|
||||||
- onClose(回调):() => void —— 关闭回调(点遮罩 / × / Esc 触发)
|
- onClose(回调):() => void —— 关闭回调(点遮罩 / × / Esc 触发)
|
||||||
- ...children:unknown[] —— 面板内容子节点
|
- ...children:unknown[] —— 面板内容子节点
|
||||||
|
|
||||||
@@ -900,6 +901,7 @@ const open = signal(false); drawer({ title: '详情', open, width: 360, onClose:
|
|||||||
3. 点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素,Esc 只关最上层弹层
|
3. 点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素,Esc 只关最上层弹层
|
||||||
4. 底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑
|
4. 底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑
|
||||||
5. 面板内容与 modal 一样接受任意 DOM 与响应式函数
|
5. 面板内容与 modal 一样接受任意 DOM 与响应式函数
|
||||||
|
6. 要「一边看一边改」就把 modal 设成 false:遮罩透明且不吃点击、背景滚动不锁、Tab 不被困在面板里,× 与 Esc 仍能关
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-drawer
|
现场演示:apps/docs/demo.html#sec-drawer
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,16 @@ let seq = 0; // 实例序号:给标题生成唯一 id,供 aria-labelledby
|
|||||||
/*** 抽屉:open 信号驱动侧滑面板,点遮罩 / × / Esc 关闭;Tab 焦点陷阱 + 关闭归还焦点,与 modal 的分工是不打断主视图、可从侧边保留上下文 ***/
|
/*** 抽屉:open 信号驱动侧滑面板,点遮罩 / × / Esc 关闭;Tab 焦点陷阱 + 关闭归还焦点,与 modal 的分工是不打断主视图、可从侧边保留上下文 ***/
|
||||||
export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement {
|
export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement {
|
||||||
const titleId = `ua-drawer-title-${++seq}`; // 标题 id:抽屉的可访问名
|
const titleId = `ua-drawer-title-${++seq}`; // 标题 id:抽屉的可访问名
|
||||||
|
const modal = props.modal !== false; // 是否模态:false 时无遮罩、不锁滚动、不困焦点,页面仍可操作
|
||||||
const close = (): void => {
|
const close = (): void => {
|
||||||
if (!props.open()) return; // 卫语句:已关闭不重复触发回调
|
if (!props.open()) return; // 卫语句:已关闭不重复触发回调
|
||||||
props.open(false);
|
props.open(false);
|
||||||
props.onClose?.();
|
props.onClose?.();
|
||||||
};
|
};
|
||||||
const overlay = h('div', {
|
const overlay = h('div', {
|
||||||
class: () => `ua-drawer-overlay${props.open() ? ' ua-open' : ''}`,
|
class: () => `ua-drawer-overlay${props.open() ? ' ua-open' : ''}${modal ? '' : ' is-plain'}`,
|
||||||
onClick: (e: Event) => {
|
onClick: (e: Event) => {
|
||||||
|
if (!modal) return; // 卫语句:非模态没有遮罩,点外面不算关闭
|
||||||
if (e.target !== overlay) return; // 卫语句:只处理点在遮罩本身的
|
if (e.target !== overlay) return; // 卫语句:只处理点在遮罩本身的
|
||||||
if (props.closeOnOverlay === false) return; // 显式禁止时防误关,× 与 Esc 仍可用
|
if (props.closeOnOverlay === false) return; // 显式禁止时防误关,× 与 Esc 仍可用
|
||||||
close();
|
close();
|
||||||
@@ -28,6 +30,7 @@ export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement
|
|||||||
role: 'dialog',
|
role: 'dialog',
|
||||||
'aria-labelledby': titleId,
|
'aria-labelledby': titleId,
|
||||||
'aria-hidden': () => String(!props.open()),
|
'aria-hidden': () => String(!props.open()),
|
||||||
|
'aria-modal': modal ? 'true' : 'false',
|
||||||
},
|
},
|
||||||
h('div', { class: 'ua-drawer-head' }, h('h3', { class: 'ua-drawer-title', id: titleId }, props.title), h('button', { class: 'ua-drawer-close', 'aria-label': '关闭抽屉', onClick: close }, '×')),
|
h('div', { class: 'ua-drawer-head' }, h('h3', { class: 'ua-drawer-title', id: titleId }, props.title), h('button', { class: 'ua-drawer-close', 'aria-label': '关闭抽屉', onClick: close }, '×')),
|
||||||
h('div', { class: 'ua-drawer-body' }, ...children),
|
h('div', { class: 'ua-drawer-body' }, ...children),
|
||||||
@@ -42,7 +45,7 @@ export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement
|
|||||||
if (props.open()) {
|
if (props.open()) {
|
||||||
lastFocus = document.activeElement as HTMLElement | null; // 记录打开前焦点
|
lastFocus = document.activeElement as HTMLElement | null; // 记录打开前焦点
|
||||||
release ??= pushOverlay(close); // 入 Esc 栈:多弹层时只关最上层
|
release ??= pushOverlay(close); // 入 Esc 栈:多弹层时只关最上层
|
||||||
releaseLock ??= lockScroll(); // 模态期锁背景滚动
|
if (modal) releaseLock ??= lockScroll(); // 仅模态期锁背景滚动
|
||||||
(panel.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素
|
(panel.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素
|
||||||
} else {
|
} else {
|
||||||
release?.(); // 出 Esc 栈
|
release?.(); // 出 Esc 栈
|
||||||
@@ -53,6 +56,6 @@ export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement
|
|||||||
lastFocus = null;
|
lastFocus = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
trapFocus(panel, () => props.open()); // Tab 焦点陷阱:与 modal 共用同一份实现
|
if (modal) trapFocus(panel, () => props.open()); // 模态才困焦点:非模态要让用户能继续操作背后页面
|
||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,6 +360,7 @@ export interface DrawerProps {
|
|||||||
width?: number; // 面板尺寸像素:左右滑出为宽度,上下滑出为高度;缺省 320
|
width?: number; // 面板尺寸像素:左右滑出为宽度,上下滑出为高度;缺省 320
|
||||||
footer?: unknown; // 底部操作区内容;缺省不渲染
|
footer?: unknown; // 底部操作区内容;缺省不渲染
|
||||||
closeOnOverlay?: boolean; // 点遮罩是否关闭;缺省 true(× 与 Esc 始终可用)
|
closeOnOverlay?: boolean; // 点遮罩是否关闭;缺省 true(× 与 Esc 始终可用)
|
||||||
|
modal?: boolean; // 是否模态:false 时无遮罩、不锁背景滚动、不困焦点,页面仍可操作;缺省 true
|
||||||
onClose?: () => void; // 关闭回调(遮罩 / × / Esc 触发)
|
onClose?: () => void; // 关闭回调(遮罩 / × / Esc 触发)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1563,6 +1563,11 @@
|
|||||||
"t": "boolean",
|
"t": "boolean",
|
||||||
"d": "点遮罩是否关闭抽屉;缺省 true(× 与 Esc 始终可用)"
|
"d": "点遮罩是否关闭抽屉;缺省 true(× 与 Esc 始终可用)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"n": "modal",
|
||||||
|
"t": "boolean",
|
||||||
|
"d": "是否模态:false 时无遮罩、不锁背景滚动、不困焦点,页面仍可操作(检查器 / 详情面板);缺省 true"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"n": "onClose",
|
"n": "onClose",
|
||||||
"t": "() => void",
|
"t": "() => void",
|
||||||
@@ -1581,7 +1586,8 @@
|
|||||||
"打开后从侧边滑出:placement: 'right'(默认)或 'left',width 控制宽度(窄屏自动收到 92vw)",
|
"打开后从侧边滑出:placement: 'right'(默认)或 'left',width 控制宽度(窄屏自动收到 92vw)",
|
||||||
"点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素,Esc 只关最上层弹层",
|
"点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素,Esc 只关最上层弹层",
|
||||||
"底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑",
|
"底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑",
|
||||||
"面板内容与 modal 一样接受任意 DOM 与响应式函数"
|
"面板内容与 modal 一样接受任意 DOM 与响应式函数",
|
||||||
|
"要「一边看一边改」就把 modal 设成 false:遮罩透明且不吃点击、背景滚动不锁、Tab 不被困在面板里,× 与 Esc 仍能关"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3922,3 +3922,13 @@ button.ua-step-dot:hover {
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* —— 非模态抽屉:无遮罩、页面可点,仅面板浮在右侧 —— */
|
||||||
|
.ua-drawer-overlay.is-plain {
|
||||||
|
background: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.ua-drawer-overlay.is-plain .ua-drawer {
|
||||||
|
pointer-events: auto;
|
||||||
|
box-shadow: 0 0 24px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user