iter: d17 表单错误态覆盖包裹式控件 + 校验时机事件
This commit is contained in:
@@ -184,6 +184,14 @@
|
|||||||
"acceptance": "dataTable 渲染 colgroup:数字 width 落成固定像素 <col>,'flex' 或不设交给自适应,选择列固定 44px;demo 表格演示 name 160 / age 100 / city flex;manifest 列定义说明与手把手同步;npm run check 全绿",
|
"acceptance": "dataTable 渲染 colgroup:数字 width 落成固定像素 <col>,'flex' 或不设交给自适应,选择列固定 44px;demo 表格演示 name 160 / age 100 / city flex;manifest 列定义说明与手把手同步;npm run check 全绿",
|
||||||
"status": "done",
|
"status": "done",
|
||||||
"notes": "用 colgroup 而非逐 th 设宽,保证表头与表体一致且不入侵单元格渲染;未开 table-layout:fixed(nowrap 内容会被裁),宽高仍由内容兜底;pinned 固定列仍未消费(多列 sticky 偏移需测量),留作后续条目"
|
"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,提交结果显示城市"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-23T08:58:43.429Z",
|
"generatedAt": "2026-10-23T08:59:45.207Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -1205,7 +1205,9 @@
|
|||||||
"把句柄数组交给 form:form({ onSubmit }, items),form 渲染全部字段 + 提交/重置按钮",
|
"把句柄数组交给 form:form({ onSubmit }, items),form 渲染全部字段 + 提交/重置按钮",
|
||||||
"提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项",
|
"提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项",
|
||||||
"规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)",
|
"规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)",
|
||||||
"非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)"
|
"非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)",
|
||||||
|
"错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致",
|
||||||
|
"触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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: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: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: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 全绿"}
|
||||||
|
|||||||
+2
-1
@@ -268,10 +268,11 @@ const content = h(
|
|||||||
'表单校验',
|
'表单校验',
|
||||||
'form + formItem:必填 / 长度 / 正则校验,全部通过才触发提交;失焦校验、输入清错。',
|
'form + formItem:必填 / 长度 / 正则校验,全部通过才触发提交;失焦校验、输入清错。',
|
||||||
form(
|
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: 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: 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() || '等待提交…'),
|
h('span', { class: 'out' }, () => formResult() || '等待提交…'),
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @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)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -679,6 +679,8 @@ const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label:
|
|||||||
4. 提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项
|
4. 提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项
|
||||||
5. 规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)
|
5. 规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)
|
||||||
6. 非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)
|
6. 非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)
|
||||||
|
7. 错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致
|
||||||
|
8. 触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-formlab
|
现场演示:apps/docs/demo.html#sec-formlab
|
||||||
|
|
||||||
|
|||||||
@@ -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 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));
|
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<HTMLElement>('input, textarea, select, .ua-select-trigger')),
|
||||||
|
];
|
||||||
|
|
||||||
|
/*** 执行全部规则:返回首个错误消息,同时同步错误区、控件态与 aria-invalid ***/
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
const value = props.value();
|
const value = props.value();
|
||||||
let msg: string | null = null;
|
let msg: string | null = null;
|
||||||
@@ -36,14 +42,18 @@ export function formItem(props: FormItemProps, control: HTMLElement): FormItemHa
|
|||||||
}
|
}
|
||||||
error.textContent = msg ?? '';
|
error.textContent = msg ?? '';
|
||||||
control.classList.toggle('ua-invalid', !!msg);
|
control.classList.toggle('ua-invalid', !!msg);
|
||||||
|
for (const node of marked()) node.setAttribute('aria-invalid', String(!!msg)); // 读屏同步错误态
|
||||||
return msg;
|
return msg;
|
||||||
};
|
};
|
||||||
|
|
||||||
control.addEventListener('input', () => {
|
control.addEventListener('input', () => {
|
||||||
if (mode === 'input' || error.textContent) validate(); // input 模式即时校验;其余模式仅清已报错误
|
if (mode === 'input' || error.textContent) validate(); // input 模式即时校验;其余模式仅清已报错误
|
||||||
});
|
});
|
||||||
control.addEventListener('blur', () => {
|
control.addEventListener('change', () => {
|
||||||
if (mode !== 'submit') validate();
|
if (mode === 'input') validate(); // select / checkbox / radio 变更即校验(原生 input 事件不冒泡)
|
||||||
|
});
|
||||||
|
control.addEventListener('focusout', () => {
|
||||||
|
if (mode !== 'submit') validate(); // 用会冒泡的 focusout 取代 blur,包裹式控件同样生效
|
||||||
});
|
});
|
||||||
return { el, validate };
|
return { el, validate };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1076,7 +1076,9 @@
|
|||||||
"把句柄数组交给 form:form({ onSubmit }, items),form 渲染全部字段 + 提交/重置按钮",
|
"把句柄数组交给 form:form({ onSubmit }, items),form 渲染全部字段 + 提交/重置按钮",
|
||||||
"提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项",
|
"提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项",
|
||||||
"规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)",
|
"规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)",
|
||||||
"非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)"
|
"非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)",
|
||||||
|
"错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致",
|
||||||
|
"触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1003,8 +1003,13 @@
|
|||||||
.ua-form-error:empty {
|
.ua-form-error:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
/* 校验失败:控件描红 + 危险色光环 */
|
/* 校验失败:控件描红 + 危险色光环(root 与包裹式控件的内部触发器一并覆盖) */
|
||||||
.ua-invalid {
|
.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;
|
border-color: var(--ua-color-danger) !important;
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 15%, transparent) !important;
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 15%, transparent) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user