fix(ui): 日期选择器空值时改用自定义占位,遮住浏览器自带的 yyyy/mm/日
问题:截止日期为空时,Chrome 会按页面 lang="zh-CN" 拼出「yyyy/mm/日」 这个半英半中的格式提示。既与界面语言不统一,又容易被误读成「已经有内容」。 处理:空值且未聚焦时把原生分段文字设为透明,改由组件自己渲染「选择日期」 占位(与标签输入框的「回车添加标签」风格一致)。 两个必要的边界: - 只在 :not(:focus) 时透明 —— 否则聚焦编辑时连正在输入的数字都看不见。 实测聚焦后原生分段会正常显示为「年/月/日」,比空值时的混合格式更干净。 - 占位层用 pointer-events-none,点击仍能唤起原生日期选择器。 只作用于 :::-webkit-datetime-edit(Chromium 系)。Firefox 的分段控件 不支持该伪元素,会退回浏览器自身的提示——这比强行自绘一个日期控件更好, 原生控件的可访问性与格式保证是这个组件当初的选型理由。 验证:单元测试 485 个(新增 5 个针对占位与遮挡)、E2E 25 个通过; 空值与聚焦两种状态均已截图确认。
This commit is contained in:
@@ -58,3 +58,30 @@ describe('DatePicker', () => {
|
|||||||
expect(mountPicker(null, '开始日期').find('input').attributes('aria-label')).toBe('开始日期')
|
expect(mountPicker(null, '开始日期').find('input').attributes('aria-label')).toBe('开始日期')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('DatePicker 空值占位', () => {
|
||||||
|
it('空值时显示自定义占位文案', () => {
|
||||||
|
expect(mountPicker(null).text()).toContain('选择日期')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('有值时不再显示占位文案', () => {
|
||||||
|
expect(mountPicker('2026-10-20').text()).not.toContain('选择日期')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('空值时遮住浏览器自带的格式提示(yyyy/mm/日)', () => {
|
||||||
|
const classes = mountPicker(null).find('input').classes().join(' ')
|
||||||
|
expect(classes).toContain('::-webkit-datetime-edit')
|
||||||
|
// 只在未聚焦时透明,否则聚焦后看不见正在编辑的数字
|
||||||
|
expect(classes).toContain(':not(:focus)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('有值时不做任何遮挡', () => {
|
||||||
|
const classes = mountPicker('2026-10-20').find('input').classes().join(' ')
|
||||||
|
expect(classes).not.toContain('::-webkit-datetime-edit')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('占位层不拦截鼠标事件(点击仍能唤起原生选择器)', () => {
|
||||||
|
const placeholder = mountPicker(null).find('span')
|
||||||
|
expect(placeholder.classes()).toContain('pointer-events-none')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -6,9 +6,13 @@
|
|||||||
* - 其 value 规范就是 YYYY-MM-DD,与 ADR-003 的存储约定天然一致
|
* - 其 value 规范就是 YYYY-MM-DD,与 ADR-003 的存储约定天然一致
|
||||||
* - 自带可访问性、键盘操作与本地化日历,无需自己实现
|
* - 自带可访问性、键盘操作与本地化日历,无需自己实现
|
||||||
* - 深色模式下日历弹层跟随 html 的 color-scheme
|
* - 深色模式下日历弹层跟随 html 的 color-scheme
|
||||||
|
*
|
||||||
|
* 空值时刻意遮住原生占位文字:Chrome 会按页面 lang 拼出「yyyy/mm/日」
|
||||||
|
* 这种半英半中的提示,既别扭又容易被误读成已有内容。改为显示自己的
|
||||||
|
* 「选择日期」,聚焦时再让位给原生分段输入(否则编辑时看不见数字)。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defineProps<{ modelValue: string | null; label?: string }>()
|
const props = defineProps<{ modelValue: string | null; label?: string }>()
|
||||||
|
|
||||||
const emit = defineEmits<{ 'update:modelValue': [value: string | null] }>()
|
const emit = defineEmits<{ 'update:modelValue': [value: string | null] }>()
|
||||||
|
|
||||||
@@ -16,17 +20,37 @@ function onChange(event: Event): void {
|
|||||||
const value = (event.target as HTMLInputElement).value
|
const value = (event.target as HTMLInputElement).value
|
||||||
emit('update:modelValue', value === '' ? null : value)
|
emit('update:modelValue', value === '' ? null : value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 空值且未聚焦时才遮住原生占位文字 */
|
||||||
|
function hideNativePlaceholder(): boolean {
|
||||||
|
return props.modelValue === null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="relative min-w-0 flex-1">
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
class="min-w-0 flex-1 rounded-md border border-slate-300 px-2.5 py-1.5 text-sm text-slate-700 outline-none focus:border-sky-400 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:focus:border-sky-600"
|
class="peer w-full rounded-md border border-slate-300 px-2.5 py-1.5 text-sm text-slate-700 outline-none focus:border-sky-400 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:focus:border-sky-600"
|
||||||
|
:class="
|
||||||
|
hideNativePlaceholder()
|
||||||
|
? '[&:not(:focus)::-webkit-datetime-edit]:text-transparent'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
:value="modelValue ?? ''"
|
:value="modelValue ?? ''"
|
||||||
:aria-label="label ?? '截止日期'"
|
:aria-label="label ?? '截止日期'"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="hideNativePlaceholder()"
|
||||||
|
class="pointer-events-none absolute inset-y-0 left-2.5 flex items-center text-sm text-slate-400 peer-focus:hidden dark:text-slate-500"
|
||||||
|
>
|
||||||
|
选择日期
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="modelValue"
|
v-if="modelValue"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user