iter(g67): tag 支持 checkable 筛选片,选中态与 aria-pressed 同步

This commit is contained in:
HIsheR
2026-10-26 12:36:14 +08:00
parent 173c3d520a
commit cc3c649b80
7 changed files with 101 additions and 9 deletions
+18 -5
View File
@@ -1,15 +1,28 @@
import { h } from '@ui-arch/frame';
import { effect, h, signal } from '@ui-arch/frame';
import type { TagProps } from './types.js';
/*** 标签:语义色小胶囊,可选 × 关闭;与 badge 的分工是 tag 可交互(带关闭回调) ***/
/*** 标签:语义色小胶囊;checkable 时作筛选片(aria-pressed + 信号双向绑定),否则可选 × 关闭 ***/
export function tag(props: TagProps): HTMLElement {
const label = h('span', { class: 'ua-tag-label' }, props.label);
if (!props.closable) {
return h('span', { class: `ua-tag ua-tag-${props.variant ?? 'default'}` }, label); // 纯展示形态
const base = `ua-tag ua-tag-${props.variant ?? 'default'}`; // 语义色共用
if (props.checkable) {
const on = props.checked ?? signal(false); // 未受控时内部自持,行为与受控一致
const btn = h('button', { type: 'button', class: `${base} is-checkable`, 'aria-pressed': String(on()) }, label) as HTMLButtonElement;
btn.addEventListener('click', () => {
on(!on()); // 用户点击写回信号
props.onChange?.(on());
});
effect(() => {
const value = on();
btn.setAttribute('aria-pressed', String(value)); // 读屏与视觉同源
btn.classList.toggle('is-checked', value);
});
return btn;
}
if (!props.closable) return h('span', { class: base }, label); // 纯展示形态
return h(
'span',
{ class: `ua-tag is-closable ua-tag-${props.variant ?? 'default'}` },
{ class: `${base} is-closable` },
label,
h('button', { type: 'button', class: 'ua-tag-close', 'aria-label': `移除 ${props.label}`, onClick: () => props.onClose?.() }, '×'),
);
+3
View File
@@ -254,6 +254,9 @@ export interface TagProps {
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info'; // 语义色;缺省 default
closable?: boolean; // 显示 × 关闭按钮;缺省 false
onClose?: () => void; // 关闭回调(调用方自行从数据源移除该项)
checkable?: boolean; // 可选中(当筛选片用);与 closable 互斥,同时传时以 checkable 为准
checked?: Signal<boolean>; // 选中态信号,双向绑定;不传则组件内部自持
onChange?: (checked: boolean) => void; // 选中态变化回调(点一下即回调新值)
}
/*** 图片入参 ***/
+18 -1
View File
@@ -801,6 +801,22 @@
"t": "() => void",
"k": 1,
"d": "关闭回调:调用方从数据源移除该项后重新渲染"
},
{
"n": "checkable",
"t": "boolean",
"df": "false",
"d": "可选中:渲染为筛选片(button + aria-pressed);与 closable 互斥,同时传以 checkable 为准"
},
{
"n": "checked",
"t": "Signal<boolean>",
"d": "选中态信号,双向绑定(点击写回、外部赋值同步);不传则组件内部自持"
},
{
"n": "onChange",
"t": "(checked: boolean) => void",
"d": "选中态变化回调,参数是点击后的新值"
}
],
"example": "tag({ label: '前端', variant: 'info', closable: true, onClose: () => remove('前端') })",
@@ -809,7 +825,8 @@
"按语义选 variant:info 中性标记、success 通过、warning 待处理、danger 已失效",
"closable: true 时必须配 onClose:把数据源里的该项删掉再重渲染(组件无状态,不自行摘除节点)",
"组合用法:容器里用 renderList 按信号数组渲染 tag 列表,删除即写回信号",
"标签是内联元素,可直接排进 .row 等容器,自动换行由容器决定"
"标签是内联元素,可直接排进 .row 等容器,自动换行由容器决定",
"需要可点的筛选片时传 checkable + checked:点击写回信号,视觉与 aria-pressed 同步,不做整块重渲染(焦点不丢)"
]
},
{
+31
View File
@@ -708,6 +708,37 @@
transform: scale(1.15);
}
/* —— 可选标签(筛选片)—— */
.ua-tag.is-checkable {
font: inherit;
color: var(--ua-color-text-muted);
background: transparent;
cursor: pointer;
transition: transform 0.15s, border-color 0.15s, background 0.15s, color 0.15s;
}
.ua-tag.is-checkable:hover {
transform: translateY(-1px);
color: var(--ua-color-text);
}
.ua-tag.is-checkable:active {
transform: scale(0.97);
}
.ua-tag.is-checkable:focus-visible {
outline: 2px solid color-mix(in srgb, var(--ua-color-accent) 45%, transparent);
outline-offset: 2px;
}
.ua-tag.is-checked {
border-color: color-mix(in srgb, var(--ua-color-accent) 55%, transparent);
background: color-mix(in srgb, var(--ua-color-accent) 10%, var(--ua-color-bg));
color: var(--ua-color-text);
}
@media (prefers-reduced-motion: reduce) {
.ua-tag.is-checkable { transition: none; }
.ua-tag.is-checkable:hover,
.ua-tag.is-checkable:active { transform: none; }
}
/* —— 滑块 —— */
.ua-slider {
display: inline-flex;