From 295a1dbeeb93340899b29a8a72332f0920f376ed Mon Sep 17 00:00:00 2001 From: J-Sek Date: Fri, 17 Jan 2025 21:25:50 +0100 Subject: [PATCH 1/4] fix(VField, VInput): emit `click:*` events using keyboard --- .../vuetify/src/components/VField/VField.tsx | 24 ++++++++++++++----- .../vuetify/src/components/VInput/VInput.tsx | 12 +++++++++- packages/vuetify/src/util/events.ts | 9 +++++++ 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/vuetify/src/components/VField/VField.tsx b/packages/vuetify/src/components/VField/VField.tsx index b12b064782e..ffd1ce68010 100644 --- a/packages/vuetify/src/components/VField/VField.tsx +++ b/packages/vuetify/src/components/VField/VField.tsx @@ -30,6 +30,7 @@ import { pick, propsFactory, standardEasing, + triggerAsClick, useRender, } from '@/util' @@ -212,12 +213,15 @@ export const VField = genericComponent( } function onKeydownClear (e: KeyboardEvent) { - if (e.key !== 'Enter' && e.key !== ' ') return + triggerAsClick(e, props['onClick:clear']) + } - e.preventDefault() - e.stopPropagation() + function onKeydownAppendInner (e: KeyboardEvent) { + triggerAsClick(e, props['onClick:appendInner']) + } - props['onClick:clear']?.(new MouseEvent('click')) + function onKeydownPrependInner (e: KeyboardEvent) { + triggerAsClick(e, props['onClick:prependInner']) } useRender(() => { @@ -282,7 +286,11 @@ export const VField = genericComponent( { hasPrepend && (
{ props.prependInnerIcon && ( - + )} { slots['prepend-inner']?.(slotProps.value) } @@ -366,7 +374,11 @@ export const VField = genericComponent( { slots['append-inner']?.(slotProps.value) } { props.appendInnerIcon && ( - + )}
)} diff --git a/packages/vuetify/src/components/VInput/VInput.tsx b/packages/vuetify/src/components/VInput/VInput.tsx index 4234aeb0aad..e6aa079422d 100644 --- a/packages/vuetify/src/components/VInput/VInput.tsx +++ b/packages/vuetify/src/components/VInput/VInput.tsx @@ -16,7 +16,7 @@ import { makeValidationProps, useValidation } from '@/composables/validation' // Utilities import { computed } from 'vue' -import { EventProp, genericComponent, getUid, only, propsFactory, useRender } from '@/util' +import { EventProp, genericComponent, getUid, only, propsFactory, triggerAsClick, useRender } from '@/util' // Types import type { ComputedRef, PropType, Ref } from 'vue' @@ -147,6 +147,14 @@ export const VInput = genericComponent( } }) + function onKeydownAppend (e: KeyboardEvent) { + triggerAsClick(e, props['onClick:append']) + } + + function onKeydownPrepend (e: KeyboardEvent) { + triggerAsClick(e, props['onClick:prepend']) + } + useRender(() => { const hasPrepend = !!(slots.prepend || props.prependIcon) const hasAppend = !!(slots.append || props.appendIcon) @@ -184,6 +192,7 @@ export const VInput = genericComponent( )} @@ -201,6 +210,7 @@ export const VInput = genericComponent( )} diff --git a/packages/vuetify/src/util/events.ts b/packages/vuetify/src/util/events.ts index 9a9d1aa10ce..e9cc97249f6 100644 --- a/packages/vuetify/src/util/events.ts +++ b/packages/vuetify/src/util/events.ts @@ -15,3 +15,12 @@ export function getPrefixedEventHandlers ( return acc }, {} as Record<`${string}${T}`, EventHandler>) } + +export function triggerAsClick (e: KeyboardEvent, handler?: (evt: MouseEvent) => void) { + if (e.key !== 'Enter' && e.key !== ' ') return + + e.preventDefault() + e.stopPropagation() + + handler?.(new MouseEvent('click')) +} From 26ce6ece241fef2bb900a070054d92c4b2f248e4 Mon Sep 17 00:00:00 2001 From: Kael Date: Mon, 20 Jan 2025 19:24:00 +1100 Subject: [PATCH 2/4] use el.click() --- .../vuetify/src/components/VField/VField.tsx | 17 ----------------- .../vuetify/src/components/VInput/InputIcon.tsx | 4 ++++ .../vuetify/src/components/VInput/VInput.tsx | 12 +----------- packages/vuetify/src/util/events.ts | 5 ++--- 4 files changed, 7 insertions(+), 31 deletions(-) diff --git a/packages/vuetify/src/components/VField/VField.tsx b/packages/vuetify/src/components/VField/VField.tsx index ffd1ce68010..4288e46d4b0 100644 --- a/packages/vuetify/src/components/VField/VField.tsx +++ b/packages/vuetify/src/components/VField/VField.tsx @@ -30,7 +30,6 @@ import { pick, propsFactory, standardEasing, - triggerAsClick, useRender, } from '@/util' @@ -212,18 +211,6 @@ export const VField = genericComponent( } } - function onKeydownClear (e: KeyboardEvent) { - triggerAsClick(e, props['onClick:clear']) - } - - function onKeydownAppendInner (e: KeyboardEvent) { - triggerAsClick(e, props['onClick:appendInner']) - } - - function onKeydownPrependInner (e: KeyboardEvent) { - triggerAsClick(e, props['onClick:prependInner']) - } - useRender(() => { const isOutlined = props.variant === 'outlined' const hasPrepend = !!(slots['prepend-inner'] || props.prependInnerIcon) @@ -289,7 +276,6 @@ export const VField = genericComponent( )} @@ -350,7 +336,6 @@ export const VField = genericComponent( ? slots.clear({ ...slotProps.value, props: { - onKeydown: onKeydownClear, onFocus: focus, onBlur: blur, onClick: props['onClick:clear'], @@ -359,7 +344,6 @@ export const VField = genericComponent( : ( @@ -377,7 +361,6 @@ export const VField = genericComponent( )} diff --git a/packages/vuetify/src/components/VInput/InputIcon.tsx b/packages/vuetify/src/components/VInput/InputIcon.tsx index de317956e76..f4eed838a99 100644 --- a/packages/vuetify/src/components/VInput/InputIcon.tsx +++ b/packages/vuetify/src/components/VInput/InputIcon.tsx @@ -4,6 +4,9 @@ import { VIcon } from '@/components/VIcon' // Composables import { useLocale } from '@/composables/locale' +// Utilities +import { triggerAsClick } from '@/util' + // Types import type { IconValue } from '@/composables/icons' @@ -41,6 +44,7 @@ export function useInputIcon> (prop icon={ props[`${name}Icon`] } aria-label={ label } onClick={ listener } + onKeydown={ triggerAsClick } /> ) } diff --git a/packages/vuetify/src/components/VInput/VInput.tsx b/packages/vuetify/src/components/VInput/VInput.tsx index e6aa079422d..4234aeb0aad 100644 --- a/packages/vuetify/src/components/VInput/VInput.tsx +++ b/packages/vuetify/src/components/VInput/VInput.tsx @@ -16,7 +16,7 @@ import { makeValidationProps, useValidation } from '@/composables/validation' // Utilities import { computed } from 'vue' -import { EventProp, genericComponent, getUid, only, propsFactory, triggerAsClick, useRender } from '@/util' +import { EventProp, genericComponent, getUid, only, propsFactory, useRender } from '@/util' // Types import type { ComputedRef, PropType, Ref } from 'vue' @@ -147,14 +147,6 @@ export const VInput = genericComponent( } }) - function onKeydownAppend (e: KeyboardEvent) { - triggerAsClick(e, props['onClick:append']) - } - - function onKeydownPrepend (e: KeyboardEvent) { - triggerAsClick(e, props['onClick:prepend']) - } - useRender(() => { const hasPrepend = !!(slots.prepend || props.prependIcon) const hasAppend = !!(slots.append || props.appendIcon) @@ -192,7 +184,6 @@ export const VInput = genericComponent( )} @@ -210,7 +201,6 @@ export const VInput = genericComponent( )} diff --git a/packages/vuetify/src/util/events.ts b/packages/vuetify/src/util/events.ts index e9cc97249f6..aaef98c7a08 100644 --- a/packages/vuetify/src/util/events.ts +++ b/packages/vuetify/src/util/events.ts @@ -16,11 +16,10 @@ export function getPrefixedEventHandlers ( }, {} as Record<`${string}${T}`, EventHandler>) } -export function triggerAsClick (e: KeyboardEvent, handler?: (evt: MouseEvent) => void) { +export function triggerAsClick (e: KeyboardEvent) { if (e.key !== 'Enter' && e.key !== ' ') return e.preventDefault() e.stopPropagation() - - handler?.(new MouseEvent('click')) + ;(e.currentTarget as HTMLElement).click() } From fe766c22f03991c0765426cbcc589dac8282c935 Mon Sep 17 00:00:00 2001 From: J-Sek Date: Mon, 20 Jan 2025 12:34:06 +0100 Subject: [PATCH 3/4] keep focus on the icon --- .../vuetify/src/components/VInput/InputIcon.tsx | 14 ++++++++++---- packages/vuetify/src/util/events.ts | 8 -------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/vuetify/src/components/VInput/InputIcon.tsx b/packages/vuetify/src/components/VInput/InputIcon.tsx index f4eed838a99..454a723e9a9 100644 --- a/packages/vuetify/src/components/VInput/InputIcon.tsx +++ b/packages/vuetify/src/components/VInput/InputIcon.tsx @@ -4,9 +4,6 @@ import { VIcon } from '@/components/VIcon' // Composables import { useLocale } from '@/composables/locale' -// Utilities -import { triggerAsClick } from '@/util' - // Types import type { IconValue } from '@/composables/icons' @@ -35,6 +32,15 @@ export function useInputIcon> (prop clear: 'clear', }[name] const listener = props[`onClick:${name}`] + + function onKeydown (e: KeyboardEvent) { + if (e.key !== 'Enter' && e.key !== ' ') return + + e.preventDefault() + e.stopPropagation() + ;(listener as Function)?.(new MouseEvent('click')) + } + const label = listener && localeKey ? t(`$vuetify.input.${localeKey}`, props.label ?? '') : undefined @@ -44,7 +50,7 @@ export function useInputIcon> (prop icon={ props[`${name}Icon`] } aria-label={ label } onClick={ listener } - onKeydown={ triggerAsClick } + onKeydown={ onKeydown } /> ) } diff --git a/packages/vuetify/src/util/events.ts b/packages/vuetify/src/util/events.ts index aaef98c7a08..9a9d1aa10ce 100644 --- a/packages/vuetify/src/util/events.ts +++ b/packages/vuetify/src/util/events.ts @@ -15,11 +15,3 @@ export function getPrefixedEventHandlers ( return acc }, {} as Record<`${string}${T}`, EventHandler>) } - -export function triggerAsClick (e: KeyboardEvent) { - if (e.key !== 'Enter' && e.key !== ' ') return - - e.preventDefault() - e.stopPropagation() - ;(e.currentTarget as HTMLElement).click() -} From 3690cf603df946f2a3ed9fafee9b5d833b27e48e Mon Sep 17 00:00:00 2001 From: Kael Date: Tue, 21 Jan 2025 15:38:01 +1100 Subject: [PATCH 4/4] fix: handle array events, emit PointerEvent --- packages/vuetify/src/components/VInput/InputIcon.tsx | 9 ++++++--- packages/vuetify/src/util/helpers.ts | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/vuetify/src/components/VInput/InputIcon.tsx b/packages/vuetify/src/components/VInput/InputIcon.tsx index 454a723e9a9..0ae9c6e87e5 100644 --- a/packages/vuetify/src/components/VInput/InputIcon.tsx +++ b/packages/vuetify/src/components/VInput/InputIcon.tsx @@ -4,12 +4,15 @@ import { VIcon } from '@/components/VIcon' // Composables import { useLocale } from '@/composables/locale' +// Utilities +import { callEvent } from '@/util' + // Types import type { IconValue } from '@/composables/icons' +import type { EventProp } from '@/util' type names = 'clear' | 'prepend' | 'append' | 'appendInner' | 'prependInner' -type EventProp any> = T | T[] type InputIconProps = { label: string | undefined } & { @@ -31,14 +34,14 @@ export function useInputIcon> (prop appendInner: 'appendAction', clear: 'clear', }[name] - const listener = props[`onClick:${name}`] + const listener = props[`onClick:${name}`] as EventProp | undefined function onKeydown (e: KeyboardEvent) { if (e.key !== 'Enter' && e.key !== ' ') return e.preventDefault() e.stopPropagation() - ;(listener as Function)?.(new MouseEvent('click')) + callEvent(listener, new PointerEvent('click', e)) } const label = listener && localeKey diff --git a/packages/vuetify/src/util/helpers.ts b/packages/vuetify/src/util/helpers.ts index bd8eddea62e..01e719fde54 100644 --- a/packages/vuetify/src/util/helpers.ts +++ b/packages/vuetify/src/util/helpers.ts @@ -616,6 +616,7 @@ export function eventName (propName: string) { return propName[2].toLowerCase() + propName.slice(3) } +// TODO: this should be an array but vue's types don't accept arrays: vuejs/core#8025 export type EventProp void> = F export const EventProp = () => [Function, Array] as PropType> @@ -624,7 +625,7 @@ export function hasEvent (props: Record, name: string) { return !!(props[name] || props[`${name}Once`] || props[`${name}Capture`] || props[`${name}OnceCapture`] || props[`${name}CaptureOnce`]) } -export function callEvent (handler: EventProp | undefined, ...args: T) { +export function callEvent (handler: EventProp | EventProp[] | undefined, ...args: T) { if (Array.isArray(handler)) { for (const h of handler) { h(...args)