fix(ui): 日期占位改用不透明遮罩盖住原生文字,不再依赖伪元素着色
把 ::-webkit-datetime-edit 系列伪元素设为透明的做法在部分环境下不生效, 空值日期框会同时显示自绘的「选择日期」与原生「yyyy/mm/日」,文字重叠。 改为用一层不透明背景直接盖住原生提示:靠绘制顺序生效,不依赖伪元素支持 与 CSS 选择器匹配;显示时机由 Vue 状态(空值 && 未聚焦)控制。同时删掉 style.css 里已失效的 .date-field--blank 规则。
This commit is contained in:
@@ -59,25 +59,51 @@ describe('DatePicker', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 占位层是「盖住」而不是「隐藏」原生文字:靠绘制顺序生效,不依赖
|
||||||
|
* ::-webkit-datetime-edit 之类的伪元素着色。因此这里重点断言
|
||||||
|
* 显示时机与不透明背景 —— 少任何一项都会重新出现文字重叠。
|
||||||
|
*/
|
||||||
describe('DatePicker 空值占位', () => {
|
describe('DatePicker 空值占位', () => {
|
||||||
it('空值时显示自定义占位文案', () => {
|
const placeholderOf = (wrapper: ReturnType<typeof mountPicker>) =>
|
||||||
expect(mountPicker(null).text()).toContain('选择日期')
|
wrapper.find('[data-role="date-placeholder"]')
|
||||||
|
|
||||||
|
it('空值且未聚焦时显示自绘占位文案', () => {
|
||||||
|
expect(placeholderOf(mountPicker(null)).text()).toBe('选择日期')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('有值时不再显示占位文案', () => {
|
it('有值时不再显示占位', () => {
|
||||||
expect(mountPicker('2026-10-20').text()).not.toContain('选择日期')
|
expect(placeholderOf(mountPicker('2026-10-20')).exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('空值时给输入框加上遮罩类(遮住 yyyy/mm/日)', () => {
|
it('聚焦后占位让位,原生分段可见', async () => {
|
||||||
expect(mountPicker(null).find('input').classes()).toContain('date-field--blank')
|
const wrapper = mountPicker(null)
|
||||||
|
await wrapper.find('input').trigger('focus')
|
||||||
|
expect(placeholderOf(wrapper).exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('有值时不加遮罩类', () => {
|
it('失焦后占位恢复', async () => {
|
||||||
expect(mountPicker('2026-10-20').find('input').classes()).not.toContain('date-field--blank')
|
const wrapper = mountPicker(null)
|
||||||
|
await wrapper.find('input').trigger('focus')
|
||||||
|
await wrapper.find('input').trigger('blur')
|
||||||
|
expect(placeholderOf(wrapper).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('占位层不拦截鼠标事件(点击仍能唤起原生选择器)', () => {
|
it('占位层不拦截鼠标事件(点击仍能唤起原生选择器)', () => {
|
||||||
const placeholder = mountPicker(null).find('span')
|
expect(placeholderOf(mountPicker(null)).classes()).toContain('pointer-events-none')
|
||||||
expect(placeholder.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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,24 +7,35 @@
|
|||||||
* - 自带可访问性、键盘操作与本地化日历,无需自己实现
|
* - 自带可访问性、键盘操作与本地化日历,无需自己实现
|
||||||
* - 深色模式下日历弹层跟随 html 的 color-scheme
|
* - 深色模式下日历弹层跟随 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 props = defineProps<{ modelValue: string | null; label?: string }>()
|
||||||
|
|
||||||
const emit = defineEmits<{ 'update:modelValue': [value: string | null] }>()
|
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 {
|
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>
|
||||||
@@ -32,16 +43,22 @@ function hideNativePlaceholder(): boolean {
|
|||||||
<div class="relative min-w-0 flex-1">
|
<div class="relative min-w-0 flex-1">
|
||||||
<input
|
<input
|
||||||
type="date"
|
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="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"
|
||||||
:class="{ 'date-field--blank': hideNativePlaceholder() }"
|
|
||||||
:value="modelValue ?? ''"
|
:value="modelValue ?? ''"
|
||||||
:aria-label="label ?? '截止日期'"
|
:aria-label="label ?? '截止日期'"
|
||||||
|
@focus="focused = true"
|
||||||
|
@blur="focused = false"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
背景色必须与输入框一致(浅色 bg-white / 深色 bg-slate-800),否则盖不干净。
|
||||||
|
左侧内缩 1px 让边框可见;右侧留出日历图标的位置,避免把它也盖住。
|
||||||
|
-->
|
||||||
<span
|
<span
|
||||||
v-if="hideNativePlaceholder()"
|
v-if="showPlaceholder"
|
||||||
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"
|
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>
|
</span>
|
||||||
|
|||||||
+3
-18
@@ -73,22 +73,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 空值的日期输入框:遮住浏览器自带的格式提示。
|
* 注:日期输入框空值时的原生提示(「yyyy / mm / 日」)不在这里处理。
|
||||||
* Edge 会按页面 lang 拼出「yyyy / mm / 日」这种半英半中的提示,
|
* 试过把 `::-webkit-datetime-edit` 系列伪元素设成透明,但部分环境下不生效,
|
||||||
* 既与界面语言不统一,又容易被误读成已经有内容。
|
* 已改为在 DatePicker.vue 里用不透明占位层直接盖住。
|
||||||
*
|
|
||||||
* 只在未聚焦时遮挡 —— 否则聚焦编辑时连正在输入的数字都看不见。
|
|
||||||
* 同时覆盖输入框本身与各分段伪元素,避免不同 Chromium 版本着色位置不一致。
|
|
||||||
*
|
|
||||||
* 刻意写在 @layer 之外:Tailwind 的 utilities 层优先级高于 components 层,
|
|
||||||
* 若放进层里会被 text-slate-700 之类的工具类盖掉。也不写成 Tailwind 任意变体,
|
|
||||||
* 因为那依赖扫描器从模板字符串里提取类名,HMR 或增量构建时容易漏。
|
|
||||||
*/
|
*/
|
||||||
.date-field--blank:not(:focus),
|
|
||||||
.date-field--blank:not(:focus)::-webkit-datetime-edit,
|
|
||||||
.date-field--blank:not(:focus)::-webkit-datetime-edit-text,
|
|
||||||
.date-field--blank:not(:focus)::-webkit-datetime-edit-year-field,
|
|
||||||
.date-field--blank:not(:focus)::-webkit-datetime-edit-month-field,
|
|
||||||
.date-field--blank:not(:focus)::-webkit-datetime-edit-day-field {
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user