iter(g216): 时间选择支持清空
This commit is contained in:
+1
-1
@@ -1066,7 +1066,7 @@ const content = h(
|
|||||||
h(
|
h(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'row' },
|
{ class: 'row' },
|
||||||
timePicker({ value: atTime, minuteStep: 15, onChange: (t) => toast.success(`定在 ${t}`) }),
|
timePicker({ value: atTime, minuteStep: 15, clearable: true, onChange: (t) => toast.success(t ? `定在 ${t}` : '已清空时间') }),
|
||||||
timePicker({ placeholder: '禁用态', disabled: true }),
|
timePicker({ placeholder: '禁用态', disabled: true }),
|
||||||
h('span', { class: 'out' }, () => `当前时间:${atTime() || '未选'}`),
|
h('span', { class: 'out' }, () => `当前时间:${atTime() || '未选'}`),
|
||||||
),
|
),
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-26T11:54:31.013Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-26T11:56:46.981Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -1994,6 +1994,7 @@ const cmdk = commandPalette({ open: signal(false), items: [{ id: 'save', label:
|
|||||||
|
|
||||||
- value:Signal<string> —— 选中时间信号(HH:mm);不传则内部维护
|
- value:Signal<string> —— 选中时间信号(HH:mm);不传则内部维护
|
||||||
- minuteStep:number —— 分钟步长;缺省 5,超过 30 按 30 处理(如 15 只有 00/15/30/45)
|
- minuteStep:number —— 分钟步长;缺省 5,超过 30 按 30 处理(如 15 只有 00/15/30/45)
|
||||||
|
- clearable:boolean —— 显示清空按钮 ×(仅在有值时出现);缺省 false,键盘 Delete / Backspace 等价
|
||||||
- placeholder:string —— 未选时占位文案;缺省「请选择时间」
|
- placeholder:string —— 未选时占位文案;缺省「请选择时间」
|
||||||
- disabled:boolean | Signal<boolean> —— 禁用触发器
|
- disabled:boolean | Signal<boolean> —— 禁用触发器
|
||||||
- onChange:(time: string) => void —— 写回回调:选时即触发一次(分默认 00),选完分再触发一次
|
- onChange:(time: string) => void —— 写回回调:选时即触发一次(分默认 00),选完分再触发一次
|
||||||
@@ -2011,6 +2012,7 @@ const at = signal('09:30'); timePicker({ value: at, minuteStep: 15, onChange: (t
|
|||||||
3. 先点时列定小时(分默认落到首个步长值,值立刻可用),再点分列即完成并收起,焦点回到触发器
|
3. 先点时列定小时(分默认落到首个步长值,值立刻可用),再点分列即完成并收起,焦点回到触发器
|
||||||
4. 列内可用 ↑↓ 环形移动、Home/End 直达首末;Tab、Esc、点击外部都能正常进出与收起
|
4. 列内可用 ↑↓ 环形移动、Home/End 直达首末;Tab、Esc、点击外部都能正常进出与收起
|
||||||
5. 需要 15 分钟一档就传 minuteStep: 15,步长只影响分列的可选值,时列始终 24 档
|
5. 需要 15 分钟一档就传 minuteStep: 15,步长只影响分列的可选值,时列始终 24 档
|
||||||
|
6. 需要允许回到「未选」态就传 clearable: true:有值时触发器右侧出现 ×(不进 Tab 序),聚焦触发器按 Delete / Backspace 也能清空,回调收到空串
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-timepicker
|
现场演示:apps/docs/demo.html#sec-timepicker
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ export function timePicker(props: TimePickerProps): HTMLElement {
|
|||||||
props.onChange?.(next);
|
props.onChange?.(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*** 清空:回到「未选」态,收起弹层并写回空串(与 select / multiSelect 的清空语义一致) ***/
|
||||||
|
const clear = () => {
|
||||||
|
if (!value()) return; // 卫语句:本来就没选,不重复派发回调
|
||||||
|
value('');
|
||||||
|
props.onChange?.('');
|
||||||
|
open(false);
|
||||||
|
};
|
||||||
|
|
||||||
/*** 列内聚焦导航:↑↓ 环形移动、Home/End 直达首末(列内按钮顺序即时间顺序) ***/
|
/*** 列内聚焦导航:↑↓ 环形移动、Home/End 直达首末(列内按钮顺序即时间顺序) ***/
|
||||||
const navigate = (col: HTMLElement, cells: HTMLButtonElement[]) => {
|
const navigate = (col: HTMLElement, cells: HTMLButtonElement[]) => {
|
||||||
col.addEventListener('keydown', (e: KeyboardEvent) => {
|
col.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
@@ -82,13 +90,23 @@ export function timePicker(props: TimePickerProps): HTMLElement {
|
|||||||
h('span', { class: 'ua-timepicker-arrow', 'aria-hidden': 'true' }, '▾'),
|
h('span', { class: 'ua-timepicker-arrow', 'aria-hidden': 'true' }, '▾'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// —— 清空按钮:clearable 且有值时出现,压在触发器右缘(与箭头错开)——
|
||||||
|
const clearable = props.clearable ?? false; // 缺省不带清空:没选的场景多,按钮会白占位置
|
||||||
|
const clearBtn = h('button', {
|
||||||
|
type: 'button',
|
||||||
|
class: 'ua-timepicker-clear',
|
||||||
|
'aria-label': '清空时间',
|
||||||
|
tabindex: '-1', // 不进 Tab 序:清空是鼠标 / 触屏便利,键盘用 Delete / Backspace
|
||||||
|
onClick: () => clear(),
|
||||||
|
}, '×') as HTMLElement;
|
||||||
|
|
||||||
const pop = h(
|
const pop = h(
|
||||||
'div',
|
'div',
|
||||||
{ class: () => `ua-timepicker-pop${open() ? ' ua-open' : ''}${place() === 'top' ? ' is-flip' : ''}`, role: 'dialog', 'aria-label': '选择时间' },
|
{ class: () => `ua-timepicker-pop${open() ? ' ua-open' : ''}${place() === 'top' ? ' is-flip' : ''}`, role: 'dialog', 'aria-label': '选择时间' },
|
||||||
hourCol.col,
|
hourCol.col,
|
||||||
minuteCol.col,
|
minuteCol.col,
|
||||||
);
|
);
|
||||||
const root = h('div', { class: () => `ua-timepicker${open() ? ' is-open' : ''}` }, trigger, pop);
|
const root = h('div', { class: () => `ua-timepicker${open() ? ' is-open' : ''}${props.clearable && value() ? ' has-clear' : ''}` }, trigger, clearable ? clearBtn : null, pop);
|
||||||
|
|
||||||
/*** 把激活格滚到列中部:列是独立滚动容器,直接算 scrollTop 不惊动页面 ***/
|
/*** 把激活格滚到列中部:列是独立滚动容器,直接算 scrollTop 不惊动页面 ***/
|
||||||
const reveal = (col: HTMLElement, cells: HTMLButtonElement[]) => {
|
const reveal = (col: HTMLElement, cells: HTMLButtonElement[]) => {
|
||||||
@@ -98,6 +116,13 @@ export function timePicker(props: TimePickerProps): HTMLElement {
|
|||||||
col.scrollTop = Math.max(0, target);
|
col.scrollTop = Math.max(0, target);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
effect(() => { clearBtn.hidden = !clearable || !value(); }); // 无值或未开启时藏起来(hidden 一并移出点击区)
|
||||||
|
root.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (open() || !clearable) return; // 卫语句:展开时按键归列内导航,未开启清空则不接管
|
||||||
|
if (e.key !== 'Backspace' && e.key !== 'Delete') return; // 卫语句:只接管这两个键
|
||||||
|
e.preventDefault();
|
||||||
|
clear(); // 键盘等价:不必伸手去点 ×
|
||||||
|
});
|
||||||
bindOutside(root, open, () => open(false)); // 点外部收起:仅展开期间挂监听
|
bindOutside(root, open, () => open(false)); // 点外部收起:仅展开期间挂监听
|
||||||
effect(() => {
|
effect(() => {
|
||||||
if (open()) {
|
if (open()) {
|
||||||
|
|||||||
@@ -670,6 +670,7 @@ export interface TimePickerProps {
|
|||||||
value?: Signal<string>; // 选中时间(HH:mm),未传则组件内部持有
|
value?: Signal<string>; // 选中时间(HH:mm),未传则组件内部持有
|
||||||
minuteStep?: number; // 分钟步长;缺省 5,超过 30 按 30 处理
|
minuteStep?: number; // 分钟步长;缺省 5,超过 30 按 30 处理
|
||||||
placeholder?: string; // 未选时占位文案;缺省「请选择时间」
|
placeholder?: string; // 未选时占位文案;缺省「请选择时间」
|
||||||
|
clearable?: boolean; // 显示清空按钮 ×(有值时才出现);缺省 false,键盘 Delete / Backspace 等价
|
||||||
disabled?: boolean | Signal<boolean>; // 禁用触发器
|
disabled?: boolean | Signal<boolean>; // 禁用触发器
|
||||||
onChange?: (time: string) => void; // 写回回调(选时也会触发,分默认落到首个步长值)
|
onChange?: (time: string) => void; // 写回回调(选时也会触发,分默认落到首个步长值)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3669,6 +3669,11 @@
|
|||||||
"t": "number",
|
"t": "number",
|
||||||
"d": "分钟步长;缺省 5,超过 30 按 30 处理(如 15 只有 00/15/30/45)"
|
"d": "分钟步长;缺省 5,超过 30 按 30 处理(如 15 只有 00/15/30/45)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"n": "clearable",
|
||||||
|
"t": "boolean",
|
||||||
|
"d": "显示清空按钮 ×(仅在有值时出现);缺省 false,键盘 Delete / Backspace 等价"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"n": "placeholder",
|
"n": "placeholder",
|
||||||
"t": "string",
|
"t": "string",
|
||||||
@@ -3691,7 +3696,8 @@
|
|||||||
"点触发器展开时 / 分两列,当前值所在格高亮并自动滚到列中部,不必手动找",
|
"点触发器展开时 / 分两列,当前值所在格高亮并自动滚到列中部,不必手动找",
|
||||||
"先点时列定小时(分默认落到首个步长值,值立刻可用),再点分列即完成并收起,焦点回到触发器",
|
"先点时列定小时(分默认落到首个步长值,值立刻可用),再点分列即完成并收起,焦点回到触发器",
|
||||||
"列内可用 ↑↓ 环形移动、Home/End 直达首末;Tab、Esc、点击外部都能正常进出与收起",
|
"列内可用 ↑↓ 环形移动、Home/End 直达首末;Tab、Esc、点击外部都能正常进出与收起",
|
||||||
"需要 15 分钟一档就传 minuteStep: 15,步长只影响分列的可选值,时列始终 24 档"
|
"需要 15 分钟一档就传 minuteStep: 15,步长只影响分列的可选值,时列始终 24 档",
|
||||||
|
"需要允许回到「未选」态就传 clearable: true:有值时触发器右侧出现 ×(不进 Tab 序),聚焦触发器按 Delete / Backspace 也能清空,回调收到空串"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5144,3 +5144,32 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); }
|
|||||||
.ua-multiselect-pop.ua-open.is-flip {
|
.ua-multiselect-pop.ua-open.is-flip {
|
||||||
animation-name: ua-pop-rise; /* 翻到上方:入场方向随位置镜像 */
|
animation-name: ua-pop-rise; /* 翻到上方:入场方向随位置镜像 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- 时间选择:清空按钮(clearable 且有值时才出现) ---- */
|
||||||
|
.ua-timepicker-clear {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: var(--ua-space-6); /* 让到箭头左侧,不与 ▾ 重叠 */
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: none;
|
||||||
|
color: var(--ua-color-text-muted);
|
||||||
|
font-size: var(--ua-font-size-md);
|
||||||
|
line-height: var(--ua-leading-none); /* 行高压到 1:× 在 16px 圆里垂直居中 */
|
||||||
|
cursor: pointer;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
transition: color var(--ua-duration-fast) var(--ua-ease), background-color var(--ua-duration-fast) var(--ua-ease);
|
||||||
|
}
|
||||||
|
.ua-timepicker-clear:hover {
|
||||||
|
background: var(--ua-color-surface-2); /* 悬停给一层淡底:小 × 只变颜色太难点中 */
|
||||||
|
color: var(--ua-color-text);
|
||||||
|
}
|
||||||
|
.ua-timepicker.has-clear .ua-timepicker-trigger {
|
||||||
|
padding-right: var(--ua-space-6); /* 给 × 让出位置:文案不会被压在按钮下面 */
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user