iter(g116): 抽屉支持非模态模式
This commit is contained in:
@@ -7,14 +7,16 @@ let seq = 0; // 实例序号:给标题生成唯一 id,供 aria-labelledby
|
||||
/*** 抽屉:open 信号驱动侧滑面板,点遮罩 / × / Esc 关闭;Tab 焦点陷阱 + 关闭归还焦点,与 modal 的分工是不打断主视图、可从侧边保留上下文 ***/
|
||||
export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement {
|
||||
const titleId = `ua-drawer-title-${++seq}`; // 标题 id:抽屉的可访问名
|
||||
const modal = props.modal !== false; // 是否模态:false 时无遮罩、不锁滚动、不困焦点,页面仍可操作
|
||||
const close = (): void => {
|
||||
if (!props.open()) return; // 卫语句:已关闭不重复触发回调
|
||||
props.open(false);
|
||||
props.onClose?.();
|
||||
};
|
||||
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) => {
|
||||
if (!modal) return; // 卫语句:非模态没有遮罩,点外面不算关闭
|
||||
if (e.target !== overlay) return; // 卫语句:只处理点在遮罩本身的
|
||||
if (props.closeOnOverlay === false) return; // 显式禁止时防误关,× 与 Esc 仍可用
|
||||
close();
|
||||
@@ -28,6 +30,7 @@ export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement
|
||||
role: 'dialog',
|
||||
'aria-labelledby': titleId,
|
||||
'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-body' }, ...children),
|
||||
@@ -42,7 +45,7 @@ export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement
|
||||
if (props.open()) {
|
||||
lastFocus = document.activeElement as HTMLElement | null; // 记录打开前焦点
|
||||
release ??= pushOverlay(close); // 入 Esc 栈:多弹层时只关最上层
|
||||
releaseLock ??= lockScroll(); // 模态期锁背景滚动
|
||||
if (modal) releaseLock ??= lockScroll(); // 仅模态期锁背景滚动
|
||||
(panel.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素
|
||||
} else {
|
||||
release?.(); // 出 Esc 栈
|
||||
@@ -53,6 +56,6 @@ export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement
|
||||
lastFocus = null;
|
||||
}
|
||||
});
|
||||
trapFocus(panel, () => props.open()); // Tab 焦点陷阱:与 modal 共用同一份实现
|
||||
if (modal) trapFocus(panel, () => props.open()); // 模态才困焦点:非模态要让用户能继续操作背后页面
|
||||
return overlay;
|
||||
}
|
||||
|
||||
@@ -360,6 +360,7 @@ export interface DrawerProps {
|
||||
width?: number; // 面板尺寸像素:左右滑出为宽度,上下滑出为高度;缺省 320
|
||||
footer?: unknown; // 底部操作区内容;缺省不渲染
|
||||
closeOnOverlay?: boolean; // 点遮罩是否关闭;缺省 true(× 与 Esc 始终可用)
|
||||
modal?: boolean; // 是否模态:false 时无遮罩、不锁背景滚动、不困焦点,页面仍可操作;缺省 true
|
||||
onClose?: () => void; // 关闭回调(遮罩 / × / Esc 触发)
|
||||
}
|
||||
|
||||
|
||||
@@ -1563,6 +1563,11 @@
|
||||
"t": "boolean",
|
||||
"d": "点遮罩是否关闭抽屉;缺省 true(× 与 Esc 始终可用)"
|
||||
},
|
||||
{
|
||||
"n": "modal",
|
||||
"t": "boolean",
|
||||
"d": "是否模态:false 时无遮罩、不锁背景滚动、不困焦点,页面仍可操作(检查器 / 详情面板);缺省 true"
|
||||
},
|
||||
{
|
||||
"n": "onClose",
|
||||
"t": "() => void",
|
||||
@@ -1581,7 +1586,8 @@
|
||||
"打开后从侧边滑出:placement: 'right'(默认)或 'left',width 控制宽度(窄屏自动收到 92vw)",
|
||||
"点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素,Esc 只关最上层弹层",
|
||||
"底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑",
|
||||
"面板内容与 modal 一样接受任意 DOM 与响应式函数"
|
||||
"面板内容与 modal 一样接受任意 DOM 与响应式函数",
|
||||
"要「一边看一边改」就把 modal 设成 false:遮罩透明且不吃点击、背景滚动不锁、Tab 不被困在面板里,× 与 Esc 仍能关"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3922,3 +3922,13 @@ button.ua-step-dot:hover {
|
||||
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