fix(ui): 日期占位改用不透明遮罩盖住原生文字,不再依赖伪元素着色

把 ::-webkit-datetime-edit 系列伪元素设为透明的做法在部分环境下不生效,
空值日期框会同时显示自绘的「选择日期」与原生「yyyy/mm/日」,文字重叠。

改为用一层不透明背景直接盖住原生提示:靠绘制顺序生效,不依赖伪元素支持
与 CSS 选择器匹配;显示时机由 Vue 状态(空值 && 未聚焦)控制。同时删掉
style.css 里已失效的 .date-field--blank 规则。
This commit is contained in:
chenshoulin
2026-10-18 18:54:47 +08:00
parent 54c08d6ade
commit c45d2b72b4
3 changed files with 68 additions and 40 deletions
+36 -10
View File
@@ -59,25 +59,51 @@ describe('DatePicker', () => {
})
})
/**
* 占位层是「盖住」而不是「隐藏」原生文字:靠绘制顺序生效,不依赖
* ::-webkit-datetime-edit 之类的伪元素着色。因此这里重点断言
* 显示时机与不透明背景 —— 少任何一项都会重新出现文字重叠。
*/
describe('DatePicker 空值占位', () => {
it('空值时显示自定义占位文案', () => {
expect(mountPicker(null).text()).toContain('选择日期')
const placeholderOf = (wrapper: ReturnType<typeof mountPicker>) =>
wrapper.find('[data-role="date-placeholder"]')
it('空值且未聚焦时显示自绘占位文案', () => {
expect(placeholderOf(mountPicker(null)).text()).toBe('选择日期')
})
it('有值时不再显示占位文案', () => {
expect(mountPicker('2026-10-20').text()).not.toContain('选择日期')
it('有值时不再显示占位', () => {
expect(placeholderOf(mountPicker('2026-10-20')).exists()).toBe(false)
})
it('空值时给输入框加上遮罩类(遮住 yyyy/mm/日)', () => {
expect(mountPicker(null).find('input').classes()).toContain('date-field--blank')
it('聚焦后占位让位,原生分段可见', async () => {
const wrapper = mountPicker(null)
await wrapper.find('input').trigger('focus')
expect(placeholderOf(wrapper).exists()).toBe(false)
})
it('有值时不加遮罩类', () => {
expect(mountPicker('2026-10-20').find('input').classes()).not.toContain('date-field--blank')
it('失焦后占位恢复', async () => {
const wrapper = mountPicker(null)
await wrapper.find('input').trigger('focus')
await wrapper.find('input').trigger('blur')
expect(placeholderOf(wrapper).exists()).toBe(true)
})
it('占位层不拦截鼠标事件(点击仍能唤起原生选择器)', () => {
const placeholder = mountPicker(null).find('span')
expect(placeholder.classes()).toContain('pointer-events-none')
expect(placeholderOf(mountPicker(null)).classes()).toContain('pointer-events-none')
})
it('占位层带不透明背景,且与输入框配色一致', () => {
const classes = placeholderOf(mountPicker(null)).classes()
expect(classes).toContain('bg-white')
expect(classes).toContain('dark:bg-slate-800')
expect(mountPicker(null).find('input').classes()).toContain('dark:bg-slate-800')
})
it('占位层只覆盖左侧,给日历图标留出位置', () => {
const classes = placeholderOf(mountPicker(null)).classes()
expect(classes).toContain('right-9')
expect(classes).toContain('inset-y-px')
expect(classes).toContain('left-px')
})
})
+29 -12
View File
@@ -7,24 +7,35 @@
* - 自带可访问性、键盘操作与本地化日历,无需自己实现
* - 深色模式下日历弹层跟随 html 的 color-scheme
*
* 空值时刻意遮住原生占位文字:Chrome 会按页面 lang 拼出「yyyy/mm/日」
* 这种半英半中的提示,既别扭又容易被误读成已有内容。改为显示自己的
* 「选择日期」,聚焦时再让位给原生分段输入(否则编辑时看不见数字)。
* 空值时的占位处理:
* 浏览器(Edge 尤甚)会给空日期框显示「yyyy / mm / 日」这种半英半中的
* 格式提示,既与界面语言不统一,又容易被误读成已有内容。
*
* 曾经用 CSS 把 `::-webkit-datetime-edit` 等伪元素设成透明,但在部分环境下
* 不生效(实测在用户机器上原生文字照样显示)。现在改为**用一层不透明背景
* 直接盖住**:
* - 不依赖伪元素支持,也不依赖 CSS 选择器匹配,纯靠绘制顺序(后者在上)
* - 显示时机由 Vue 状态决定(空值 && 未聚焦),确定可控
* - 聚焦时让位,原生分段正常显示,不影响编辑
*
* 代价:占位层的背景色必须与输入框完全一致,否则会看出色块。
*/
import { computed, ref } from 'vue'
const props = defineProps<{ modelValue: string | null; label?: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string | null] }>()
const focused = ref(false)
/** 空值且未聚焦时才盖住原生提示 */
const showPlaceholder = computed(() => props.modelValue === null && !focused.value)
function onChange(event: Event): void {
const value = (event.target as HTMLInputElement).value
emit('update:modelValue', value === '' ? null : value)
}
/** 空值且未聚焦时才遮住原生占位文字 */
function hideNativePlaceholder(): boolean {
return props.modelValue === null
}
</script>
<template>
@@ -32,16 +43,22 @@ function hideNativePlaceholder(): boolean {
<div class="relative min-w-0 flex-1">
<input
type="date"
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="{ 'date-field--blank': hideNativePlaceholder() }"
class="w-full rounded-md border border-slate-300 bg-white 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"
:value="modelValue ?? ''"
:aria-label="label ?? '截止日期'"
@focus="focused = true"
@blur="focused = false"
@change="onChange"
/>
<!--
背景色必须与输入框一致(浅色 bg-white / 深色 bg-slate-800),否则盖不干净。
左侧内缩 1px 让边框可见;右侧留出日历图标的位置,避免把它也盖住。
-->
<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"
v-if="showPlaceholder"
data-role="date-placeholder"
class="pointer-events-none absolute inset-y-px left-px right-9 flex items-center rounded-l-md bg-white pl-2 text-sm text-slate-400 dark:bg-slate-800 dark:text-slate-500"
>
选择日期
</span>