mirror of
https://github.com/element-plus/element-plus.git
synced 2024-11-21 01:02:59 +08:00
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:
parent
0e8454a99e
commit
845c07adef
@ -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
|
||||
|
||||
|
@ -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` |
|
||||
|
@ -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` |
|
||||
|
@ -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', () => {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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>
|
@ -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
|
||||
*/
|
||||
|
@ -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)
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user