Skip to content

Commit

Permalink
fix(kotti-fields): dont emit blur event when interacting with the con…
Browse files Browse the repository at this point in the history
…tent inside the field container
  • Loading branch information
santiagoballadares committed Feb 25, 2025
1 parent bcfe992 commit ac642e4
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 103 deletions.
26 changes: 20 additions & 6 deletions packages/kotti-ui/source/kotti-field-currency/KtFieldCurrency.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<template>
<KtField :field="modifiedField" :helpTextSlot="$slots.helpText">
<input ref="inputRef" v-bind="inputProps" @blur="onBlur" @input="onInput" />
<KtField
ref="ktFieldRef"
:field="modifiedField"
:helpTextSlot="$slots.helpText"
>
<input ref="inputRef" v-bind="inputProps" @input="onInput" />
</KtField>
</template>

Expand All @@ -12,7 +16,12 @@ import type { InputHTMLAttributes } from 'vue/types/jsx'
import { Yoco } from '@3yourmind/yoco'
import { KtField } from '../kotti-field'
import { useField, useForceUpdate } from '../kotti-field/hooks'
import {
useEmitBlur,
useField,
useForceUpdate,
useKtFieldRef,
} from '../kotti-field/hooks'
import { useI18nContext } from '../kotti-i18n/hooks'
import type { KottiI18n } from '../kotti-i18n/types'
import { makeProps } from '../make-props'
Expand Down Expand Up @@ -57,6 +66,13 @@ export default defineComponent({
})
const i18nContext = useI18nContext()
const ktFieldRef = useKtFieldRef()
useEmitBlur({
emit,
field,
fieldTarget: computed(() => ktFieldRef.value?.inputContainerRef ?? null),
})
const currencyFormat = computed<KottiI18n.CurrencyMap[string]>(() => {
const result = i18nContext.currencyMap[props.currency]
Expand Down Expand Up @@ -176,13 +192,11 @@ export default defineComponent({
}),
),
inputRef,
ktFieldRef,
modifiedField: computed(() => ({
...field,
prefix: currencyFormat.value.symbol,
})),
onBlur: () => {
emit('blur', field.currentValue)
},
onInput: (event: Event) => {
const value = (event.target as HTMLInputElement).value
Expand Down
49 changes: 17 additions & 32 deletions packages/kotti-ui/source/kotti-field-number/KtFieldNumber.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<KtField v-bind="{ field }" :helpTextSlot="$slots.helpText">
<KtField v-bind="{ field }" ref="ktFieldRef" :helpTextSlot="$slots.helpText">
<div
ref="wrapperRef"
class="kt-field-number"
Expand All @@ -11,7 +11,6 @@
>
<div
v-if="!hideChangeButtons"
ref="decrementButtonRef"
class="kt-field-number__button"
:class="decrementButtonClasses"
:data-test="`${inputProps['data-test']}-decrement`"
Expand All @@ -36,7 +35,6 @@
</div>
<div
v-if="!hideChangeButtons"
ref="incrementButtonRef"
class="kt-field-number__button"
:class="incrementButtonClasses"
:data-test="`${inputProps['data-test']}-increment`"
Expand Down Expand Up @@ -66,7 +64,13 @@ import type { InputHTMLAttributes } from 'vue/types/jsx'
import { Yoco } from '@3yourmind/yoco'
import { KtField } from '../kotti-field'
import { useField, useForceUpdate, useInput } from '../kotti-field/hooks'
import {
useEmitBlur,
useField,
useForceUpdate,
useInput,
useKtFieldRef,
} from '../kotti-field/hooks'
import type { KottiField } from '../kotti-field/types'
import { useI18nContext } from '../kotti-i18n/hooks'
import type { KottiI18n } from '../kotti-i18n/types'
Expand Down Expand Up @@ -102,8 +106,14 @@ export default defineComponent({
const { focusInput } = useInput(field.inputProps.id)
const { forceUpdate, forceUpdateKey } = useForceUpdate()
const i18nContext = useI18nContext()
const ktFieldRef = useKtFieldRef()
useEmitBlur({
emit,
field,
fieldTarget: computed(() => ktFieldRef.value?.inputContainerRef ?? null),
})
const isDecrementEnabled = computed(
() =>
Expand Down Expand Up @@ -250,29 +260,9 @@ export default defineComponent({
}
const wrapperRef = ref<HTMLDivElement | null>(null)
const incrementButtonRef = ref<HTMLDivElement | null>(null)
const decrementButtonRef = ref<HTMLDivElement | null>(null)
/**
* last element to capture the click or focus event
*/
const lastEventTarget = ref<EventTarget | null>(null)
const isFieldTargeted = (target: Event['target'] | null): boolean =>
isOrContainsEventTarget(inputRef.value, target) ||
isOrContainsEventTarget(decrementButtonRef.value, target) ||
isOrContainsEventTarget(incrementButtonRef.value, target)
const onClickOrFocusChange = (event: Event) => {
if (event.target === null || props.isDisabled) return
const wasFieldTargetedBefore = isFieldTargeted(lastEventTarget.value)
const isFieldTargetedNow = isFieldTargeted(event.target)
if (!isFieldTargetedNow && wasFieldTargetedBefore)
emit('blur', field.currentValue)
lastEventTarget.value = event.target
}
isOrContainsEventTarget(inputRef.value, target)
const onKeyup = (event: KeyboardEvent) => {
if (!isFieldTargeted(event.target)) return
Expand All @@ -282,28 +272,22 @@ export default defineComponent({
}
onBeforeMount(() => {
window.addEventListener('click', onClickOrFocusChange, true)
window.addEventListener('focus', onClickOrFocusChange, true)
window.addEventListener('keyup', onKeyup, true)
})
onUnmounted(() => {
window.removeEventListener('click', onClickOrFocusChange)
window.removeEventListener('focus', onClickOrFocusChange)
window.removeEventListener('keyup', onKeyup)
})
return {
decrementButtonClasses: computed(() => ({
'kt-field-number__button--is-disabled': !isDecrementEnabled.value,
})),
decrementButtonRef,
decrementValue,
field,
incrementButtonClasses: computed(() => ({
'kt-field-number__button--is-disabled': !isIncrementEnabled.value,
})),
incrementButtonRef,
incrementValue,
inputProps: computed(
(): InputHTMLAttributes &
Expand All @@ -326,6 +310,7 @@ export default defineComponent({
}),
),
inputRef,
ktFieldRef,
onBlur: () => {
forceUpdateDisplayedValue(field.currentValue)
},
Expand Down
25 changes: 18 additions & 7 deletions packages/kotti-ui/source/kotti-field-password/KtFieldPassword.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<template>
<KtField
v-bind="{ field }"
ref="ktFieldRef"
:getEmptyValue="() => null"
:helpTextSlot="$slots.helpText"
@visibilityChange="handleVisibilityChange"
>
<input v-bind="inputProps" @blur="onBlur" @input="onInput" />
<input v-bind="inputProps" @input="onInput" />
</KtField>
</template>

Expand All @@ -14,14 +15,17 @@ import { computed, defineComponent, ref } from 'vue'
import type { InputHTMLAttributes } from 'vue/types/jsx'
import { KtField } from '../kotti-field'
import { useField, useForceUpdate } from '../kotti-field/hooks'
import {
useEmitBlur,
useField,
useForceUpdate,
useKtFieldRef,
} from '../kotti-field/hooks'
import { makeProps } from '../make-props'
import { KOTTI_FIELD_PASSWORD_SUPPORTS } from './constants'
import { KottiFieldPassword } from './types'
const VALUE_PLACEHOLDER = '•••'
export default defineComponent({
name: 'KtFieldPassword',
components: { KtField },
Expand All @@ -36,6 +40,15 @@ export default defineComponent({
})
const fieldType = ref('password')
const { forceUpdate, forceUpdateKey } = useForceUpdate()
const ktFieldRef = useKtFieldRef()
useEmitBlur({
emit,
field,
fieldTarget: computed(() => ktFieldRef.value?.inputContainerRef ?? null),
valuePlaceholder: '•••',
})
return {
field,
handleVisibilityChange: () => {
Expand All @@ -57,9 +70,7 @@ export default defineComponent({
value: field.currentValue ?? '',
}),
),
onBlur: () => {
emit('blur', field.currentValue === null ? null : VALUE_PLACEHOLDER)
},
ktFieldRef,
onInput: (event: Event) => {
const newValue = (event.target as HTMLInputElement).value
field.setValue(newValue === '' ? null : newValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
</template>
</KtField>
</div>
<div :id="tippyContentId" ref="tippyContentRef">
<div ref="tippyContentRef">
<FieldSelectOptions
:actions="actions"
:dataTestPrefix="inputProps['data-test']"
Expand All @@ -75,25 +75,20 @@
</template>

<script lang="ts">
import type { Ref } from 'vue'
import {
computed,
defineComponent,
onBeforeMount,
onUnmounted,
ref,
watch,
} from 'vue'
import { computed, defineComponent, ref, watch } from 'vue'
import { z } from 'zod'
import { Yoco } from '@3yourmind/yoco'
import { KtField } from '../../kotti-field'
import { useField } from '../../kotti-field/hooks'
import { useForceUpdate } from '../../kotti-field/hooks'
import {
useEmitBlur,
useForceUpdate,
useKtFieldRef,
} from '../../kotti-field/hooks'
import { KtTag } from '../../kotti-tag'
import { makeProps } from '../../make-props'
import { isOrContainsEventTarget } from '../../utilities'
import { KOTTI_FIELD_SELECT_SUPPORTS } from '../constants'
import {
isTippyContentWrapper,
Expand Down Expand Up @@ -166,20 +161,14 @@ export default defineComponent({
const localQuery = ref<string | null>(null)
const { forceUpdateKey, forceUpdate } = useForceUpdate()
const ktFieldRef = useKtFieldRef()
/**
* fieldLabelRef is a template ref on KtField.vue
*/
const ktFieldRef = ref<{ inputContainerRef: Ref<HTMLDivElement> } | null>(
null,
)
const triggerTargets = computed(() =>
ktFieldRef.value ? [ktFieldRef.value.inputContainerRef] : [],
)
const { isDropdownOpen, isDropdownMounted, ...selectTippy } =
useSelectTippy(field, triggerTargets)
const tippyContentId = `TIPPY_CONTENT_${field.inputProps.id}`
const deleteQuery = () => {
if (props.isRemote) {
Expand All @@ -188,46 +177,28 @@ export default defineComponent({
}
/**
* last element to capture the click or focus event
* Tippy wraps the content inside additional div elements.
* So, we need to look for the actual content element.
*/
const lastEventTarget = ref<EventTarget | null>(null)
const isFieldTargeted = (target: Event['target'] | null): boolean =>
isOrContainsEventTarget(
ktFieldRef.value?.inputContainerRef ?? null,
target,
) || isOrContainsEventTarget(selectTippy.tippyContentRef.value, target)
const getEventTarget = (target: EventTarget | null): EventTarget | null => {
const findEventTarget = (
target: EventTarget | null,
): EventTarget | null => {
if (target === null || !(target instanceof HTMLElement)) return target
if (target.id === tippyContentId) return target
if (target === selectTippy.tippyContentRef.value) return target
return isTippyContentWrapper(target)
? getEventTarget((target.childNodes[0] ?? null) as EventTarget | null)
? findEventTarget((target.childNodes[0] ?? null) as EventTarget | null)
: target
}
const onClickOrFocusChange = (event: Event) => {
if (event.target === null || props.isDisabled) return
const target = getEventTarget(event.target)
const wasFieldTargetedBefore = isFieldTargeted(lastEventTarget.value)
const isFieldTargetedNow = isFieldTargeted(target)
if (!isFieldTargetedNow && wasFieldTargetedBefore)
emit('blur', field.currentValue)
lastEventTarget.value = target
}
onBeforeMount(() => {
window.addEventListener('click', onClickOrFocusChange, true)
window.addEventListener('focus', onClickOrFocusChange, true)
})
onUnmounted(() => {
window.removeEventListener('click', onClickOrFocusChange)
window.removeEventListener('focus', onClickOrFocusChange)
useEmitBlur({
emit,
field,
fieldTarget: computed(() => [
ktFieldRef.value?.inputContainerRef ?? null,
selectTippy.tippyContentRef.value,
]),
findEventTarget,
})
watch(
Expand Down Expand Up @@ -362,13 +333,13 @@ export default defineComponent({
field.setValue(
(field.currentValue as MultiValue).filter((v) => v !== value),
)
inputRef.value?.focus()
},
setIsDropdownOpen: selectTippy.setIsDropdownOpen,
showClearIcon: computed(
() => (showClear: boolean) =>
showClear && (isFieldHovered.value || isFieldFocused.value),
),
tippyContentId,
tippyContentRef: selectTippy.tippyContentRef,
tippyRef: selectTippy.tippyRef,
updateQuery: (event: Event) => {
Expand Down
Loading

0 comments on commit ac642e4

Please sign in to comment.