iter(g223): 非档值按最近档对齐
This commit is contained in:
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-26T12:02:54.662Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-26T12:04:13.764Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -2015,6 +2015,7 @@ const at = signal('09:30'); timePicker({ value: at, minuteStep: 15, onChange: (t
|
|||||||
5. 需要 15 分钟一档就传 minuteStep: 15,步长只影响分列的可选值,时列始终 24 档
|
5. 需要 15 分钟一档就传 minuteStep: 15,步长只影响分列的可选值,时列始终 24 档
|
||||||
6. 需要允许回到「未选」态就传 clearable: true:有值时触发器右侧出现 ×(不进 Tab 序),聚焦触发器按 Delete / Backspace 也能清空,回调收到空串
|
6. 需要允许回到「未选」态就传 clearable: true:有值时触发器右侧出现 ×(不进 Tab 序),聚焦触发器按 Delete / Backspace 也能清空,回调收到空串
|
||||||
7. 要和输入框并排对齐就传 size(sm / md / lg):高度走 --ua-control-h-* 三档,与 textField、select 的档位完全同源,不会出现「旁边的输入框矮 6px」
|
7. 要和输入框并排对齐就传 size(sm / md / lg):高度走 --ua-control-h-* 三档,与 textField、select 的档位完全同源,不会出现「旁边的输入框矮 6px」
|
||||||
|
8. 传入值不在步长档上时(如 09:07 + 15 分钟档)不会「没有高亮」:分列按最近档高亮并滚到位,点它即落到 09:15;原值在触发器上照常显示,直到用户重新选
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-timepicker
|
现场演示:apps/docs/demo.html#sec-timepicker
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ export function timePicker(props: TimePickerProps): HTMLElement {
|
|||||||
const minutes = Array.from({ length: Math.ceil(60 / step) }, (_, i) => pad(i * step)); // 00..55(按步长取)
|
const minutes = Array.from({ length: Math.ceil(60 / step) }, (_, i) => pad(i * step)); // 00..55(按步长取)
|
||||||
const picked = () => { const [hh = '', mm = ''] = value().split(':'); return { hh, mm }; }; // 拆分当前值
|
const picked = () => { const [hh = '', mm = ''] = value().split(':'); return { hh, mm }; }; // 拆分当前值
|
||||||
|
|
||||||
|
/*** 分钟档对齐:值不在步长档上(如 09:07 + 15 分钟档)时取最近档,作为高亮与滚动锚点 ***/
|
||||||
|
const nearestMinute = (mm: string): string => {
|
||||||
|
const n = Number(mm);
|
||||||
|
if (!mm || Number.isNaN(n)) return ''; // 卫语句:没值或脏值不参与对齐
|
||||||
|
return minutes[Math.min(minutes.length - 1, Math.round(n / step))] ?? '';
|
||||||
|
};
|
||||||
|
|
||||||
/*** 写回时间:两段都补齐后调用回调;值未变时不重复派发(选时与选分各写一次) ***/
|
/*** 写回时间:两段都补齐后调用回调;值未变时不重复派发(选时与选分各写一次) ***/
|
||||||
const commit = (hh: string, mm: string): void => {
|
const commit = (hh: string, mm: string): void => {
|
||||||
const next = `${hh}:${mm}`;
|
const next = `${hh}:${mm}`;
|
||||||
@@ -51,12 +58,13 @@ export function timePicker(props: TimePickerProps): HTMLElement {
|
|||||||
|
|
||||||
/*** 造一列时间格:点击写回,激活格加 is-active 供样式与初始滚动定位 ***/
|
/*** 造一列时间格:点击写回,激活格加 is-active 供样式与初始滚动定位 ***/
|
||||||
const column = (label: string, list: string[], isHour: boolean) => {
|
const column = (label: string, list: string[], isHour: boolean) => {
|
||||||
|
const activeText = () => (isHour ? picked().hh : nearestMinute(picked().mm)); // 本列该高亮的文案:分列按最近档对齐
|
||||||
const cells = list.map((text) =>
|
const cells = list.map((text) =>
|
||||||
h('button', {
|
h('button', {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
class: () => `ua-timepicker-cell${(isHour ? picked().hh : picked().mm) === text ? ' is-active' : ''}`,
|
class: () => `ua-timepicker-cell${activeText() === text ? ' is-active' : ''}`,
|
||||||
role: 'option',
|
role: 'option',
|
||||||
'aria-selected': () => String((isHour ? picked().hh : picked().mm) === text),
|
'aria-selected': () => String(activeText() === text),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const cur = picked();
|
const cur = picked();
|
||||||
if (isHour) return commit(text, cur.mm || minutes[0]); // 先选时:分默认落到首个步长值,值立即可用
|
if (isHour) return commit(text, cur.mm || minutes[0]); // 先选时:分默认落到首个步长值,值立即可用
|
||||||
@@ -129,9 +137,10 @@ export function timePicker(props: TimePickerProps): HTMLElement {
|
|||||||
if (open()) {
|
if (open()) {
|
||||||
releaseEsc ??= pushOverlay(() => { open(false); trigger.focus(); }); // 入 Esc 栈:与弹窗/抽屉共用逐层关闭
|
releaseEsc ??= pushOverlay(() => { open(false); trigger.focus(); }); // 入 Esc 栈:与弹窗/抽屉共用逐层关闭
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const now = new Date(); // 未选值时的锚点:打开就停在「此刻」附近,省得从 00:00 开始翻
|
const now = new Date(); // 无值时的锚点:打开就停在「此刻」附近,省得从 00:00 开始翻
|
||||||
reveal(hourCol.col, hourCol.buttons, now.getHours());
|
const { hh, mm } = picked();
|
||||||
reveal(minuteCol.col, minuteCol.buttons, Math.floor(now.getMinutes() / step));
|
reveal(hourCol.col, hourCol.buttons, hh ? Number(hh) : now.getHours()); // 有值先滚到值上
|
||||||
|
reveal(minuteCol.col, minuteCol.buttons, mm ? minutes.indexOf(nearestMinute(mm)) : Math.floor(now.getMinutes() / step));
|
||||||
}); // 布局就绪后再量
|
}); // 布局就绪后再量
|
||||||
} else {
|
} else {
|
||||||
releaseEsc?.();
|
releaseEsc?.();
|
||||||
|
|||||||
@@ -3703,7 +3703,8 @@
|
|||||||
"列内可用 ↑↓ 环形移动、Home/End 直达首末;Tab、Esc、点击外部都能正常进出与收起",
|
"列内可用 ↑↓ 环形移动、Home/End 直达首末;Tab、Esc、点击外部都能正常进出与收起",
|
||||||
"需要 15 分钟一档就传 minuteStep: 15,步长只影响分列的可选值,时列始终 24 档",
|
"需要 15 分钟一档就传 minuteStep: 15,步长只影响分列的可选值,时列始终 24 档",
|
||||||
"需要允许回到「未选」态就传 clearable: true:有值时触发器右侧出现 ×(不进 Tab 序),聚焦触发器按 Delete / Backspace 也能清空,回调收到空串",
|
"需要允许回到「未选」态就传 clearable: true:有值时触发器右侧出现 ×(不进 Tab 序),聚焦触发器按 Delete / Backspace 也能清空,回调收到空串",
|
||||||
"要和输入框并排对齐就传 size(sm / md / lg):高度走 --ua-control-h-* 三档,与 textField、select 的档位完全同源,不会出现「旁边的输入框矮 6px」"
|
"要和输入框并排对齐就传 size(sm / md / lg):高度走 --ua-control-h-* 三档,与 textField、select 的档位完全同源,不会出现「旁边的输入框矮 6px」",
|
||||||
|
"传入值不在步长档上时(如 09:07 + 15 分钟档)不会「没有高亮」:分列按最近档高亮并滚到位,点它即落到 09:15;原值在触发器上照常显示,直到用户重新选"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user