iter(g296): 修正表单校验失败仍提交
This commit is contained in:
@@ -3050,6 +3050,15 @@
|
||||
"motivation": "对比度门禁只管「文字 vs 底色」,管不到「底色 vs 底色」;实测表面→二阶表面亮度比只有 1.008(暗色 1.006),即悬停底与表面几乎同色,6 处 surface-2 与新建的列面板悬停都白改。",
|
||||
"acceptance": "① ua audit:surface 按四套主题逐档验阶梯(页面底→表面→悬停底→按下底 ≥1.04,凹陷底 ≥1.02),不达标退出码 1,负向自检确认会红;② surface-2/3 改为从 surface 派生并拉开档距;③ audit:surface 进 check 链、package.json 脚本、usage 串与 doctor 的 check 链检查;④ 列面板悬停改用 surface-2(原先撞 .ua-pop 同底色);⑤ audit:contrast 与 audit:visual 仍全绿。",
|
||||
"notes": "新 lib/surface-audit.mjs:四套主题 × 四段阶梯算亮度比(1.04 / 凹陷 1.02),--json/--list 双出口,进 check 链与 doctor;tokens surface-2/3 改为从 surface 派生(亮 6%/14%,暗 7%/17%),阶梯由 1.008→1.123(暗 1.006→1.193);列面板悬停改用 surface-2(原与 .ua-pop 同底色等于没反馈);contrast-audit 导出 resolve/ratio 供复用"
|
||||
},
|
||||
{
|
||||
"id": "g296",
|
||||
"domain": "tune",
|
||||
"status": "open",
|
||||
"title": "修 form 校验失败仍提交:自绘控件字段没有原生 input 时守卫失效",
|
||||
"motivation": "提交守卫写成「找到焦点目标才中止」,而焦点目标只查 input/textarea/select;非搜索式下拉、评分、开关容器里没有原生 input,firstBad 为 null 时校验有错也照样调 onSubmit(demo 的「城市」必填下拉即可复现)。",
|
||||
"acceptance": "① 中止判据改为错误条数 bad > 0;② 焦点落点按 控件 → 触发器(button/[tabindex]) → 字段容器(tabIndex=-1) 三级退让;③ manifest 的 form 步骤与 demo 描述同步说明;④ npm run check 全绿。",
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "ui-arch-registry-index/0.1",
|
||||
"generatedAt": "2026-10-26T15:31:57.268Z",
|
||||
"generatedAt": "2026-10-26T15:36:58.959Z",
|
||||
"repoRoot": ".",
|
||||
"packages": [
|
||||
{
|
||||
@@ -2034,7 +2034,8 @@
|
||||
"需要给字段加格式说明就传 hint:平时显示在控件下方,校验失败时自动让位给错误文案,同一位置不堆两行",
|
||||
"字段多的编辑页传 columns: 2 或 3 走多列栅格:列间距与窄屏堆叠沿用 grid,按钮区始终整行;单列表单不要传(默认 1)",
|
||||
"多列表单里想整宽就传 span:formItem({ span: 2 }, textarea(...)) 会占满两列,窄屏单列时自动忽略跨列(否则会撑出横向滚动)",
|
||||
"字段要按另一个字段的值决定是否出现(选了「其他」才出现输入框),给 formItem 传 visible 信号:隐藏项自动清掉错误态并退出校验,不用自己在 items 数组里增删"
|
||||
"字段要按另一个字段的值决定是否出现(选了「其他」才出现输入框),给 formItem 传 visible 信号:隐藏项自动清掉错误态并退出校验,不用自己在 items 数组里增删",
|
||||
"校验失败必定中止提交:判据是错误条数而不是「有没有找到焦点目标」,所以只有自绘控件(非搜索式下拉 / 评分 / 开关,容器里没有原生 input)的字段也能拦住;焦点定位顺序是控件 → 触发器 → 字段容器(后者临时 tabindex=-1,只接受程序聚焦,不污染 Tab 序)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -579,7 +579,7 @@ const content = h(
|
||||
section(
|
||||
'sec-formlab',
|
||||
'表单校验',
|
||||
'form + formItem:必填 / 长度 / 正则同步校验,validator 返回 Promise 即异步规则(提交会等待);全部通过才触发提交。校验不通过时顶部出现「有 N 项需要修改」汇总条(role=alert 会被读屏播报)并聚焦第一处,onError 拿到不通字段数。本例 columns: 2 走两列栅格,窄屏自动堆叠,按钮区始终整行;字段传 span 可跨列(备注整宽)。',
|
||||
'form + formItem:必填 / 长度 / 正则同步校验,validator 返回 Promise 即异步规则(提交会等待);全部通过才触发提交。校验不通过时顶部出现「有 N 项需要修改」汇总条(role=alert 会被读屏播报)并聚焦第一处——自绘控件(下拉 / 评分 / 开关)没有原生 input,焦点退到它的触发器,整块不可聚焦时落在字段容器上;onError 拿到不通字段数。本例 columns: 2 走两列栅格,窄屏自动堆叠,按钮区始终整行;字段传 span 可跨列(备注整宽)。',
|
||||
form(
|
||||
{ columns: 2, submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()} / ${city()}`) },
|
||||
[
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||
|
||||
> 自动生成于 2026-10-26T15:31:57.268Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||
> 自动生成于 2026-10-26T15:36:58.959Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||
|
||||
## 快速上手
|
||||
@@ -1010,6 +1010,7 @@ const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label:
|
||||
13. 字段多的编辑页传 columns: 2 或 3 走多列栅格:列间距与窄屏堆叠沿用 grid,按钮区始终整行;单列表单不要传(默认 1)
|
||||
14. 多列表单里想整宽就传 span:formItem({ span: 2 }, textarea(...)) 会占满两列,窄屏单列时自动忽略跨列(否则会撑出横向滚动)
|
||||
15. 字段要按另一个字段的值决定是否出现(选了「其他」才出现输入框),给 formItem 传 visible 信号:隐藏项自动清掉错误态并退出校验,不用自己在 items 数组里增删
|
||||
16. 校验失败必定中止提交:判据是错误条数而不是「有没有找到焦点目标」,所以只有自绘控件(非搜索式下拉 / 评分 / 开关,容器里没有原生 input)的字段也能拦住;焦点定位顺序是控件 → 触发器 → 字段容器(后者临时 tabindex=-1,只接受程序聚焦,不污染 Tab 序)
|
||||
|
||||
现场演示:apps/docs/demo.html#sec-formlab
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ async function checkRuleAsync(value: string, rule: FormRule): Promise<string | n
|
||||
return r === true ? null : typeof r === 'string' ? r : rule.message ?? '校验未通过';
|
||||
}
|
||||
|
||||
/*** 字段容器里的焦点落点:自绘控件(非搜索式下拉、评分、开关)没有原生 input,只查 input 会拿到 null,退到按钮 / 带 tabindex 的节点 ***/
|
||||
const focusableOf = (box: HTMLElement): HTMLElement | null =>
|
||||
box.querySelector<HTMLElement>('input:not([type="hidden"]), textarea, select, button, [tabindex]:not([tabindex="-1"])');
|
||||
|
||||
/*** 表单字段:label + 控件插槽 + 错误区;返回句柄(同步 / 异步两种校验)供 form 聚合 ***/
|
||||
export function formItem(props: FormItemProps, control: HTMLElement): FormItemHandle {
|
||||
// —— 状态集中声明 ——
|
||||
@@ -120,13 +124,15 @@ export function form(props: FormProps, items: FormItemHandle[]): HTMLElement {
|
||||
if (!msg) continue; // 卫语句:本项通过
|
||||
bad += 1;
|
||||
if (firstBad) continue; // 只把首个错误控件留作焦点目标
|
||||
firstBad = item.el.querySelector('input, textarea, select') as HTMLElement | null;
|
||||
const target = focusableOf(item.el);
|
||||
if (!target) item.el.tabIndex = -1; // 整块无可聚焦控件时把字段容器本身设为焦点落点(-1 不进 Tab 序,只接受程序聚焦)
|
||||
firstBad = target ?? item.el;
|
||||
}
|
||||
submitBtn.disabled = false;
|
||||
running = false;
|
||||
summary.hidden = bad === 0;
|
||||
summary.textContent = bad ? `有 ${bad} 项需要修改,已定位到第一处` : '';
|
||||
if (firstBad) { firstBad.focus(); props.onError?.(bad); return; } // 卫语句:有错误不提交
|
||||
if (bad > 0) { firstBad?.focus(); props.onError?.(bad); return; } // 有错即中止:判据用错误数而非焦点目标,否则自绘控件字段会漏过去直接提交
|
||||
props.onSubmit?.();
|
||||
};
|
||||
|
||||
|
||||
@@ -1905,7 +1905,8 @@
|
||||
"需要给字段加格式说明就传 hint:平时显示在控件下方,校验失败时自动让位给错误文案,同一位置不堆两行",
|
||||
"字段多的编辑页传 columns: 2 或 3 走多列栅格:列间距与窄屏堆叠沿用 grid,按钮区始终整行;单列表单不要传(默认 1)",
|
||||
"多列表单里想整宽就传 span:formItem({ span: 2 }, textarea(...)) 会占满两列,窄屏单列时自动忽略跨列(否则会撑出横向滚动)",
|
||||
"字段要按另一个字段的值决定是否出现(选了「其他」才出现输入框),给 formItem 传 visible 信号:隐藏项自动清掉错误态并退出校验,不用自己在 items 数组里增删"
|
||||
"字段要按另一个字段的值决定是否出现(选了「其他」才出现输入框),给 formItem 传 visible 信号:隐藏项自动清掉错误态并退出校验,不用自己在 items 数组里增删",
|
||||
"校验失败必定中止提交:判据是错误条数而不是「有没有找到焦点目标」,所以只有自绘控件(非搜索式下拉 / 评分 / 开关,容器里没有原生 input)的字段也能拦住;焦点定位顺序是控件 → 触发器 → 字段容器(后者临时 tabindex=-1,只接受程序聚焦,不污染 Tab 序)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user