Files
UI/packages/skin/ui/src/notification.ts
T

71 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { h } from '@ui-arch/frame';
import type { NotificationProps } from './types.js';
let host: HTMLElement | null = null; // 右下角堆叠容器:首条通知出现时懒创建
const MAX_STACK = 3; // 同屏最多条数,超出挤掉最早一条
/*** 确保堆叠容器存在:fixed 挂 body 末尾,与右上角 toast 分区不打架 ***/
function ensureHost(): HTMLElement {
if (host) return host;
host = h('div', { class: 'ua-notice-host' });
document.body.appendChild(host);
return host;
}
/*** 退场并移除:摘 ua-open 触发过渡,transitionend 与 320ms 超时二选一兜底 ***/
function dismiss(item: HTMLElement): void {
item.classList.remove('ua-open');
item.addEventListener('transitionend', () => item.remove(), { once: true });
window.setTimeout(() => item.isConnected && item.remove(), 320); // reduced-motion 下不触发 transition
}
/*** 通知卡片:标题 + 描述 + 可选操作区,返回 { el, close } 句柄;duration 为 0 表示常驻,非 0 时悬停 / 聚焦自动暂停计时 ***/
export function notification(props: NotificationProps): { el: HTMLElement; close: () => void } {
// —— 状态集中声明 ——
const duration = props.duration ?? 0; // 缺省常驻:通知通常需要用户读完
const closable = props.closable ?? true; // 缺省带关闭按钮
let timer = 0; // 自动关闭计时器句柄
let remain = duration; // 剩余毫秒:暂停时扣减,续计时接着走
let startedAt = 0; // 本轮计时起点,用于折算暂停前已流逝的时间
let paused = false; // 是否因悬停 / 聚焦而暂停
const closeBtn = h('button', { type: 'button', class: 'ua-notice-close', 'aria-label': '关闭通知' }, '×') as HTMLButtonElement; // 关闭按钮,回调在下方绑定
const el = h(
'div',
{ class: `ua-notice ua-notice-${props.variant ?? 'info'}`, role: 'alert' },
h(
'div',
{ class: 'ua-notice-head' },
h('span', { class: 'ua-notice-dot', 'aria-hidden': 'true' }),
h('span', { class: 'ua-notice-title' }, props.title),
closable ? closeBtn : null,
),
props.description ? h('div', { class: 'ua-notice-desc' }, props.description) : null,
props.action ? h('div', { class: 'ua-notice-action' }, props.action) : null,
) as HTMLElement;
/*** 关闭:清计时器后走退场动画 ***/
const close = () => {
window.clearTimeout(timer);
dismiss(el);
};
closeBtn.addEventListener('click', () => close());
/*** 起表:按剩余毫秒设定时器 ***/
const arm = () => { if (remain <= 0) return; startedAt = Date.now(); timer = window.setTimeout(() => dismiss(el), remain); };
/*** 暂停计时:扣掉已流逝部分,剩余时间留给续计 ***/
const pause = () => { if (paused || remain <= 0) return; paused = true; window.clearTimeout(timer); remain -= Date.now() - startedAt; };
/*** 续计:离开后按剩余毫秒重新起表 ***/
const resume = () => { if (!paused) return; paused = false; arm(); };
el.addEventListener('pointerenter', pause); // 悬停阅读时不倒计时
el.addEventListener('pointerleave', resume);
el.addEventListener('focusin', pause); // 键盘聚焦到关闭/操作按钮同样暂停
el.addEventListener('focusout', resume);
const stack = ensureHost();
if (stack.childElementCount >= MAX_STACK) stack.firstElementChild?.remove(); // 超出上限挤掉最早一条
stack.appendChild(el);
requestAnimationFrame(() => el.classList.add('ua-open')); // 下一帧入场,保证过渡生效
if (duration > 0) arm(); // 常驻(0)不设定时器
return { el, close };
}