feat(components): [date-picker] accessibility enhancement (#18109)

* feat(components): [date-picker] accessibility enhancement

closed #14150

* docs: update

* test: add test

* chore: change prevent to passive

* fix: long press the clear icon to open the time panel

* fix: cannot read $el

* fix: cannot read $el

* docs: updata
This commit is contained in:
qiang 2024-10-27 12:14:53 +08:00 committed by GitHub
parent 0e8454a99e
commit 845c07adef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 443 additions and 270 deletions

View File

@ -220,11 +220,12 @@ Note, date time locale (month name, first day of the week ...) are also configur
### Exposes
| Name | Description | Type |
| --------------------- | --------------------------- | ------------------------------------------------------------------------------ |
| focus | focus the Input component | ^[Function]`(focusStartInput?: boolean, isIgnoreFocusEvent?: boolean) => void` |
| handleOpen ^(2.2.16) | open the DatePicker popper | ^[Function]`() => void` |
| handleClose ^(2.2.16) | close the DatePicker popper | ^[Function]`() => void` |
| Name | Description | Type |
| --------------------- | ------------------------------ | ----------------------- |
| focus | focus the DatePicker component | ^[Function]`() => void` |
| blur ^(2.8.7) | blur the DatePicker component | ^[Function]`() => void` |
| handleOpen ^(2.2.16) | open the DatePicker popper | ^[Function]`() => void` |
| handleClose ^(2.2.16) | close the DatePicker popper | ^[Function]`() => void` |
## Type Declarations

View File

@ -124,12 +124,6 @@ datetime-picker/custom-icon
| calendar-change | triggers when the calendar selected date is changed. Only for `datetimerange` | [Date, Date] |
| visible-change | triggers when the DateTimePicker's dropdown appears/disappears | true when it appears, and false otherwise |
## Methods
| Method | Description | Parameters |
| ------ | ------------------------- | ---------- |
| focus | focus the Input component | — |
## Slots
| Name | Description |
@ -140,3 +134,10 @@ datetime-picker/custom-icon
| next-month ^(2.8.0) | next month icon |
| prev-year ^(2.8.0) | prev year icon |
| next-year ^(2.8.0) | next year icon |
## Exposes
| Method | Description | Type |
| ------------- | ------------------------------ | ----------------------- |
| focus | focus the DatePicker component | ^[Function]`() => void` |
| blur ^(2.8.7) | blur the DatePicker component | ^[Function]`() => void` |

View File

@ -85,9 +85,9 @@ time-picker/range
### Exposes
| Name | Description | Type |
| --------------------- | --------------------------- | ------------------------------------------------- |
| focus | focus the Input component | ^[Function]`(e: FocusEvent \| undefined) => void` |
| blur | blur the Input component | ^[Function]`(e: FocusEvent \| undefined) => void` |
| handleOpen ^(2.2.16) | open the TimePicker popper | ^[Function]`() => void` |
| handleClose ^(2.2.16) | close the TimePicker popper | ^[Function]`() => void` |
| Name | Description | Type |
| --------------------- | ------------------------------ | ----------------------- |
| focus | focus the TimePicker component | ^[Function]`() => void` |
| blur | blur the TimePicker component | ^[Function]`() => void` |
| handleOpen ^(2.2.16) | open the TimePicker popper | ^[Function]`() => void` |
| handleClose ^(2.2.16) | close the TimePicker popper | ^[Function]`() => void` |

View File

@ -262,7 +262,7 @@ describe('DatePicker', () => {
await nextTick()
await rAF()
expect(focusHandler).toHaveBeenCalledTimes(1)
expect(blurHandler).toHaveBeenCalledTimes(1)
expect(blurHandler).toHaveBeenCalled()
expect(keydownHandler).toHaveBeenCalledTimes(1)
input.trigger('focus')
await nextTick()
@ -832,6 +832,73 @@ describe('DatePicker', () => {
expect(wrapper.findComponent(Input).vm.modelValue).toBe(dateStr)
})
})
describe('It should generate accessible attributes', () => {
it('should generate aria attributes', async () => {
const wrapper = _mount(
`<el-date-picker
v-model="value"
type="date"
aria-label="Date picker"
/>`,
() => ({ value: '' })
)
const input = wrapper.find('input')
expect(input.attributes('role')).toBe('combobox')
expect(input.attributes('aria-controls')).toBeTruthy()
expect(input.attributes('aria-expanded')).toBe('false')
expect(input.attributes('aria-haspopup')).toBe('dialog')
expect(input.attributes('aria-label')).toBe('Date picker')
input.trigger('focus')
await nextTick()
const popper = document.querySelector('.el-picker__popper')
expect(input.attributes('aria-expanded')).toBe('true')
expect(input.attributes('aria-controls')).toBe(popper.getAttribute('id'))
expect(popper.getAttribute('role')).toBe('dialog')
expect(popper.getAttribute('aria-modal')).toBe('false')
expect(popper.getAttribute('aria-hidden')).toBe('false')
})
it('should generate aria attributes for range', async () => {
const wrapper = _mount(
`<el-date-picker
v-model="value"
type="daterange"
aria-label="Date picker"
/>`,
() => ({ value: [] })
)
const inputs = wrapper.findAll('input')
expect(inputs[0].attributes('role')).toBe('combobox')
expect(inputs[0].attributes('aria-controls')).toBeTruthy()
expect(inputs[0].attributes('aria-expanded')).toBe('false')
expect(inputs[0].attributes('aria-haspopup')).toBe('dialog')
expect(inputs[0].attributes('aria-label')).toBe('Date picker')
expect(inputs[1].attributes('role')).toBe('combobox')
expect(inputs[1].attributes('aria-controls')).toBeTruthy()
expect(inputs[1].attributes('aria-expanded')).toBe('false')
expect(inputs[1].attributes('aria-haspopup')).toBe('dialog')
expect(inputs[1].attributes('aria-label')).toBe('Date picker')
expect(inputs[1].attributes('aria-controls')).toBe(
inputs[0].attributes('aria-controls')
)
wrapper.find('input').trigger('focus')
await nextTick()
const popper = document.querySelector('.el-picker__popper')
expect(inputs[0].attributes('aria-expanded')).toBe('true')
expect(inputs[0].attributes('aria-controls')).toBe(
popper.getAttribute('id')
)
expect(popper.getAttribute('role')).toBe('dialog')
expect(popper.getAttribute('aria-modal')).toBe('false')
expect(popper.getAttribute('aria-hidden')).toBe('false')
})
})
})
describe('DatePicker Navigation', () => {

View File

@ -45,8 +45,11 @@ export default defineComponent({
const commonPicker = ref<InstanceType<typeof CommonPicker>>()
const refProps: DatePickerExpose = {
focus: (focusStartInput = true) => {
commonPicker.value?.focus(focusStartInput)
focus: () => {
commonPicker.value?.focus()
},
blur: () => {
commonPicker.value?.blur()
},
handleOpen: () => {
commonPicker.value?.handleOpen()

View File

@ -2,7 +2,8 @@ import type { ComponentPublicInstance } from 'vue'
import type { DatePickerProps } from './props/date-picker'
export type DatePickerExpose = {
focus: (focusStartInput: boolean) => void
focus: () => void
blur: () => void
handleOpen: () => void
handleClose: () => void
}

View File

@ -1,6 +1,5 @@
<template>
<div
v-bind="containerAttrs"
:class="[
containerKls,
{
@ -9,7 +8,6 @@
},
]"
:style="containerStyle"
:role="containerRole"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
@ -48,6 +46,7 @@
:style="inputStyle"
:form="form"
:autofocus="autofocus"
:role="containerRole"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@ -126,6 +125,7 @@
:form="form"
:autofocus="autofocus"
:rows="rows"
:role="containerRole"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@ -201,18 +201,9 @@ const props = defineProps(inputProps)
const emit = defineEmits(inputEmits)
const rawAttrs = useRawAttrs()
const attrs = useAttrs()
const slots = useSlots()
const containerAttrs = computed(() => {
const comboBoxAttrs: Record<string, unknown> = {}
if (props.containerRole === 'combobox') {
comboBoxAttrs['aria-haspopup'] = rawAttrs['aria-haspopup']
comboBoxAttrs['aria-owns'] = rawAttrs['aria-owns']
comboBoxAttrs['aria-expanded'] = rawAttrs['aria-expanded']
}
return comboBoxAttrs
})
const containerKls = computed(() => [
props.type === 'textarea' ? nsTextarea.b() : nsInput.b(),
nsInput.m(inputSize.value),
@ -235,11 +226,6 @@ const wrapperKls = computed(() => [
nsInput.is('focus', isFocused.value),
])
const attrs = useAttrs({
excludeKeys: computed<string[]>(() => {
return Object.keys(containerAttrs.value)
}),
})
const { form: elForm, formItem: elFormItem } = useFormItem()
const { inputId } = useFormItemInputId(props, {
formItemContext: elFormItem,

View File

@ -17,7 +17,7 @@ import { isNil } from 'lodash-unified'
import { unrefElement } from '@vueuse/core'
import { ElOnlyChild } from '@element-plus/components/slot'
import { useForwardRef } from '@element-plus/hooks'
import { isElement } from '@element-plus/utils'
import { isElement, isFocusable } from '@element-plus/utils'
import { POPPER_INJECTION_KEY } from './constants'
import { popperTriggerProps } from './trigger'
@ -100,24 +100,26 @@ onMounted(() => {
)
}
})
virtualTriggerAriaStopWatch = watch(
[ariaControls, ariaDescribedby, ariaHaspopup, ariaExpanded],
(watches) => {
;[
'aria-controls',
'aria-describedby',
'aria-haspopup',
'aria-expanded',
].forEach((key, idx) => {
isNil(watches[idx])
? el.removeAttribute(key)
: el.setAttribute(key, watches[idx]!)
})
},
{ immediate: true }
)
if (isFocusable(el as HTMLElement)) {
virtualTriggerAriaStopWatch = watch(
[ariaControls, ariaDescribedby, ariaHaspopup, ariaExpanded],
(watches) => {
;[
'aria-controls',
'aria-describedby',
'aria-haspopup',
'aria-expanded',
].forEach((key, idx) => {
isNil(watches[idx])
? el.removeAttribute(key)
: el.setAttribute(key, watches[idx]!)
})
},
{ immediate: true }
)
}
}
if (isElement(prevEl)) {
if (isElement(prevEl) && isFocusable(prevEl as HTMLElement)) {
;[
'aria-controls',
'aria-describedby',

View File

@ -224,7 +224,7 @@ describe('TimePicker', () => {
await nextTick()
await rAF()
expect(focusHandler).toHaveBeenCalledTimes(1)
expect(blurHandler).toHaveBeenCalledTimes(1)
expect(blurHandler).toHaveBeenCalled()
expect(keydownHandler).toHaveBeenCalledTimes(1)
input.trigger('focus')
@ -316,36 +316,15 @@ describe('TimePicker', () => {
expect(enabledSeconds).toEqual([0])
})
it('ref focus', async () => {
it('exposed focus & blur', async () => {
const value = ref(new Date(2016, 9, 10, 18, 40))
const wrapper = mount(() => <TimePicker v-model={value.value} />)
await nextTick()
wrapper.findComponent(TimePicker).vm.$.exposed.focus()
// This one allows mounted to take effect
await nextTick()
// These following two allows popper to gets rendered.
await rAF()
const popperEl = document.querySelector('.el-picker__popper')
const attr = popperEl.getAttribute('aria-hidden')
expect(attr).toEqual('false')
})
it('ref blur', async () => {
const value = ref(new Date(2016, 9, 10, 18, 40))
const wrapper = mount(() => <TimePicker v-model={value.value} />)
const timePickerExposed = wrapper.findComponent(TimePicker).vm.$.exposed
await nextTick()
timePickerExposed.focus()
await nextTick()
timePickerExposed.blur()
await nextTick()
const popperEl = document.querySelector('.el-picker__popper')
const attr = popperEl.getAttribute('aria-hidden')
expect(attr).toEqual('false')
expect(timePickerExposed.focus).toBeTruthy()
expect(timePickerExposed.blur).toBeTruthy()
})
it('ref handleOpen', async () => {
@ -957,4 +936,60 @@ describe('TimePicker(range)', () => {
await nextTick()
expect(value.value).toEqual(null)
})
describe('It should generate accessible attributes', () => {
it('should generate aria attributes', async () => {
const wrapper = mount(() => <TimePicker aria-label="time picker" />)
const input = wrapper.find('input')
expect(input.attributes('role')).toBe('combobox')
expect(input.attributes('aria-controls')).toBeTruthy()
expect(input.attributes('aria-haspopup')).toBe('dialog')
expect(input.attributes('aria-expanded')).toBe('false')
expect(input.attributes('aria-label')).toBe('time picker')
input.trigger('focus')
await nextTick()
await rAF()
const popper = document.querySelector('.el-picker__popper')
expect(input.attributes('aria-expanded')).toBe('true')
expect(input.attributes('aria-controls')).toBe(popper?.getAttribute('id'))
expect(popper?.getAttribute('role')).toBe('dialog')
expect(popper?.getAttribute('aria-hidden')).toBe('false')
expect(popper?.getAttribute('aria-modal')).toBe('false')
})
it('should generate aria attributes for range', async () => {
const wrapper = mount(() => (
<TimePicker is-range aria-label="time picker" />
))
const inputs = wrapper.findAll('input')
expect(inputs[0].attributes('role')).toBe('combobox')
expect(inputs[0].attributes('aria-controls')).toBeTruthy()
expect(inputs[0].attributes('aria-haspopup')).toBe('dialog')
expect(inputs[0].attributes('aria-expanded')).toBe('false')
expect(inputs[0].attributes('aria-label')).toBe('time picker')
expect(inputs[1].attributes('role')).toBe('combobox')
expect(inputs[1].attributes('aria-controls')).toBeTruthy()
expect(inputs[1].attributes('aria-haspopup')).toBe('dialog')
expect(inputs[1].attributes('aria-expanded')).toBe('false')
expect(inputs[1].attributes('aria-label')).toBe('time picker')
expect(inputs[0].attributes('aria-controls')).toBe(
inputs[1].attributes('aria-controls')
)
wrapper.find('input').trigger('focus')
await nextTick()
await rAF()
const popper = document.querySelector('.el-picker__popper')
expect(inputs[0].attributes('aria-expanded')).toBe('true')
expect(inputs[1].attributes('aria-expanded')).toBe('true')
expect(inputs[0].attributes('aria-controls')).toBe(
popper?.getAttribute('id')
)
expect(popper?.getAttribute('role')).toBe('dialog')
expect(popper?.getAttribute('aria-hidden')).toBe('false')
expect(popper?.getAttribute('aria-modal')).toBe('false')
})
})
})

View File

@ -0,0 +1,117 @@
<template>
<div
ref="wrapperRef"
:class="[nsDate.is('active', isFocused), $attrs.class]"
:style="($attrs.style as any)"
@click="handleClick"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@touchstart="handleTouchStart"
>
<slot name="prefix" />
<input
v-bind="attrs"
:id="id && id[0]"
ref="inputRef"
:name="name && name[0]"
:placeholder="startPlaceholder"
:value="modelValue && modelValue[0]"
:class="nsRange.b('input')"
@input="handleStartInput"
@change="handleStartChange"
/>
<slot name="range-separator" />
<input
v-bind="attrs"
:id="id && id[1]"
ref="endInputRef"
:name="name && name[1]"
:placeholder="endPlaceholder"
:value="modelValue && modelValue[1]"
:class="nsRange.b('input')"
@input="handleEndInput"
@change="handleEndChange"
/>
<slot name="suffix" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useAttrs, useFocusController, useNamespace } from '@element-plus/hooks'
import { timePickerRngeTriggerProps } from './props'
defineOptions({
name: 'PickerRangeTrigger',
inheritAttrs: false,
})
defineProps(timePickerRngeTriggerProps)
const emit = defineEmits([
'mouseenter',
'mouseleave',
'click',
'touchstart',
'focus',
'blur',
'startInput',
'endInput',
'startChange',
'endChange',
])
const attrs = useAttrs()
const nsDate = useNamespace('date')
const nsRange = useNamespace('range')
const inputRef = ref<HTMLInputElement>()
const endInputRef = ref<HTMLInputElement>()
const { wrapperRef, isFocused } = useFocusController(inputRef)
const handleClick = (evt: MouseEvent) => {
emit('click', evt)
}
const handleMouseEnter = (evt: MouseEvent) => {
emit('mouseenter', evt)
}
const handleMouseLeave = (evt: MouseEvent) => {
emit('mouseleave', evt)
}
const handleTouchStart = (evt: TouchEvent) => {
emit('mouseenter', evt)
}
const handleStartInput = (evt: Event) => {
emit('startInput', evt)
}
const handleEndInput = (evt: Event) => {
emit('endInput', evt)
}
const handleStartChange = (evt: Event) => {
emit('startChange', evt)
}
const handleEndChange = (evt: Event) => {
emit('endChange', evt)
}
const focus = () => {
inputRef.value?.focus()
}
const blur = () => {
inputRef.value?.blur()
endInputRef.value?.blur()
}
defineExpose({
focus,
blur,
})
</script>

View File

@ -46,12 +46,9 @@
:tabindex="tabindex"
:validate-event="false"
@input="onUserInput"
@focus="handleFocusInput"
@blur="handleBlurInput"
@keydown="
//
handleKeydownInput as any
"
@focus="handleFocus"
@blur="handleBlur"
@keydown="handleKeydownInput"
@change="handleChange"
@mousedown="onMouseDownInput"
@mouseenter="onMouseEnter"
@ -73,72 +70,66 @@
<el-icon
v-if="showClose && clearIcon"
:class="`${nsInput.e('icon')} clear-icon`"
@click.stop="onClearIconClick"
@mousedown.prevent="NOOP"
@click="onClearIconClick"
>
<component :is="clearIcon" />
</el-icon>
</template>
</el-input>
<div
<picker-range-trigger
v-else
:id="(id as string[] | undefined)"
ref="inputRef"
:model-value="displayValue"
:name="(name as string[] | undefined)"
:disabled="pickerDisabled"
:readonly="!editable || readonly"
:start-placeholder="startPlaceholder"
:end-placeholder="endPlaceholder"
:class="rangeInputKls"
:style="($attrs.style as any)"
@click="handleFocusInput"
:style="$attrs.style"
:aria-label="ariaLabel"
:tabindex="tabindex"
autocomplete="off"
role="combobox"
@click="onMouseDownInput"
@focus="handleFocus"
@blur="handleBlur"
@start-input="handleStartInput"
@start-change="handleStartChange"
@end-input="handleEndInput"
@end-change="handleEndChange"
@mousedown="onMouseDownInput"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
@touchstart.passive="onTouchStartInput"
@keydown="handleKeydownInput"
>
<el-icon
v-if="triggerIcon"
:class="[nsInput.e('icon'), nsRange.e('icon')]"
@mousedown.prevent="onMouseDownInput"
@touchstart.passive="onTouchStartInput"
>
<component :is="triggerIcon" />
</el-icon>
<input
:id="id && id[0]"
autocomplete="off"
:name="name && name[0]"
:placeholder="startPlaceholder"
:value="displayValue && displayValue[0]"
:disabled="pickerDisabled"
:readonly="!editable || readonly"
:class="nsRange.b('input')"
@mousedown="onMouseDownInput"
@input="handleStartInput"
@change="handleStartChange"
@focus="handleFocusInput"
@blur="handleBlurInput"
/>
<slot name="range-separator">
<span :class="nsRange.b('separator')">{{ rangeSeparator }}</span>
</slot>
<input
:id="id && id[1]"
autocomplete="off"
:name="name && name[1]"
:placeholder="endPlaceholder"
:value="displayValue && displayValue[1]"
:disabled="pickerDisabled"
:readonly="!editable || readonly"
:class="nsRange.b('input')"
@mousedown="onMouseDownInput"
@focus="handleFocusInput"
@blur="handleBlurInput"
@input="handleEndInput"
@change="handleEndChange"
/>
<el-icon
v-if="clearIcon"
:class="clearIconKls"
@click="onClearIconClick"
>
<component :is="clearIcon" />
</el-icon>
</div>
<template #prefix>
<el-icon
v-if="triggerIcon"
:class="[nsInput.e('icon'), nsRange.e('icon')]"
>
<component :is="triggerIcon" />
</el-icon>
</template>
<template #range-separator>
<slot name="range-separator">
<span :class="nsRange.b('separator')">{{ rangeSeparator }}</span>
</slot>
</template>
<template #suffix>
<el-icon
v-if="clearIcon"
:class="clearIconKls"
@mousedown.prevent="NOOP"
@click="onClearIconClick"
>
<component :is="clearIcon" />
</el-icon>
</template>
</picker-range-trigger>
</template>
<template #content>
<slot
@ -157,7 +148,6 @@
@set-picker-option="onSetPickerOption"
@calendar-change="onCalendarChange"
@panel-change="onPanelChange"
@keydown="onKeydownPopperContent"
@mousedown.stop
/>
</template>
@ -176,20 +166,27 @@ import {
watch,
} from 'vue'
import { isEqual } from 'lodash-unified'
import { onClickOutside } from '@vueuse/core'
import { useEmptyValues, useLocale, useNamespace } from '@element-plus/hooks'
import { onClickOutside, unrefElement } from '@vueuse/core'
import {
useEmptyValues,
useFocusController,
useLocale,
useNamespace,
} from '@element-plus/hooks'
import { useFormItem, useFormSize } from '@element-plus/components/form'
import ElInput from '@element-plus/components/input'
import ElIcon from '@element-plus/components/icon'
import ElTooltip from '@element-plus/components/tooltip'
import { debugWarn, isArray } from '@element-plus/utils'
import { NOOP, debugWarn, isArray } from '@element-plus/utils'
import { EVENT_CODE } from '@element-plus/constants'
import { Calendar, Clock } from '@element-plus/icons-vue'
import { formatter, parseDate, valueEquals } from '../utils'
import { timePickerDefaultProps } from './props'
import PickerRangeTrigger from './picker-range-trigger.vue'
import type { InputInstance } from '@element-plus/components/input'
import type { Dayjs } from 'dayjs'
import type { ComponentPublicInstance } from 'vue'
import type { ComponentPublicInstance, Ref } from 'vue'
import type { Options } from '@popperjs/core'
import type {
DateModelType,
@ -233,13 +230,32 @@ const elPopperOptions = inject('ElPopperOptions', {} as Options)
const { valueOnClear } = useEmptyValues(props, null)
const refPopper = ref<TooltipInstance>()
const inputRef = ref<HTMLElement | ComponentPublicInstance>()
const inputRef = ref<InputInstance>()
const pickerVisible = ref(false)
const pickerActualVisible = ref(false)
const valueOnOpen = ref<TimePickerDefaultProps['modelValue'] | null>(null)
let hasJustTabExitedInput = false
let ignoreFocusEvent = false
const { isFocused, handleFocus, handleBlur } = useFocusController(inputRef, {
beforeFocus() {
return !props.editable || props.readonly || pickerDisabled.value
},
afterFocus() {
pickerVisible.value = true
},
beforeBlur(event) {
return (
!hasJustTabExitedInput && refPopper.value?.isFocusInsideContent(event)
)
},
afterBlur() {
handleChange()
pickerVisible.value = false
hasJustTabExitedInput = false
props.validateEvent &&
formItem?.validate('blur').catch((err) => debugWarn(err))
},
})
const rangeInputKls = computed(() => [
nsDate.b('editor'),
@ -302,10 +318,9 @@ const emitKeydown = (e: KeyboardEvent) => {
const refInput = computed<HTMLInputElement[]>(() => {
if (inputRef.value) {
const _r = isRangeInput.value
? inputRef.value
: (inputRef.value as any as ComponentPublicInstance).$el
return Array.from<HTMLInputElement>(_r.querySelectorAll('input'))
return Array.from<HTMLInputElement>(
inputRef.value.$el.querySelectorAll('input')
)
}
return []
})
@ -321,17 +336,8 @@ const setSelectionRange = (start: number, end: number, pos?: 'min' | 'max') => {
_inputs[1].focus()
}
}
const focusOnInputBox = () => {
focus(true, true)
nextTick(() => {
ignoreFocusEvent = false
})
}
const onPick = (date: any = '', visible = false) => {
if (!visible) {
ignoreFocusEvent = true
}
pickerVisible.value = visible
let result
if (isArray(date)) {
@ -352,16 +358,9 @@ const onShow = () => {
emit('visible-change', true)
}
const onKeydownPopperContent = (event: KeyboardEvent) => {
if ((event as KeyboardEvent)?.key === EVENT_CODE.esc) {
focus(true, true)
}
}
const onHide = () => {
pickerActualVisible.value = false
pickerVisible.value = false
ignoreFocusEvent = false
emit('visible-change', false)
}
@ -373,62 +372,6 @@ const handleClose = () => {
pickerVisible.value = false
}
const focus = (focusStartInput = true, isIgnoreFocusEvent = false) => {
ignoreFocusEvent = isIgnoreFocusEvent
const [leftInput, rightInput] = unref(refInput)
let input = leftInput
if (!focusStartInput && isRangeInput.value) {
input = rightInput
}
if (input) {
input.focus()
}
}
const handleFocusInput = (e?: FocusEvent) => {
if (
props.readonly ||
pickerDisabled.value ||
pickerVisible.value ||
ignoreFocusEvent
) {
return
}
pickerVisible.value = true
emit('focus', e)
}
let currentHandleBlurDeferCallback:
| (() => Promise<void> | undefined)
| undefined = undefined
// Check if document.activeElement is inside popper or any input before popper close
const handleBlurInput = (e?: FocusEvent) => {
const handleBlurDefer = async () => {
setTimeout(() => {
if (currentHandleBlurDeferCallback === handleBlurDefer) {
if (
!(
refPopper.value?.isFocusInsideContent() && !hasJustTabExitedInput
) &&
refInput.value.filter((input) => {
return input.contains(document.activeElement)
}).length === 0
) {
handleChange()
pickerVisible.value = false
emit('blur', e)
props.validateEvent &&
formItem?.validate('blur').catch((err) => debugWarn(err))
}
hasJustTabExitedInput = false
}
}, 0)
}
currentHandleBlurDeferCallback = handleBlurDefer
handleBlurDefer()
}
const pickerDisabled = computed(() => {
return props.disabled || form?.disabled
})
@ -513,7 +456,6 @@ const onClearIconClick = (event: MouseEvent) => {
if (props.readonly || pickerDisabled.value) return
if (showClose.value) {
event.stopPropagation()
focusOnInputBox()
// When the handleClear Function was provided, emit null will be executed inside it
// There is no need for us to execute emit null twice. #14752
if (pickerOptions.value.handleClear) {
@ -537,10 +479,7 @@ const valueIsEmpty = computed(() => {
const onMouseDownInput = async (event: MouseEvent) => {
if (props.readonly || pickerDisabled.value) return
if (
(event.target as HTMLElement)?.tagName !== 'INPUT' ||
refInput.value.includes(document.activeElement as HTMLInputElement)
) {
if ((event.target as HTMLElement)?.tagName !== 'INPUT' || isFocused.value) {
pickerVisible.value = true
}
}
@ -553,15 +492,17 @@ const onMouseEnter = () => {
const onMouseLeave = () => {
showClose.value = false
}
const onTouchStartInput = (event: TouchEvent) => {
if (props.readonly || pickerDisabled.value) return
if (
(event.touches[0].target as HTMLElement)?.tagName !== 'INPUT' ||
refInput.value.includes(document.activeElement as HTMLInputElement)
isFocused.value
) {
pickerVisible.value = true
}
}
const isRangeInput = computed(() => {
return props.type.includes('range')
})
@ -569,27 +510,23 @@ const isRangeInput = computed(() => {
const pickerSize = useFormSize()
const popperEl = computed(() => unref(refPopper)?.popperRef?.contentRef)
const actualInputRef = computed(() => {
if (unref(isRangeInput)) {
return unref(inputRef)
const stophandle = onClickOutside(
inputRef as Ref<ComponentPublicInstance>,
(e: PointerEvent) => {
const unrefedPopperEl = unref(popperEl)
const inputEl = unrefElement(inputRef as Ref<ComponentPublicInstance>)
if (
(unrefedPopperEl &&
(e.target === unrefedPopperEl ||
e.composedPath().includes(unrefedPopperEl))) ||
e.target === inputEl ||
(inputEl && e.composedPath().includes(inputEl))
)
return
pickerVisible.value = false
}
return (unref(inputRef) as ComponentPublicInstance)?.$el
})
const stophandle = onClickOutside(actualInputRef, (e: PointerEvent) => {
const unrefedPopperEl = unref(popperEl)
const inputEl = unref(actualInputRef)
if (
(unrefedPopperEl &&
(e.target === unrefedPopperEl ||
e.composedPath().includes(unrefedPopperEl))) ||
e.target === inputEl ||
e.composedPath().includes(inputEl)
)
return
pickerVisible.value = false
})
)
onBeforeUnmount(() => {
stophandle?.()
@ -632,11 +569,11 @@ const isValidValue = (value: DayOrDays) => {
return pickerOptions.value.isValidValue!(value)
}
const handleKeydownInput = async (event: KeyboardEvent) => {
const handleKeydownInput = async (event: Event | KeyboardEvent) => {
if (props.readonly || pickerDisabled.value) return
const { code } = event
emitKeydown(event)
const { code } = event as KeyboardEvent
emitKeydown(event as KeyboardEvent)
if (code === EVENT_CODE.esc) {
if (pickerVisible.value === true) {
pickerVisible.value = false
@ -685,7 +622,7 @@ const handleKeydownInput = async (event: KeyboardEvent) => {
return
}
if (pickerOptions.value.handleKeydownInput) {
pickerOptions.value.handleKeydownInput(event)
pickerOptions.value.handleKeydownInput(event as KeyboardEvent)
}
}
const onUserInput = (e: string) => {
@ -769,6 +706,14 @@ const onPanelChange = (
emit('panel-change', value, mode, view)
}
const focus = () => {
inputRef.value?.focus()
}
const blur = () => {
inputRef.value?.blur()
}
provide('EP_PICKER_BASE', {
props,
})
@ -779,13 +724,9 @@ defineExpose({
*/
focus,
/**
* @description emit focus event
* @description blur input box.
*/
handleFocusInput,
/**
* @description emit blur event
*/
handleBlurInput,
blur,
/**
* @description opens picker
*/

View File

@ -44,7 +44,6 @@ export const timePickerDefaultProps = buildProps({
*/
name: {
type: definePropType<SingleOrRange<string>>([Array, String]),
default: '',
},
/**
* @description custom class name for TimePicker's dropdown
@ -251,3 +250,17 @@ export interface PickerOptions {
handleClear: () => void
handleFocusPicker?: () => void
}
export const timePickerRngeTriggerProps = buildProps({
id: {
type: definePropType<string[]>(Array),
},
name: {
type: definePropType<string[]>(Array),
},
modelValue: {
type: definePropType<UserInput>([Array, String]),
},
startPlaceholder: String,
endPlaceholder: String,
} as const)

View File

@ -291,11 +291,14 @@ const handleClick = (
}
const handleScroll = (type: TimeUnit) => {
const scrollbar = unref(listRefsMap[type])
if (!scrollbar) return
isScrolling = true
debouncedResetScroll(type)
const value = Math.min(
Math.round(
(getScrollbarElement(unref(listRefsMap[type])!.$el).scrollTop -
(getScrollbarElement(scrollbar.$el).scrollTop -
(scrollBarHeight(type) * 0.5 - 10) / typeItemHeight(type) +
3) /
typeItemHeight(type)
@ -334,8 +337,8 @@ onMounted(() => {
})
})
const setRef = (scrollbar: ScrollbarInstance, type: TimeUnit) => {
listRefsMap[type].value = scrollbar
const setRef = (scrollbar: ScrollbarInstance | null, type: TimeUnit) => {
listRefsMap[type].value = scrollbar ?? undefined
}
emit('set-option', [`${props.role}_scrollDown`, scrollDown])

View File

@ -34,14 +34,14 @@ export default defineComponent({
/**
* @description focus the Input component
*/
focus: (e: FocusEvent | undefined) => {
commonPicker.value?.handleFocusInput(e)
focus: () => {
commonPicker.value?.focus()
},
/**
* @description blur the Input component
*/
blur: (e: FocusEvent | undefined) => {
commonPicker.value?.handleBlurInput(e)
blur: () => {
commonPicker.value?.blur()
},
/**
* @description open the TimePicker popper

View File

@ -31,8 +31,11 @@ export const isFocusable = (element: HTMLElement): boolean => {
) {
return true
}
// HTMLButtonElement has disabled
if ((element as HTMLButtonElement).disabled) {
if (
element.tabIndex < 0 ||
element.hasAttribute('disabled') ||
element.getAttribute('aria-disabled') === 'true'
) {
return false
}