From bdc1fec3f93f547e730d8e6dda21cc2f718edb46 Mon Sep 17 00:00:00 2001 From: ui-arch-bot Date: Fri, 23 Oct 2026 16:59:46 +0800 Subject: [PATCH] =?UTF-8?q?iter:=20d17=20=E8=A1=A8=E5=8D=95=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=80=81=E8=A6=86=E7=9B=96=E5=8C=85=E8=A3=B9=E5=BC=8F?= =?UTF-8?q?=E6=8E=A7=E4=BB=B6=20+=20=E6=A0=A1=E9=AA=8C=E6=97=B6=E6=9C=BA?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/backlog.json | 8 ++++++++ .ua/index.json | 6 ++++-- .ua/iterations.log | 1 + apps/demo/main.ts | 3 ++- llms-full.txt | 4 +++- packages/skin/ui/src/form.ts | 16 +++++++++++++--- packages/skin/ui/ua.manifest.json | 4 +++- packages/skin/ui/ui.css | 9 +++++++-- 8 files changed, 41 insertions(+), 10 deletions(-) diff --git a/.ua/backlog.json b/.ua/backlog.json index 7e789c7..df04310 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -184,6 +184,14 @@ "acceptance": "dataTable 渲染 colgroup:数字 width 落成固定像素 ,'flex' 或不设交给自适应,选择列固定 44px;demo 表格演示 name 160 / age 100 / city flex;manifest 列定义说明与手把手同步;npm run check 全绿", "status": "done", "notes": "用 colgroup 而非逐 th 设宽,保证表头与表体一致且不入侵单元格渲染;未开 table-layout:fixed(nowrap 内容会被裁),宽高仍由内容兜底;pinned 固定列仍未消费(多列 sticky 偏移需测量),留作后续条目" + }, + { + "id": "d17", + "title": "表单错误态覆盖包裹式控件并修正校验时机事件", + "motivation": "① 现有 blur 不冒泡,checkbox/select/radioGroup 这类包裹式控件失焦永远不校验;② ua-invalid 只加在控件根,自绘 select 的边框在内部触发器上,视觉上无错误提示;③ 缺 aria-invalid,读屏感知不到", + "acceptance": "formItem 用 focusout 取代 blur、补 change(select/checkbox 变更即校验);错误态同时落到控件根与内部原生控件,并同步 aria-invalid;CSS 覆盖 .ua-invalid 后代 .ua-input/.ua-textarea/.ua-select-trigger/.ua-radio/.ua-slider-input;demo 表单加必填 select 演示;manifest/文档/llms-full.txt 同步;npm run check 全绿", + "status": "done", + "notes": "marked() 统一收集控件根 + 内部 input/textarea/select/.ua-select-trigger 两处落点;.ua-invalid 后代选择器让包裹式控件描红,无需组件各自改签名(低侵入);demo formlab 新增城市必填 select,提交结果显示城市" } ] } diff --git a/.ua/index.json b/.ua/index.json index 136ac65..d8660c5 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-23T08:58:43.429Z", + "generatedAt": "2026-10-23T08:59:45.207Z", "repoRoot": ".", "packages": [ { @@ -1205,7 +1205,9 @@ "把句柄数组交给 form:form({ onSubmit }, items),form 渲染全部字段 + 提交/重置按钮", "提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项", "规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)", - "非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)" + "非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)", + "错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致", + "触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验" ] }, { diff --git a/.ua/iterations.log b/.ua/iterations.log index f520720..0246e0c 100644 --- a/.ua/iterations.log +++ b/.ua/iterations.log @@ -21,3 +21,4 @@ {"time": "2026-10-23T16:56:36+08:00", "id": "d14", "result": "done", "summary": "新增抽屉 drawer:open 信号驱动侧滑、placement/width 可配、footer 操作区、遮罩/×/Esc 关闭并聚焦首元素;demo/manifest/llms 同步,check 全绿"} {"time": "2026-10-23T16:57:22+08:00", "id": "d15", "result": "done", "summary": "CLI 包补 ua.manifest.json(type: tooling,七个子命令 + 约定),注册表 5 包;修正 AGENTS/ARCHITECTURE 过期组件数 17→27;check 全绿"} {"time": "2026-10-23T16:58:51+08:00", "id": "d16", "result": "done", "summary": "dataTable 消费内核列宽 width:colgroup 固定像素 / flex 自适应,选择列 44px;demo 三列演示宽档;manifest/llms 同步,check 全绿"} +{"time": "2026-10-23T16:59:46+08:00", "id": "d17", "result": "done", "summary": "表单错误态打通包裹式控件:focusout/change 取代 blur + aria-invalid + .ua-invalid 后代选择器覆盖 select/radio/slider;demo 加必填 select;check 全绿"} diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 9adbe9a..b8a1007 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -268,10 +268,11 @@ const content = h( '表单校验', 'form + formItem:必填 / 长度 / 正则校验,全部通过才触发提交;失焦校验、输入清错。', form( - { submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()}`) }, + { submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()} / ${city()}`) }, [ formItem({ label: '用户名', required: true, value: username, rules: [{ min: 2, max: 12, message: '用户名需 2-12 个字符' }] }, textField({ placeholder: '2-12 个字符', value: username })), formItem({ label: '邮箱', required: true, value: email, rules: [{ pattern: /^[\w.-]+@[\w-]+\.[\w.]+$/, message: '邮箱格式不正确' }] }, textField({ placeholder: 'you@example.com', value: email })), + formItem({ label: '城市', required: true, value: city }, select({ options: cities.map((c) => ({ value: c, label: c })), value: city, placeholder: '请选择城市…' })), ], ), h('span', { class: 'out' }, () => formResult() || '等待提交…'), diff --git a/llms-full.txt b/llms-full.txt index 34be8cc..7a66a54 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-23T08:58:43.429Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-23T08:59:45.207Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -679,6 +679,8 @@ const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label: 4. 提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项 5. 规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误) 6. 非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程) +7. 错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致 +8. 触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验 现场演示:apps/docs/demo.html#sec-formlab diff --git a/packages/skin/ui/src/form.ts b/packages/skin/ui/src/form.ts index 489029c..4beebcb 100644 --- a/packages/skin/ui/src/form.ts +++ b/packages/skin/ui/src/form.ts @@ -26,7 +26,13 @@ export function formItem(props: FormItemProps, control: HTMLElement): FormItemHa const label = h('label', { class: 'ua-form-label' }, props.label, props.required ? h('i', { class: 'ua-form-req' }, '*') : null); const el = h('div', { class: 'ua-form-item' }, label, h('div', { class: 'ua-form-control' }, control, error)); - /*** 执行全部规则:返回首个错误消息,同时同步错误区与控件态 ***/ + /*** 错误态落点:控件根 + 内部原生控件,保证 select / checkbox 这类包裹式控件也能标红 ***/ + const marked = (): HTMLElement[] => [ + control, + ...Array.from(control.querySelectorAll('input, textarea, select, .ua-select-trigger')), + ]; + + /*** 执行全部规则:返回首个错误消息,同时同步错误区、控件态与 aria-invalid ***/ const validate = () => { const value = props.value(); let msg: string | null = null; @@ -36,14 +42,18 @@ export function formItem(props: FormItemProps, control: HTMLElement): FormItemHa } error.textContent = msg ?? ''; control.classList.toggle('ua-invalid', !!msg); + for (const node of marked()) node.setAttribute('aria-invalid', String(!!msg)); // 读屏同步错误态 return msg; }; control.addEventListener('input', () => { if (mode === 'input' || error.textContent) validate(); // input 模式即时校验;其余模式仅清已报错误 }); - control.addEventListener('blur', () => { - if (mode !== 'submit') validate(); + control.addEventListener('change', () => { + if (mode === 'input') validate(); // select / checkbox / radio 变更即校验(原生 input 事件不冒泡) + }); + control.addEventListener('focusout', () => { + if (mode !== 'submit') validate(); // 用会冒泡的 focusout 取代 blur,包裹式控件同样生效 }); return { el, validate }; } diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 0f11661..f48e1e1 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -1076,7 +1076,9 @@ "把句柄数组交给 form:form({ onSubmit }, items),form 渲染全部字段 + 提交/重置按钮", "提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项", "规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)", - "非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)" + "非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)", + "错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致", + "触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验" ] }, { diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index a372aa5..3e9ffed 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -1003,8 +1003,13 @@ .ua-form-error:empty { display: none; } -/* 校验失败:控件描红 + 危险色光环 */ -.ua-invalid { +/* 校验失败:控件描红 + 危险色光环(root 与包裹式控件的内部触发器一并覆盖) */ +.ua-invalid, +.ua-invalid .ua-input, +.ua-invalid .ua-textarea, +.ua-invalid .ua-select-trigger, +.ua-invalid .ua-radio, +.ua-invalid .ua-slider-input { border-color: var(--ua-color-danger) !important; box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 15%, transparent) !important; }