@@ -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
>()
const sv = ref>()
const alpha = ref>()
const popper = ref()
+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,
})
diff --git a/packages/hooks/__tests__/use-focus-controller.test.tsx b/packages/hooks/__tests__/use-focus-controller.test.tsx
index f16c4840b6..bab97e390c 100644
--- a/packages/hooks/__tests__/use-focus-controller.test.tsx
+++ b/packages/hooks/__tests__/use-focus-controller.test.tsx
@@ -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 () => (
+
+
+ {String(isFocused.value)}
+
+ )
+ },
+ })
+
+ 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 () => (
+
+ {String(isFocused.value)}
+
+ )
+ },
+ })
+
+ 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 () => (
+ <>
+
+
+
+ {String(isFocused.value)}
+ >
+ )
+ },
+ })
+
+ 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)
+ })
})
diff --git a/packages/hooks/use-focus-controller/index.ts b/packages/hooks/use-focus-controller/index.ts
index 46122eee23..5b35cc9754 100644
--- a/packages/hooks/use-focus-controller/index.ts
+++ b/packages/hooks/use-focus-controller/index.ts
@@ -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(
target: ShallowRef,
- { afterFocus, afterBlur }: UseFocusControllerOptions = {}
+ { afterFocus, beforeBlur, afterBlur }: UseFocusControllerOptions = {}
) {
const instance = getCurrentInstance()!
const { emit } = instance
@@ -24,9 +30,11 @@ export function useFocusController(
}
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
diff --git a/packages/theme-chalk/src/color-picker.scss b/packages/theme-chalk/src/color-picker.scss
index 912693171e..4ad201526a 100644
--- a/packages/theme-chalk/src/color-picker.scss
+++ b/packages/theme-chalk/src/color-picker.scss
@@ -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;