Skip to content

Commit

Permalink
feat(components): [color-picker] add focus and blur event (element-pl…
Browse files Browse the repository at this point in the history
…us#14244)

* feat(components): [color-picker] add focus blur event

* docs: updata
  • Loading branch information
tolking authored and cc-hearts committed Oct 15, 2023
1 parent 56eea50 commit 3de49c7
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 20 deletions.
24 changes: 14 additions & 10 deletions docs/en-US/component/color-picker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
16 changes: 16 additions & 0 deletions packages/components/color-picker/__tests__/color-picker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
2 changes: 2 additions & 0 deletions packages/components/color-picker/src/color-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
90 changes: 85 additions & 5 deletions packages/components/color-picker/src/color-picker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand All @@ -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"
Expand Down Expand Up @@ -58,15 +60,19 @@
<template #default>
<div
:id="buttonId"
ref="triggerRef"
:class="btnKls"
role="button"
:aria-label="buttonAriaLabel"
:aria-labelledby="buttonAriaLabelledby"
: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">
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
]
})
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -344,5 +416,13 @@ defineExpose({
* @description manually hide ColorPicker
*/
hide,
/**
* @description focus the input element
*/
focus,
/**
* @description blur the input element
*/
blur,
})
</script>
129 changes: 129 additions & 0 deletions packages/hooks/__tests__/use-focus-controller.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
})
})
14 changes: 11 additions & 3 deletions packages/hooks/use-focus-controller/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
Loading

0 comments on commit 3de49c7

Please sign in to comment.