mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
feat(components): [color-picker] add focus and blur event (#14244)
* feat(components): [color-picker] add focus blur event * docs: updata
This commit is contained in:
@@ -60,20 +60,24 @@ color-picker/sizes
|
||||
| predefine | predefined color options | ^[object]`string[]` | — |
|
||||
| validate-event | whether to trigger form validation | ^[boolean] | true |
|
||||
| tabindex | ColorPicker tabindex | ^[string] / ^[number] | 0 |
|
||||
| label<A11yTag/> | ColorPicker aria-label | ^[string] | — |
|
||||
| label ^(a11y) | ColorPicker aria-label | ^[string] | — |
|
||||
| id | ColorPicker id | ^[string] | — |
|
||||
|
||||
### Events
|
||||
|
||||
| Name | Description | Type |
|
||||
| ------------- | ---------------------------------------------- | ------------------------------------ |
|
||||
| change | triggers when input value changes | ^[Function]`(value: string) => void` |
|
||||
| active-change | triggers when the current active color changes | ^[Function]`(value: string) => void` |
|
||||
| Name | Description | Type |
|
||||
| -------------- | ---------------------------------------------- | ---------------------------------------- |
|
||||
| change | triggers when input value changes | ^[Function]`(value: string) => void` |
|
||||
| active-change | triggers when the current active color changes | ^[Function]`(value: string) => void` |
|
||||
| focus ^(2.4.0) | triggers when Component focuses | ^[Function]`(event: FocusEvent) => void` |
|
||||
| blur ^(2.4.0) | triggers when Component blurs | ^[Function]`(event: FocusEvent) => void` |
|
||||
|
||||
### Exposes
|
||||
|
||||
| Name | Description | Type |
|
||||
| ------------- | ------------------------- | ----------------------- |
|
||||
| color | current color object | ^[object]`Color` |
|
||||
| show ^(2.3.3) | manually show ColorPicker | ^[Function]`() => void` |
|
||||
| hide ^(2.3.3) | manually hide ColorPicker | ^[Function]`() => void` |
|
||||
| Name | Description | Type |
|
||||
| --------------- | ------------------------- | ----------------------- |
|
||||
| color | current color object | ^[object]`Color` |
|
||||
| show ^(2.3.3) | manually show ColorPicker | ^[Function]`() => void` |
|
||||
| hide ^(2.3.3) | manually hide ColorPicker | ^[Function]`() => void` |
|
||||
| focus ^(2.3.13) | focus the picker element | ^[Function]`() => void` |
|
||||
| blur ^(2.3.13) | blur the picker element | ^[Function]`() => void` |
|
||||
|
||||
@@ -453,4 +453,20 @@ describe('Color-picker', () => {
|
||||
expect(formItem.attributes().role).toBe('group')
|
||||
})
|
||||
})
|
||||
|
||||
it('it will target the focus & blur', async () => {
|
||||
const focusHandler = vi.fn()
|
||||
const blurHandler = vi.fn()
|
||||
const wrapper = mount(() => (
|
||||
<ColorPicker onFocus={focusHandler} onBlur={blurHandler} />
|
||||
))
|
||||
|
||||
await nextTick()
|
||||
await wrapper.find('.el-color-picker').trigger('focus')
|
||||
expect(focusHandler).toHaveBeenCalledTimes(1)
|
||||
|
||||
await wrapper.find('.el-color-picker').trigger('blur')
|
||||
expect(blurHandler).toHaveBeenCalledTimes(1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -70,6 +70,8 @@ export const colorPickerEmits = {
|
||||
[UPDATE_MODEL_EVENT]: (val: string | null) => isString(val) || isNil(val),
|
||||
[CHANGE_EVENT]: (val: string | null) => isString(val) || isNil(val),
|
||||
activeChange: (val: string | null) => isString(val) || isNil(val),
|
||||
focus: (event: FocusEvent) => event instanceof FocusEvent,
|
||||
blur: (event: FocusEvent) => event instanceof FocusEvent,
|
||||
}
|
||||
|
||||
export type ColorPickerProps = ExtractPropTypes<typeof colorPickerProps>
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
trigger="click"
|
||||
:transition="`${ns.namespace.value}-zoom-in-top`"
|
||||
persistent
|
||||
@hide="setShowPicker(false)"
|
||||
>
|
||||
<template #content>
|
||||
<div v-click-outside="hide">
|
||||
<div v-click-outside="handleClickOutside" @keydown.esc="handleEsc">
|
||||
<div :class="ns.be('dropdown', 'main-wrapper')">
|
||||
<hue-slider ref="hue" class="hue-slider" :color="color" vertical />
|
||||
<sv-panel ref="sv" :color="color" />
|
||||
@@ -29,6 +30,7 @@
|
||||
<div :class="ns.be('dropdown', 'btns')">
|
||||
<span :class="ns.be('dropdown', 'value')">
|
||||
<el-input
|
||||
ref="inputRef"
|
||||
v-model="customInput"
|
||||
:validate-event="false"
|
||||
size="small"
|
||||
@@ -58,6 +60,7 @@
|
||||
<template #default>
|
||||
<div
|
||||
:id="buttonId"
|
||||
ref="triggerRef"
|
||||
:class="btnKls"
|
||||
role="button"
|
||||
:aria-label="buttonAriaLabel"
|
||||
@@ -65,8 +68,11 @@
|
||||
:aria-description="
|
||||
t('el.colorpicker.description', { color: modelValue || '' })
|
||||
"
|
||||
:tabindex="tabindex"
|
||||
@keydown.enter="handleTrigger"
|
||||
:aria-disabled="colorDisabled"
|
||||
:tabindex="colorDisabled ? -1 : tabindex"
|
||||
@keydown="handleKeyDown"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
>
|
||||
<div v-if="colorDisabled" :class="ns.be('picker', 'mask')" />
|
||||
<div :class="ns.be('picker', 'trigger')" @click="handleTrigger">
|
||||
@@ -119,8 +125,12 @@ import {
|
||||
useFormItemInputId,
|
||||
useFormSize,
|
||||
} from '@element-plus/components/form'
|
||||
import { useLocale, useNamespace } from '@element-plus/hooks'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import {
|
||||
useFocusController,
|
||||
useLocale,
|
||||
useNamespace,
|
||||
} from '@element-plus/hooks'
|
||||
import { EVENT_CODE, UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { debugWarn } from '@element-plus/utils'
|
||||
import { ArrowDown, Close } from '@element-plus/icons-vue'
|
||||
import AlphaSlider from './components/alpha-slider.vue'
|
||||
@@ -155,6 +165,27 @@ const hue = ref<InstanceType<typeof HueSlider>>()
|
||||
const sv = ref<InstanceType<typeof SvPanel>>()
|
||||
const alpha = ref<InstanceType<typeof AlphaSlider>>()
|
||||
const popper = ref<TooltipInstance>()
|
||||
const triggerRef = ref()
|
||||
const inputRef = ref()
|
||||
|
||||
const {
|
||||
isFocused,
|
||||
handleFocus: _handleFocus,
|
||||
handleBlur,
|
||||
} = useFocusController(triggerRef, {
|
||||
beforeBlur(event) {
|
||||
return popper.value?.isFocusInsideContent(event)
|
||||
},
|
||||
afterBlur() {
|
||||
setShowPicker(false)
|
||||
resetColor()
|
||||
},
|
||||
})
|
||||
|
||||
const handleFocus = (event: FocusEvent) => {
|
||||
if (colorDisabled.value) return blur()
|
||||
_handleFocus(event)
|
||||
}
|
||||
|
||||
// active-change is used to prevent modelValue changes from triggering.
|
||||
let shouldActiveChange = true
|
||||
@@ -197,6 +228,7 @@ const btnKls = computed(() => {
|
||||
ns.b('picker'),
|
||||
ns.is('disabled', colorDisabled.value),
|
||||
ns.bm('picker', colorSize.value),
|
||||
ns.is('focused', isFocused.value),
|
||||
]
|
||||
})
|
||||
|
||||
@@ -280,6 +312,46 @@ function clear() {
|
||||
resetColor()
|
||||
}
|
||||
|
||||
function handleClickOutside(event: Event) {
|
||||
if (!showPicker.value) return
|
||||
hide()
|
||||
|
||||
if (isFocused.value) {
|
||||
const _event = new FocusEvent('focus', event)
|
||||
handleBlur(_event)
|
||||
}
|
||||
}
|
||||
|
||||
function handleEsc(event: KeyboardEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setShowPicker(false)
|
||||
resetColor()
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
switch (event.code) {
|
||||
case EVENT_CODE.enter:
|
||||
case EVENT_CODE.space:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
show()
|
||||
inputRef.value.focus()
|
||||
break
|
||||
case EVENT_CODE.esc:
|
||||
handleEsc(event)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function focus() {
|
||||
triggerRef.value.focus()
|
||||
}
|
||||
|
||||
function blur() {
|
||||
triggerRef.value.blur()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.modelValue) {
|
||||
customInput.value = currentColor.value
|
||||
@@ -344,5 +416,13 @@ defineExpose({
|
||||
* @description manually hide ColorPicker
|
||||
*/
|
||||
hide,
|
||||
/**
|
||||
* @description focus the input element
|
||||
*/
|
||||
focus,
|
||||
/**
|
||||
* @description blur the input element
|
||||
*/
|
||||
blur,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -8,6 +8,92 @@ describe('useFocusController', () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('it will trigger focus & blur without wrapperRef', async () => {
|
||||
const focusHandler = vi.fn()
|
||||
const blurHandler = vi.fn()
|
||||
const wrapper = mount({
|
||||
emits: ['focus', 'blur'],
|
||||
setup() {
|
||||
const targetRef = ref()
|
||||
const { isFocused, handleFocus, handleBlur } = useFocusController(
|
||||
targetRef,
|
||||
{
|
||||
afterFocus: focusHandler,
|
||||
afterBlur: blurHandler,
|
||||
}
|
||||
)
|
||||
|
||||
return () => (
|
||||
<div>
|
||||
<input ref={targetRef} onFocus={handleFocus} onBlur={handleBlur} />
|
||||
<span>{String(isFocused.value)}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
expect(wrapper.find('span').text()).toBe('false')
|
||||
expect(focusHandler).toHaveBeenCalledTimes(0)
|
||||
expect(blurHandler).toHaveBeenCalledTimes(0)
|
||||
|
||||
await wrapper.find('input').trigger('focus')
|
||||
expect(wrapper.emitted()).toHaveProperty('focus')
|
||||
expect(wrapper.find('span').text()).toBe('true')
|
||||
expect(focusHandler).toHaveBeenCalledTimes(1)
|
||||
expect(blurHandler).toHaveBeenCalledTimes(0)
|
||||
|
||||
await wrapper.find('input').trigger('blur')
|
||||
expect(wrapper.emitted()).toHaveProperty('blur')
|
||||
expect(wrapper.find('span').text()).toBe('false')
|
||||
expect(blurHandler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('it will trigger focus & blur with tabindex', async () => {
|
||||
const focusHandler = vi.fn()
|
||||
const blurHandler = vi.fn()
|
||||
const wrapper = mount({
|
||||
emits: ['focus', 'blur'],
|
||||
setup() {
|
||||
const targetRef = ref()
|
||||
const { isFocused, handleFocus, handleBlur } = useFocusController(
|
||||
targetRef,
|
||||
{
|
||||
afterFocus: focusHandler,
|
||||
afterBlur: blurHandler,
|
||||
}
|
||||
)
|
||||
|
||||
return () => (
|
||||
<div
|
||||
ref={targetRef}
|
||||
tabindex="0"
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
<span>{String(isFocused.value)}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
expect(wrapper.find('span').text()).toBe('false')
|
||||
expect(focusHandler).toHaveBeenCalledTimes(0)
|
||||
expect(blurHandler).toHaveBeenCalledTimes(0)
|
||||
|
||||
await wrapper.find('div').trigger('focus')
|
||||
expect(wrapper.emitted()).toHaveProperty('focus')
|
||||
expect(wrapper.find('span').text()).toBe('true')
|
||||
expect(focusHandler).toHaveBeenCalledTimes(1)
|
||||
expect(blurHandler).toHaveBeenCalledTimes(0)
|
||||
|
||||
await wrapper.find('div').trigger('blur')
|
||||
expect(wrapper.emitted()).toHaveProperty('blur')
|
||||
expect(wrapper.find('span').text()).toBe('false')
|
||||
expect(blurHandler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('it will avoid trigger unnecessary blur event', async () => {
|
||||
const focusHandler = vi.fn()
|
||||
const blurHandler = vi.fn()
|
||||
@@ -52,4 +138,47 @@ describe('useFocusController', () => {
|
||||
expect(wrapper.find('span').text()).toBe('false')
|
||||
expect(blurHandler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('it will avoid trigger unnecessary blur event by beforeBlur', async () => {
|
||||
const beforeBlur = vi.fn()
|
||||
const wrapper = mount({
|
||||
emits: ['focus', 'blur'],
|
||||
setup() {
|
||||
const targetRef = ref()
|
||||
const { wrapperRef, isFocused, handleFocus, handleBlur } =
|
||||
useFocusController(targetRef, {
|
||||
afterBlur: () => {
|
||||
beforeBlur()
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div ref={wrapperRef}>
|
||||
<input
|
||||
ref={targetRef}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
</div>
|
||||
<span>{String(isFocused.value)}</span>
|
||||
</>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
expect(wrapper.find('span').text()).toBe('false')
|
||||
expect(beforeBlur).toHaveBeenCalledTimes(0)
|
||||
|
||||
await wrapper.find('input').trigger('focus')
|
||||
expect(wrapper.emitted()).toHaveProperty('focus')
|
||||
expect(wrapper.find('span').text()).toBe('true')
|
||||
expect(beforeBlur).toHaveBeenCalledTimes(0)
|
||||
|
||||
await wrapper.find('span').trigger('click')
|
||||
expect(wrapper.emitted()).not.toHaveProperty('blur')
|
||||
expect(beforeBlur).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { getCurrentInstance, ref, shallowRef, watch } from 'vue'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { isFunction } from '@element-plus/utils'
|
||||
import type { ShallowRef } from 'vue'
|
||||
|
||||
interface UseFocusControllerOptions {
|
||||
afterFocus?: () => void
|
||||
/**
|
||||
* return true to cancel blur
|
||||
* @param event FocusEvent
|
||||
*/
|
||||
beforeBlur?: (event: FocusEvent) => boolean | undefined
|
||||
afterBlur?: () => void
|
||||
}
|
||||
|
||||
export function useFocusController<T extends HTMLElement>(
|
||||
target: ShallowRef<T | undefined>,
|
||||
{ afterFocus, afterBlur }: UseFocusControllerOptions = {}
|
||||
{ afterFocus, beforeBlur, afterBlur }: UseFocusControllerOptions = {}
|
||||
) {
|
||||
const instance = getCurrentInstance()!
|
||||
const { emit } = instance
|
||||
@@ -24,9 +30,11 @@ export function useFocusController<T extends HTMLElement>(
|
||||
}
|
||||
|
||||
const handleBlur = (event: FocusEvent) => {
|
||||
const cancelBlur = isFunction(beforeBlur) ? beforeBlur(event) : false
|
||||
if (
|
||||
event.relatedTarget &&
|
||||
wrapperRef.value?.contains(event.relatedTarget as Node)
|
||||
cancelBlur ||
|
||||
(event.relatedTarget &&
|
||||
wrapperRef.value?.contains(event.relatedTarget as Node))
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@@ -246,9 +246,9 @@ $color-picker-size: map.merge($common-component-size, $color-picker-size);
|
||||
line-height: normal;
|
||||
outline: none;
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
&:hover:not(.is-disabled, .is-focused) {
|
||||
.#{$namespace}-color-picker__trigger {
|
||||
border: 1px solid getCssVar('border-color-hover');
|
||||
border-color: getCssVar('border-color-hover');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +259,12 @@ $color-picker-size: map.merge($common-component-size, $color-picker-size);
|
||||
}
|
||||
}
|
||||
|
||||
@include when(focused) {
|
||||
.#{$namespace}-color-picker__trigger {
|
||||
border-color: getCssVar('color-primary');
|
||||
}
|
||||
}
|
||||
|
||||
@include when(disabled) {
|
||||
.#{$namespace}-color-picker__trigger {
|
||||
cursor: not-allowed;
|
||||
|
||||
Reference in New Issue
Block a user