iter(g107): 新增二次确认气泡 popconfirm

This commit is contained in:
HIsheR
2026-10-26 13:59:48 +08:00
parent 137bf87d83
commit 48f94659e4
14 changed files with 287 additions and 11 deletions
+1
View File
@@ -58,3 +58,4 @@ export { tour } from './tour.js';
export { watermark } from './watermark.js';
export { affix } from './affix.js';
export { anchor } from './anchor.js';
export { popconfirm } from './popconfirm.js';
+51
View File
@@ -0,0 +1,51 @@
import { effect, h, signal } from '@ui-arch/frame';
import { button } from './button.js';
import { popover } from './popover.js';
import type { PopconfirmProps } from './types.js';
/*** 二次确认气泡:危险操作就地确认,不打断页面;确定后收起再回调,取消 / 点外部 / Esc 都走 onCancel ***/
export function popconfirm(props: PopconfirmProps, trigger: HTMLElement): HTMLElement {
// —— 状态集中声明 ——
const open = props.open ?? signal(false); // 开合信号:自己持有一份,确认与取消都要能写回
let accepted = false; // 本次收起是否由「确定」触发:用来区分确认与取消
/*** 收起弹层:写回信号即触发 popover 的显隐与 Esc 栈清理 ***/
const close = (): void => { open(false); };
/*** 确认:先收起再回调,避免回调里同步改数据时弹层还开着 ***/
const accept = (): void => {
accepted = true;
close();
props.onConfirm();
};
const body = h(
'div',
{ class: 'ua-popconfirm' },
h('div', { class: 'ua-popconfirm-text' }, props.description ?? '该操作不可撤销,确定继续吗?'),
h(
'div',
{ class: 'ua-popconfirm-actions' },
button({ label: props.cancelText ?? '取消', variant: 'ghost', onClick: () => close() }),
button({ label: props.confirmText ?? '确定', variant: props.variant === 'danger' ? 'danger' : 'primary', onClick: () => accept() }),
),
);
const wrap = popover({
title: props.title,
content: body,
placement: props.placement ?? 'top', // 缺省压在上方:确认气泡不该盖住被操作的那一行
trigger: 'click',
open,
onOpenChange: (next) => {
if (next || accepted) { accepted = false; return; } // 展开、或刚由确认收起:不回调取消
props.onCancel?.(); // 点外部 / Esc 收起等同取消
},
}, trigger);
effect(() => {
if (!open()) return; // 卫语句:收起时不动焦点
requestAnimationFrame(() => (body.querySelector('button') as HTMLElement | null)?.focus()); // 展开即落在「取消」上,误按回车不会误伤
});
return wrap;
}
+1 -1
View File
@@ -8,7 +8,7 @@ let seq = 0; // 全局自增序号:保证每个弹层 id 唯一,供 aria-con
/*** 气泡卡片:包裹触发器,默认点击开合;内容可交互,点外部 / Esc / 再次点触发器收起 ***/
export function popover(props: PopoverProps, trigger: HTMLElement): HTMLElement {
// —— 状态集中声明 ——
const open = signal(false); // 弹层显隐
const open = props.open ?? signal(false); // 弹层显隐:外部传了信号就交给它控制(实现受控开合)
let releaseEsc: (() => void) | null = null; // Esc 栈注销函数:展开期间占一层,收起即释放
const place = signal(props.placement ?? 'bottom'); // 实际生效方位:打开时按视口空间自动翻转
const id = `ua-pop-${++seq}`; // 弹层唯一 id
+14
View File
@@ -16,6 +16,7 @@ export interface PopoverProps {
trigger?: 'click' | 'hover'; // 触发方式;缺省 click(hover 模式带 delay 延迟)
delay?: number; // hover 模式出场延迟毫秒;缺省 150
onOpenChange?: (open: boolean) => void; // 开合回调
open?: Signal<boolean>; // 受控开合信号;不传则组件内部维护,传了外部可编程开合
}
/*** 按钮入参 ***/
@@ -704,4 +705,17 @@ export interface AnchorProps {
onChange?: (href: string) => void; // 跳转后的回调(埋点或联动滚动)
}
/*** 二次确认气泡入参 ***/
export interface PopconfirmProps {
title: string; // 气泡标题,通常写成一句问句
description?: string; // 补充说明:影响范围 / 不可撤销提示
confirmText?: string; // 确认按钮文案;缺省「确定」
cancelText?: string; // 取消按钮文案;缺省「取消」
variant?: 'default' | 'danger'; // 确认按钮语义色;缺省 default,删除类操作传 danger
placement?: 'top' | 'bottom'; // 气泡方位;缺省 top(不盖住被操作的行)
onConfirm: () => void; // 点确定后的回调(弹层已收起)
onCancel?: () => void; // 取消 / 点外部 / Esc 收起时的回调
open?: Signal<boolean>; // 受控开合信号;不传则内部维护
}
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
+69 -1
View File
@@ -72,7 +72,8 @@
"checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项",
"affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应",
"multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用",
"anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器"
"anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器",
"popconfirm(props, trigger):二次确认气泡,挂在任意触发器上,就地确认危险操作;确定后先收起再回调,取消 / 点外部 / Esc 走 onCancel,展开即聚焦「取消」"
],
"keyTypes": [
"ButtonProps",
@@ -1098,6 +1099,11 @@
"t": "HTMLElement",
"r": 1,
"d": "第二个参数:被包裹的触发器元素"
},
{
"n": "open",
"t": "Signal<boolean>",
"d": "受控开合信号;不传则内部维护,传了外部可编程开合(popconfirm 即基于它)"
}
],
"example": "popover({ title: '筛选条件', content: h('div', null, '任意可交互内容') }, button({ label: '筛选' }))",
@@ -2753,6 +2759,68 @@
"页面有吸顶导航时把它的高度传给 offset,跳转与高亮判定都会预留这段距离",
"滚动容器不是整页时用 container 传入该元素;点击走平滑滚动,系统开启「减少动效」时改为瞬移,hash 用 replaceState 回写不堆历史"
]
},
{
"id": "popconfirm",
"title": "二次确认 Popconfirm",
"demo": "sec-popconfirm",
"props": [
{
"n": "title",
"t": "string",
"r": 1,
"d": "气泡标题,通常写成一句问句(如「删除这条记录?」)"
},
{
"n": "description",
"t": "string",
"d": "补充说明:影响范围、是否可撤销;缺省给一句通用提示"
},
{
"n": "confirmText",
"t": "string",
"d": "确认按钮文案;缺省「确定」"
},
{
"n": "cancelText",
"t": "string",
"d": "取消按钮文案;缺省「取消」"
},
{
"n": "variant",
"t": "'default' | 'danger'",
"d": "确认按钮语义色;删除类操作传 danger,缺省 default"
},
{
"n": "placement",
"t": "'top' | 'bottom'",
"d": "气泡方位;缺省 top,压在触发器上方不盖住被操作的行"
},
{
"n": "onConfirm",
"t": "() => void",
"k": 1,
"d": "点确定后的回调(此时弹层已收起)"
},
{
"n": "onCancel",
"t": "() => void",
"k": 1,
"d": "取消 / 点外部 / Esc 收起时的回调"
},
{
"n": "open",
"t": "Signal<boolean>",
"d": "受控开合信号;不传则内部维护,传了可由外部编程开合"
}
],
"example": "popconfirm({ title: '删除这条记录?', description: '删除后无法恢复', variant: 'danger', confirmText: '删除', onConfirm: () => toast.success('已删除') }, button({ label: '删除', variant: 'danger' }))",
"steps": [
"把要保护的触发器元素(按钮 / 图标)作为第二个参数传进去,返回的包裹节点替换原位置",
"写清 title(问句)与 description(影响范围),删除类操作把 variant 设成 danger,确认按钮会自动变成危险色",
"点确定先收起弹层再触发 onConfirm,回调里可以放心同步改数据;取消、点外部、Esc 一律走 onCancel",
"展开时焦点自动落在「取消」上,误按回车不会执行危险操作;需要外部控制开合时传 open 信号"
]
}
]
}
+18
View File
@@ -3720,3 +3720,21 @@ button.ua-step-dot:hover {
transform: none;
}
}
/* —— 二次确认气泡 —— */
.ua-popconfirm {
display: flex;
flex-direction: column;
gap: var(--ua-space-2);
max-width: 240px;
}
.ua-popconfirm-text {
color: var(--ua-color-text-muted);
font-size: var(--ua-font-size-md);
line-height: 1.5;
}
.ua-popconfirm-actions {
display: flex;
justify-content: flex-end;
gap: var(--ua-space-2);
}