iter(g335): 输入类组件补可访问名

This commit is contained in:
HIsheR
2026-10-27 02:09:25 +08:00
parent dcf3614d5c
commit 19d5e90006
9 changed files with 71 additions and 10 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ export function inputNumber(props: InputNumberProps): HTMLElement {
const clamp = (v: number): number => Math.min(max, Math.max(min, v)); // 任何来源的值都夹回合法区间
const quantize = (n: number): number => (props.precision === undefined ? n : Number(n.toFixed(props.precision))); // precision 归一(顺带消除步进浮点尾差)
const emit = (v: number): void => { const n = quantize(clamp(v)); props.value(n); if (props.onChange) props.onChange(n); }; // 写回信号并回调
const input = h('input', { class: `ua-input ua-input-number-field${props.size && props.size !== 'md' ? ` is-${props.size}` : ''}`, inputmode: 'decimal', disabled: props.disabled, 'aria-label': '数字输入' }) as HTMLInputElement;
const input = h('input', { class: `ua-input ua-input-number-field${props.size && props.size !== 'md' ? ` is-${props.size}` : ''}`, inputmode: 'decimal', disabled: props.disabled, 'aria-label': props.ariaLabel ?? '数字输入' }) as HTMLInputElement;
const sync = (): void => { const text = props.precision === undefined ? String(props.value()) : props.value().toFixed(props.precision); if (input.value !== text) input.value = text; }; // 外部变化写回输入框(precision 定长显示)
effect(sync);
input.addEventListener('change', () => { const n = parseFloat(input.value); if (Number.isNaN(n)) sync(); else emit(n); }); // 非法输入回滚当前值
+5 -5
View File
@@ -13,8 +13,8 @@ export function slider(props: SliderProps): HTMLElement {
const pct = (v: number): number => ((v - min) / (max - min || 1)) * 100; // 分母兜底,避免 min=max 时除零
/*** 建一条原生 range:区间模式靠 CSS 叠在同一格,只有拇指可命中 ***/
const track = (sig: typeof value, onInput: (n: number) => void): HTMLInputElement => {
const input = h('input', { type: 'range', class: 'ua-slider-input', min, max, step, disabled }) as HTMLInputElement;
const track = (sig: typeof value, onInput: (n: number) => void, label: string): HTMLInputElement => {
const input = h('input', { type: 'range', class: 'ua-slider-input', min, max, step, disabled, 'aria-label': label }) as HTMLInputElement; // 原生 range 不自带名字,读屏只念「滑块」
input.addEventListener('input', () => onInput(Number(input.value)));
effect(() => {
const next = String(sig());
@@ -30,7 +30,7 @@ export function slider(props: SliderProps): HTMLElement {
: null;
if (!props.range) { // —— 单值:一条原生轨,行为与旧版完全一致 ——
const input = track(value, (next) => { value(next); props.onChange?.(next); });
const input = track(value, (next) => { value(next); props.onChange?.(next); }, props.ariaLabel ?? '数值');
return h(
'div',
{ class: 'ua-slider' },
@@ -42,8 +42,8 @@ export function slider(props: SliderProps): HTMLElement {
// —— 区间:两条原生轨叠放,rail 打底、seg 画出两拇指之间的已选段 ——
const rail = h('div', { class: 'ua-slider-rail', 'aria-hidden': 'true' });
const seg = h('div', { class: 'ua-slider-seg', 'aria-hidden': 'true' }) as HTMLElement;
const lowIn = track(value, (next) => { value(Math.min(next, high())); props.onRangeChange?.(value(), high()); }); // 下限越过上限就压平
const highIn = track(high, (next) => { high(Math.max(next, value())); props.onRangeChange?.(value(), high()); }); // 上限低于下限就压平
const lowIn = track(value, (next) => { value(Math.min(next, high())); props.onRangeChange?.(value(), high()); }, `${props.ariaLabel ?? '数值'}下限`); // 下限越过上限就压平;区间两端各有名字
const highIn = track(high, (next) => { high(Math.max(next, value())); props.onRangeChange?.(value(), high()); }, `${props.ariaLabel ?? '数值'}上限`); // 上限低于下限就压平
lowIn.classList.add('ua-slider-input-low'); // 供 CSS 区分上下层与拇指命中区
highIn.classList.add('ua-slider-input-high');
effect(() => { seg.style.left = `${pct(value())}%`; seg.style.width = `${pct(high()) - pct(value())}%`; }); // 已选段随两端重算
+1 -1
View File
@@ -4,7 +4,7 @@ import type { TextFieldProps } from './types.js';
/*** 文本输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调;prefix/suffix 包附加组,clearable 一键清空,password 密码显隐切换,maxCount 显示字数统计 ***/
export function textField(props: TextFieldProps): HTMLElement {
const el = h('input', { class: `ua-input${props.size && props.size !== 'md' ? ` is-${props.size}` : ''}`, placeholder: props.placeholder ?? '', disabled: props.disabled }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式
const el = h('input', { class: `ua-input${props.size && props.size !== 'md' ? ` is-${props.size}` : ''}`, placeholder: props.placeholder ?? '', disabled: props.disabled, 'aria-label': props.ariaLabel ?? '文本输入' }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式
if (props.value) {
const signal = props.value;
el.addEventListener('input', () => signal(el.value)); // 用户输入写回信号
+1 -1
View File
@@ -4,7 +4,7 @@ import type { TextareaProps } from './types.js';
/*** 多行输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调;prefix/suffix 包纵向附加组,maxCount 显示字数统计 ***/
export function textarea(props: TextareaProps): HTMLElement {
const el = h('textarea', { class: 'ua-textarea', rows: props.rows ?? 3, placeholder: props.placeholder ?? '', disabled: props.disabled }) as HTMLTextAreaElement; // false/undefined 移除属性,Signal 走响应式
const el = h('textarea', { class: 'ua-textarea', rows: props.rows ?? 3, placeholder: props.placeholder ?? '', disabled: props.disabled, 'aria-label': props.ariaLabel ?? '多行文本' }) as HTMLTextAreaElement; // false/undefined 移除属性,Signal 走响应式
if (props.value) {
const signal = props.value;
el.addEventListener('input', () => signal(el.value)); // 用户输入写回信号
+4
View File
@@ -32,6 +32,7 @@ export interface ButtonProps {
/*** 文本输入入参 ***/
export interface TextFieldProps {
ariaLabel?: string; // 读屏用的字段名(如「收件人」);缺省「文本输入」
size?: 'sm' | 'md' | 'lg'; // 高度档位:走 --ua-control-h-* 三档;缺省 md
placeholder?: string; // 占位文案
value?: Signal<string>; // 双向绑定的信号
@@ -189,6 +190,7 @@ export interface AccordionProps {
/*** 滑块入参 ***/
export interface SliderProps {
ariaLabel?: string; // 读屏用的字段名(如「音量」);缺省「数值」,区间模式自动带上限/下限
value: Signal<number>; // 当前值信号,双向绑定(拖动写回、外部赋值同步)
min?: number; // 最小值;缺省 0
max?: number; // 最大值;缺省 100
@@ -204,6 +206,7 @@ export interface SliderProps {
/*** 多行输入入参 ***/
export interface TextareaProps {
ariaLabel?: string; // 读屏用的字段名(如「备注」);缺省「多行文本」
placeholder?: string; // 占位文案
rows?: number; // 行数;缺省 3
value?: Signal<string>; // 双向绑定的信号
@@ -217,6 +220,7 @@ export interface TextareaProps {
/*** 数字输入入参 ***/
export interface InputNumberProps {
ariaLabel?: string; // 读屏用的字段名(如「数量」);缺省「数字输入」
size?: 'sm' | 'md' | 'lg'; // 高度档位:走 --ua-control-h-* 三档;缺省 md
value: Signal<number>; // 当前值信号,双向绑定(按钮/键盘写回、外部赋值同步)
min?: number; // 最小值;缺省无下限
+24
View File
@@ -315,6 +315,12 @@
"t": "boolean",
"d": "密码框:原生打点渲染,内置「显示/隐藏」切换按钮(aria-label 同步);缺省普通文本"
}
,
{
"n": "ariaLabel",
"t": "string",
"d": "无可见 label 时的可访问名(如「收件人」);缺省「文本输入」"
}
],
"example": "const name = signal(''); const el = textField({ value: name, placeholder: '输入名字…', onInput: (v) => console.log(v) })",
"steps": [
@@ -383,6 +389,12 @@
"t": "'sm' | 'md' | 'lg'",
"d": "高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md"
}
,
{
"n": "ariaLabel",
"t": "string",
"d": "无可见 label 时的可访问名(如「数量」);缺省「数字输入」"
}
],
"example": "const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => console.log(v) })",
"steps": [
@@ -445,6 +457,12 @@
"t": "boolean | { minRows?, maxRows? }",
"d": "自适应内容高度:true 跟随内容涨落,对象态钳最小/最大行数,超出 maxRows 内部滚动;缺省固定行数"
}
,
{
"n": "ariaLabel",
"t": "string",
"d": "无可见 label 时的可访问名(如「备注」);缺省「多行文本」"
}
],
"example": "const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话介绍…' })",
"steps": [
@@ -522,6 +540,12 @@
"k": 1,
"d": "拖动回调"
}
,
{
"n": "ariaLabel",
"t": "string",
"d": "无可见 label 时的可访问名(如「音量」);缺省「数值」,区间模式自动带「下限 / 上限」"
}
],
"example": "const vol = signal(40); slider({ value: vol, min: 0, max: 100, step: 5, onChange: (v) => console.log(v) })",
"steps": [