From d5e16027f8acb19324a2e5db5caad7d0a212b9ec Mon Sep 17 00:00:00 2001 From: HIsheR Date: Tue, 27 Oct 2026 02:25:59 +0800 Subject: [PATCH] =?UTF-8?q?iter(g343):=20=E6=A0=A1=E9=AA=8C=E7=BA=A2?= =?UTF-8?q?=E7=8E=AF=E6=94=B9=E4=B8=BA=E8=81=9A=E7=84=A6=E6=97=B6=E4=BA=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/demo/main.ts | 2 +- packages/skin/ui/ui.css | 10 +++++++++- packages/tools/cli/bin/ua.js | 27 +++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 975ab5b..5b44e74 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -585,7 +585,7 @@ const content = h( section( 'sec-formlab', '表单校验', - 'form + formItem:必填 / 长度 / 正则同步校验,validator 返回 Promise 即异步规则(提交会等待);全部通过才触发提交。校验不通过时顶部出现「有 N 项需要修改」汇总条(role=alert 会被读屏播报)并聚焦第一处——自绘控件(下拉 / 评分 / 开关)没有原生 input,焦点退到它的触发器,整块不可聚焦时落在字段容器上;onError 拿到不通字段数。错误文案带唯一 id 并被 aria-describedby 关联到控件,读屏除了「无效」还会念出具体原因。点「重置」逐项复位:原生输入框回初始值、错误态与汇总条一起清空,自绘控件(城市下拉)靠 formItem 的 reset 钩子回写信号。本例 columns: 2 走两列栅格,窄屏自动堆叠,按钮区始终整行;字段传 span 可跨列(备注整宽)。', + 'form + formItem:必填 / 长度 / 正则同步校验,validator 返回 Promise 即异步规则(提交会等待);全部通过才触发提交。校验不通过时顶部出现「有 N 项需要修改」汇总条(role=alert 会被读屏播报)并聚焦第一处——自绘控件(下拉 / 评分 / 开关)没有原生 input,焦点退到它的触发器,整块不可聚焦时落在字段容器上;onError 拿到不通字段数。错误文案带唯一 id 并被 aria-describedby 关联到控件,读屏除了「无效」还会念出具体原因。点「重置」逐项复位:原生输入框回初始值、错误态与汇总条一起清空,自绘控件(城市下拉)靠 formItem 的 reset 钩子回写信号。校验失败只把控件描红,危险色光环聚焦时才亮——常驻红环会让整片字段看着像一起报错。本例 columns: 2 走两列栅格,窄屏自动堆叠,按钮区始终整行;字段传 span 可跨列(备注整宽)。', form( { columns: 2, submitText: '注册', resetText: '重置', onSubmit: () => formResult(`提交成功:${username()} / ${email()} / ${city()}`) }, [ diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 608ac17..23132db 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -1866,7 +1866,7 @@ button.ua-step-dot:hover { .ua-form-error:empty { display: none; } -/* 校验失败:控件描红 + 危险色光环(root 与包裹式控件的内部触发器一并覆盖) */ +/* 校验失败:控件描红常驻(root 与包裹式控件的内部触发器一并覆盖) */ .ua-invalid, .ua-invalid .ua-input, .ua-invalid .ua-textarea, @@ -1874,6 +1874,14 @@ button.ua-step-dot:hover { .ua-invalid .ua-radio, .ua-invalid .ua-slider-input { border-color: var(--ua-color-danger) !important; +} +/* 危险色光环只在聚焦时亮:常驻 3px 红环会让长表单看着像整页报错,也会和相邻字段的环叠在一起 */ +.ua-invalid:focus-visible, +.ua-invalid .ua-input:focus-visible, +.ua-invalid .ua-textarea:focus-visible, +.ua-invalid .ua-select-trigger:focus-visible, +.ua-invalid .ua-radio:focus-visible, +.ua-invalid .ua-slider-input:focus-visible { box-shadow: var(--ua-focus-ring-danger) !important; } .ua-form-actions { diff --git a/packages/tools/cli/bin/ua.js b/packages/tools/cli/bin/ua.js index c792b1f..0640ffc 100644 --- a/packages/tools/cli/bin/ua.js +++ b/packages/tools/cli/bin/ua.js @@ -1700,6 +1700,32 @@ function checkFormReset(pkgs) { return issues; } +/*** 校验错误环:红环常驻会把长表单染成一片警报,也会与相邻字段的环叠加——环只该在聚焦时亮 ***/ +function checkInvalidRing(pkgs) { + const issues = []; + for (const pkg of pkgs) { + if (pkg.type !== 'skin-dom') continue; + const cssPath = path.join(ROOT, pkg.dir, 'ui.css'); + if (!fs.existsSync(cssPath)) continue; + const lines = fs.readFileSync(cssPath, 'utf8').split(/\r?\n/); + let hasFocusRing = false; // 聚焦态危险环是否已单独写过 + lines.forEach((line, i) => { + if (!line.trim().endsWith('{')) return; // 卫语句:只从选择器收尾行起算(一选择器一行是仓库既有排版) + let top = i; // 选择器首行:往上吃掉所有逗号收尾的续行 + while (top > 0 && lines[top - 1].trim().endsWith(',')) top--; + const selector = lines.slice(top, i + 1).join(' '); + if (!selector.includes('.ua-invalid')) return; // 卫语句:与本规则无关的块 + let body = ''; // 规则体:拼到第一行收尾的 } + for (let k = i + 1; k < lines.length && !lines[k].trim().startsWith('}'); k++) body += lines[k]; + if (!/focus-ring-danger/.test(body)) return; // 卫语句:本块不涉及危险环 + if (/focus/.test(selector)) hasFocusRing = true; + else issues.push(`${pkg.name}/ui.css:${i + 1} 的「${selector.trim().slice(0, 60)}」常驻危险光环:校验失败但未聚焦的字段也挂着 3px 红环,长表单看着像整页报错`); + }); + if (!hasFocusRing) issues.push(`${pkg.name}/ui.css 缺 .ua-invalid:focus-visible 的危险环:改完常驻环后聚焦态反而没有错误强调`); + } + return issues; +} + /*** 校验同义词表:目标 id 必须真实存在(改名后残留会让回退指到空气),同一个词不得归两组(歧义会让结果随表序漂移) ***/ function checkSynonyms(ids) { const issues = []; @@ -1784,6 +1810,7 @@ function cmdVerify() { issues.push(...checkToastLive(pkgs)); // toast 必须有 live 区域与 alert 抢占 issues.push(...checkFormErrorLink(pkgs)); // 表单错误文案必须被控件关联 issues.push(...checkFormReset(pkgs)); // 重置按钮必须真复位 + issues.push(...checkInvalidRing(pkgs)); // 错误环只该在聚焦时亮 issues.push(...checkSynonyms(new Set(pkgs.flatMap((p) => (p.docs?.components ?? []).map((c) => c.id))))); // 同义词表指向的组件必须真实存在 checkDarkTokens(issues); checkTokenParity(issues);