iter(g167): 弹窗支持内置页脚与异步确认

This commit is contained in:
HIsheR
2026-10-26 16:14:19 +08:00
parent b6a6827f9b
commit ff9442904b
7 changed files with 78 additions and 17 deletions
+19 -1
View File
@@ -1,6 +1,7 @@
import { effect, h } from '@ui-arch/frame';
import type { ModalProps } from './types.js';
import { lockScroll, pushOverlay, trapFocus } from './overlay.js';
import { button } from './button.js';
let seq = 0; // 实例序号:给标题生成唯一 id,供 aria-labelledby 关联
@@ -12,6 +13,23 @@ export function modal(props: ModalProps, ...children: unknown[]): HTMLElement {
props.open(false); // 写回信号触发隐藏
props.onClose?.();
};
/*** 内置页脚:取消直接关;确认若返回 Promise,则按钮转「处理中…」并禁用,成功才收窗、失败留在窗口重试 ***/
const builtinFoot = (): HTMLElement => {
const ok = button({ label: props.confirmText ?? '确定' }) as HTMLButtonElement;
const cancel = button({ label: props.cancelText ?? '取消', variant: 'ghost', onClick: close }) as HTMLButtonElement;
const idle = ok.textContent; // 记下常态文案,异步结束后还原
ok.addEventListener('click', () => {
const out = props.onConfirm?.();
if (!(out instanceof Promise)) { close(); return; } // 同步确认:直接收窗
ok.disabled = true;
ok.textContent = '处理中…';
out.then(() => { if (props.open()) close(); }) // 成功才关;onConfirm 里已手动关掉就不重复触发
.catch(() => { /* 失败什么都不做:保持窗口打开让用户重试,提示交给调用方的 toast */ })
.finally(() => { ok.disabled = false; ok.textContent = idle; });
});
return h('div', { class: 'ua-modal-actions' }, cancel, ok);
};
const foot = props.footer ? h('div', { class: 'ua-modal-foot' }, props.footer) : props.confirmText || props.onConfirm ? h('div', { class: 'ua-modal-foot' }, builtinFoot()) : null; // footer 优先,其次内置按钮组
const overlay = h('div', {
class: 'ua-modal-overlay',
onClick: (e: Event) => {
@@ -25,7 +43,7 @@ export function modal(props: ModalProps, ...children: unknown[]): HTMLElement {
{ class: `ua-modal ua-modal-${props.size ?? 'md'}`, role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': titleId }, // 对话框语义 + 标题关联
h('div', { class: 'ua-modal-head' }, h('h3', { class: 'ua-modal-title', id: titleId }, props.title), h('button', { type: 'button', class: 'ua-modal-close', 'aria-label': '关闭弹窗', onClick: close }, '×')),
h('div', { class: 'ua-modal-body' }, ...children),
props.footer ? h('div', { class: 'ua-modal-foot' }, props.footer) : null, // 底部操作区:右对齐,仅传了才渲染
foot, // 底部操作区:自定义 footer 或内置按钮组,都没有则不渲染
);
overlay.append(box);
let lastFocus: HTMLElement | null = null; // 打开前焦点宿主,关闭时归还
+3
View File
@@ -363,6 +363,9 @@ export interface ModalProps {
open: Signal<boolean>; // 开关信号:true 显示 / false 隐藏
size?: 'sm' | 'md' | 'lg'; // 宽度档位;缺省 md(420px),sm 340px / lg 640px
footer?: unknown; // 底部操作区内容(常见为按钮组);缺省不渲染
confirmText?: string; // 内置页脚:确认按钮文案;与 footer 二选一(footer 优先),缺省「确定」
cancelText?: string; // 内置页脚:取消按钮文案;缺省「取消」
onConfirm?: () => void | Promise<void>; // 内置页脚:确认回调;返回 Promise 时装 loading 禁用按钮,成功才收窗、失败留在窗口重试
closeOnOverlay?: boolean; // 点遮罩是否关闭;缺省 true(表单类弹窗可传 false 防误关,× 与 Esc 仍可用)
onClose?: () => void; // 关闭回调(点遮罩、× 或 Esc 触发)
}