iter(g40): 修表单双提交并支持 Enter 提交

This commit is contained in:
HIsheR
2026-10-26 11:51:41 +08:00
parent 78779d7bb6
commit d685f66527
8 changed files with 45 additions and 13 deletions
+8
View File
@@ -676,6 +676,14 @@
"acceptance": "ui.css 顶部统一 [class^=ua-]:focus-visible 描边兜底(不引入硬编码色);README 增「可访问性约定」章节;npm run check 全绿", "acceptance": "ui.css 顶部统一 [class^=ua-]:focus-visible 描边兜底(不引入硬编码色);README 增「可访问性约定」章节;npm run check 全绿",
"status": "done", "status": "done",
"notes": "自补:a11y;避免组件各自 outline:none 丢失焦点指示" "notes": "自补:a11y;避免组件各自 outline:none 丢失焦点指示"
},
{
"id": "g40",
"title": "修表单双提交:button 显式 type + Enter 提交",
"motivation": "button() 不写 type,放进 <form> 里默认是 submit:点提交会先跑 click 回调再触发原生 submit(onSubmit 触发两次),点「重置」还会顺带提交表单",
"acceptance": "ButtonProps 增 type(缺省 button);form 内提交按钮用 submit、重置用 button;单行输入框 Enter 等同提交且 textarea 不受影响;文档同步;npm run check 全绿",
"status": "open",
"notes": "自补:既有组件缺陷修复"
} }
] ]
} }
+11 -4
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "ui-arch-registry-index/0.1", "$schema": "ui-arch-registry-index/0.1",
"generatedAt": "2026-10-26T03:49:26.217Z", "generatedAt": "2026-10-26T03:51:18.113Z",
"repoRoot": ".", "repoRoot": ".",
"packages": [ "packages": [
{ {
@@ -142,7 +142,7 @@
"api": { "api": {
"entry": "src/index.ts", "entry": "src/index.ts",
"exports": [ "exports": [
"button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用", "button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交)",
"textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块,clearable 开启后有值即显一键清空按钮", "textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块,clearable 开启后有值即显一键清空按钮",
"inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节(长按连发步进),min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块", "inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节(长按连发步进),min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块",
"textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块,maxCount 显示实时字数并设原生上限", "textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块,maxCount 显示实时字数并设原生上限",
@@ -298,6 +298,11 @@
"d": "视觉强度;缺省 primary", "d": "视觉强度;缺省 primary",
"df": "primary" "df": "primary"
}, },
{
"n": "type",
"t": "'button' | 'submit' | 'reset'",
"d": "原生类型;缺省 button,表单内不会意外提交"
},
{ {
"n": "disabled", "n": "disabled",
"t": "boolean | Signal<boolean>", "t": "boolean | Signal<boolean>",
@@ -314,7 +319,8 @@
"steps": [ "steps": [
"确定语义强度:主操作用 primary(默认),次要操作用 ghost,危险操作用 danger", "确定语义强度:主操作用 primary(默认),次要操作用 ghost,危险操作用 danger",
"绑定 onClick 回调处理点击逻辑", "绑定 onClick 回调处理点击逻辑",
"需要禁用态时自行在回调首行卫语句判断,或外层包条件渲染" "需要禁用态时自行在回调首行卫语句判断,或外层包条件渲染",
"放进 <form> 里需要提交时显式传 type: \"submit\";缺省 button 不会触发原生提交"
] ]
}, },
{ {
@@ -1539,7 +1545,8 @@
"规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)", "规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)",
"非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)", "非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)",
"错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致", "错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致",
"触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验" "触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验",
"单行输入框内按 Enter 等同点击提交(textarea 内仍是换行);提交按钮为 type=\"submit\",不会与点击回调重复触发"
] ]
}, },
{ {
+5 -2
View File
@@ -1,6 +1,6 @@
# @ui-arch 组件库 Agent 文档(无图形版) # @ui-arch 组件库 Agent 文档(无图形版)
> 自动生成于 2026-10-26T03:49:26.217Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 自动生成于 2026-10-26T03:51:18.113Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
## 快速上手 ## 快速上手
@@ -80,6 +80,7 @@ image({ src: '/a.png', alt: '架构图', width: 160 })
- label(必填):string —— 按钮文案 - label(必填):string —— 按钮文案
- variant(默认 primary):'primary' | 'ghost' | 'danger' —— 视觉强度;缺省 primary - variant(默认 primary):'primary' | 'ghost' | 'danger' —— 视觉强度;缺省 primary
- type:'button' | 'submit' | 'reset' —— 原生类型;缺省 button,表单内不会意外提交
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步 - disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onClick(回调):() => void —— 点击回调 - onClick(回调):() => void —— 点击回调
@@ -94,6 +95,7 @@ button({ label: '提交', variant: 'primary', onClick: () => console.log('clicke
1. 确定语义强度:主操作用 primary(默认),次要操作用 ghost,危险操作用 danger 1. 确定语义强度:主操作用 primary(默认),次要操作用 ghost,危险操作用 danger
2. 绑定 onClick 回调处理点击逻辑 2. 绑定 onClick 回调处理点击逻辑
3. 需要禁用态时自行在回调首行卫语句判断,或外层包条件渲染 3. 需要禁用态时自行在回调首行卫语句判断,或外层包条件渲染
4. 放进 <form> 里需要提交时显式传 type: "submit";缺省 button 不会触发原生提交
现场演示:apps/docs/demo.html#sec-counter 现场演示:apps/docs/demo.html#sec-counter
@@ -867,6 +869,7 @@ const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label:
6. 非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程) 6. 非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)
7. 错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致 7. 错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致
8. 触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验 8. 触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验
9. 单行输入框内按 Enter 等同点击提交(textarea 内仍是换行);提交按钮为 type="submit",不会与点击回调重复触发
现场演示:apps/docs/demo.html#sec-formlab 现场演示:apps/docs/demo.html#sec-formlab
@@ -1170,7 +1173,7 @@ result({ status: 'success', title: '提交成功', description: '工单 #2049
API 速览: API 速览:
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用 - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交)
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块,clearable 开启后有值即显一键清空按钮 - textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块,clearable 开启后有值即显一键清空按钮
- inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节(长按连发步进),min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块 - inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节(长按连发步进),min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块,maxCount 显示实时字数并设原生上限 - textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块,maxCount 显示实时字数并设原生上限
+1 -1
View File
@@ -47,7 +47,7 @@
- 类型: skin-dom · 目录: packages/skin/ui - 类型: skin-dom · 目录: packages/skin/ui
- 描述: DOM 皮肤组件库:表单 / 反馈 / 浮层 / 导航 / 数据展示,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌(组件全量见 api.exports 或刚体清单) - 描述: DOM 皮肤组件库:表单 / 反馈 / 浮层 / 导航 / 数据展示,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌(组件全量见 api.exports 或刚体清单)
- API: - API:
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用 - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交)
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块,clearable 开启后有值即显一键清空按钮 - textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块,clearable 开启后有值即显一键清空按钮
- inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节(长按连发步进),min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块 - inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节(长按连发步进),min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块,maxCount 显示实时字数并设原生上限 - textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块,maxCount 显示实时字数并设原生上限
+1 -1
View File
@@ -5,7 +5,7 @@ import type { ButtonProps } from './types.js';
export function button(props: ButtonProps): HTMLElement { export function button(props: ButtonProps): HTMLElement {
return h( return h(
'button', 'button',
{ class: `ua-btn ua-btn-${props.variant ?? 'primary'}`, disabled: props.disabled, onClick: props.onClick }, // false/undefined 移除属性,Signal 走响应式 { class: `ua-btn ua-btn-${props.variant ?? 'primary'}`, type: props.type ?? 'button', disabled: props.disabled, onClick: props.onClick }, // 缺省 button:表单内不隐式提交
props.label, props.label,
); );
} }
+8 -2
View File
@@ -71,8 +71,8 @@ export function form(props: FormProps, items: FormItemHandle[]): HTMLElement {
} }
props.onSubmit?.(); props.onSubmit?.();
}; };
const submitBtn = button({ label: props.submitText ?? '提交', onClick: submit }); const submitBtn = button({ label: props.submitText ?? '提交', type: 'submit' }); // 走原生 submit,避免 click + submit 双触发
const resetBtn = props.resetText ? button({ label: props.resetText, variant: 'ghost', onClick: props.onReset }) : null; const resetBtn = props.resetText ? button({ label: props.resetText, variant: 'ghost', type: 'button', onClick: props.onReset }) : null; // 重置不得提交表单
const actions = h('div', { class: 'ua-form-actions' }, submitBtn, resetBtn); const actions = h('div', { class: 'ua-form-actions' }, submitBtn, resetBtn);
const el = h( const el = h(
'form', 'form',
@@ -84,5 +84,11 @@ export function form(props: FormProps, items: FormItemHandle[]): HTMLElement {
e.preventDefault(); // 拦截原生提交,走校验流程 e.preventDefault(); // 拦截原生提交,走校验流程
submit(); submit();
}); });
el.addEventListener('keydown', (e) => {
if (e.key !== 'Enter' || e.target instanceof HTMLTextAreaElement) return; // 多行输入框允许换行
if (!(e.target instanceof HTMLInputElement)) return; // 只接管单行输入框
e.preventDefault(); // 拦下浏览器隐式提交,统一走校验
submit();
});
return el; return el;
} }
+1
View File
@@ -22,6 +22,7 @@ export interface PopoverProps {
export interface ButtonProps { export interface ButtonProps {
label: string; // 按钮文案 label: string; // 按钮文案
variant?: 'primary' | 'ghost' | 'danger'; // 视觉强度;缺省 primary variant?: 'primary' | 'ghost' | 'danger'; // 视觉强度;缺省 primary
type?: 'button' | 'submit' | 'reset'; // 原生类型;缺省 button(表单内不会意外触发提交)
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步 disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
onClick?: () => void; // 点击回调 onClick?: () => void; // 点击回调
} }
+10 -3
View File
@@ -13,7 +13,7 @@
"api": { "api": {
"entry": "src/index.ts", "entry": "src/index.ts",
"exports": [ "exports": [
"button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用", "button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交)",
"textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块,clearable 开启后有值即显一键清空按钮", "textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块,clearable 开启后有值即显一键清空按钮",
"inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节(长按连发步进),min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块", "inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节(长按连发步进),min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块",
"textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块,maxCount 显示实时字数并设原生上限", "textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块,maxCount 显示实时字数并设原生上限",
@@ -169,6 +169,11 @@
"d": "视觉强度;缺省 primary", "d": "视觉强度;缺省 primary",
"df": "primary" "df": "primary"
}, },
{
"n": "type",
"t": "'button' | 'submit' | 'reset'",
"d": "原生类型;缺省 button,表单内不会意外提交"
},
{ {
"n": "disabled", "n": "disabled",
"t": "boolean | Signal<boolean>", "t": "boolean | Signal<boolean>",
@@ -185,7 +190,8 @@
"steps": [ "steps": [
"确定语义强度:主操作用 primary(默认),次要操作用 ghost,危险操作用 danger", "确定语义强度:主操作用 primary(默认),次要操作用 ghost,危险操作用 danger",
"绑定 onClick 回调处理点击逻辑", "绑定 onClick 回调处理点击逻辑",
"需要禁用态时自行在回调首行卫语句判断,或外层包条件渲染" "需要禁用态时自行在回调首行卫语句判断,或外层包条件渲染",
"放进 <form> 里需要提交时显式传 type: \"submit\";缺省 button 不会触发原生提交"
] ]
}, },
{ {
@@ -1410,7 +1416,8 @@
"规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)", "规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)",
"非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)", "非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)",
"错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致", "错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致",
"触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验" "触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验",
"单行输入框内按 Enter 等同点击提交(textarea 内仍是换行);提交按钮为 type=\"submit\",不会与点击回调重复触发"
] ]
}, },
{ {