Skip to content

Commit f9288ad

Browse files
J-Sekjohnleider
andauthored
fix(VAutocomplete, VCombobox): space key should not select (#21311)
Co-authored-by: John Leider <[email protected]>
1 parent c3bd6cd commit f9288ad

File tree

8 files changed

+56
-18
lines changed

8 files changed

+56
-18
lines changed

packages/api-generator/src/locale/en/VList.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"itemType": "Designates the key on the supplied items that is used for determining the nodes type.",
44
"activatable": "Designates whether the list items are activatable.",
55
"disabled": "Puts all children inputs into a disabled state.",
6+
"filterable": "**FOR INTERNAL USE ONLY** Prevents list item selection using [space] key and pass it back to the text input. Used internally for VAutocomplete and VCombobox.",
67
"inactive": "If set, the list tile will not be rendered as a link even if it has to/href prop or @click handler.",
78
"lines": "Designates a **minimum-height** for all children `v-list-item` components. This prop uses [line-clamp](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp) and is not supported in all browsers.",
89
"link": "Applies `v-list-item` hover styles. Useful when using the item is an _activator_.",

packages/vuetify/src/components/VAutocomplete/VAutocomplete.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export const VAutocomplete = genericComponent<new <
207207
menu.value = !menu.value
208208
}
209209
function onListKeydown (e: KeyboardEvent) {
210-
if (e.key !== ' ' && checkPrintable(e)) {
210+
if (checkPrintable(e) || e.key === 'Backspace') {
211211
vTextFieldRef.value?.focus()
212212
}
213213
}
@@ -467,6 +467,7 @@ export const VAutocomplete = genericComponent<new <
467467
{ hasList && (
468468
<VList
469469
ref={ listRef }
470+
filterable
470471
selected={ selectedValues.value }
471472
selectStrategy={ props.multiple ? 'independent' : 'single-independent' }
472473
onMousedown={ (e: MouseEvent) => e.preventDefault() }

packages/vuetify/src/components/VCombobox/VCombobox.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ export const VCombobox = genericComponent<new <
261261
menu.value = !menu.value
262262
}
263263
function onListKeydown (e: KeyboardEvent) {
264-
if (e.key !== ' ' && checkPrintable(e)) {
264+
if (checkPrintable(e) || e.key === 'Backspace') {
265265
vTextFieldRef.value?.focus()
266266
}
267267
}
@@ -513,6 +513,7 @@ export const VCombobox = genericComponent<new <
513513
{ hasList && (
514514
<VList
515515
ref={ listRef }
516+
filterable
516517
selected={ selectedValues.value }
517518
selectStrategy={ props.multiple ? 'independent' : 'single-independent' }
518519
onMousedown={ (e: MouseEvent) => e.preventDefault() }

packages/vuetify/src/components/VList/VList.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export const makeVListProps = propsFactory({
9292
activeClass: String,
9393
bgColor: String,
9494
disabled: Boolean,
95+
filterable: Boolean,
9596
expandIcon: IconValue,
9697
collapseIcon: IconValue,
9798
lines: {
@@ -174,7 +175,9 @@ export const VList = genericComponent<new <
174175
const baseColor = toRef(() => props.baseColor)
175176
const color = toRef(() => props.color)
176177

177-
createList()
178+
createList({
179+
filterable: props.filterable,
180+
})
178181

179182
provideDefaults({
180183
VListGroup: {

packages/vuetify/src/components/VList/VListItem.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import vRipple from '@/directives/ripple'
2828

2929
// Utilities
3030
import { computed, onBeforeMount, toDisplayString, toRef, watch } from 'vue'
31-
import { deprecate, EventProp, genericComponent, propsFactory, useRender } from '@/util'
31+
import { deprecate, EventProp, genericComponent, keyCodes, propsFactory, useRender } from '@/util'
3232

3333
// Types
3434
import type { PropType } from 'vue'
@@ -179,6 +179,15 @@ export const VListItem = genericComponent<VListItemSlots>()({
179179
const { elevationClasses } = useElevation(props)
180180
const { roundedClasses } = useRounded(roundedProps)
181181
const lineClasses = toRef(() => props.lines ? `v-list-item--${props.lines}-line` : undefined)
182+
const rippleOptions = toRef(() =>
183+
(
184+
props.ripple !== undefined &&
185+
!!props.ripple &&
186+
list?.filterable
187+
)
188+
? { keys: [keyCodes.enter] }
189+
: props.ripple
190+
)
182191

183192
const slotProps = computed(() => ({
184193
isActive: isActive.value,
@@ -212,8 +221,9 @@ export const VListItem = genericComponent<VListItemSlots>()({
212221

213222
if (['INPUT', 'TEXTAREA'].includes(target.tagName)) return
214223

215-
if (e.key === 'Enter' || e.key === ' ') {
224+
if (e.key === 'Enter' || (e.key === ' ' && !list?.filterable)) {
216225
e.preventDefault()
226+
e.stopPropagation()
217227
e.target!.dispatchEvent(new MouseEvent('click', e))
218228
}
219229
}
@@ -271,7 +281,7 @@ export const VListItem = genericComponent<VListItemSlots>()({
271281
}
272282
onClick={ onClick }
273283
onKeydown={ isClickable.value && !isLink.value && onKeyDown }
274-
v-ripple={ isClickable.value && props.ripple }
284+
v-ripple={ isClickable.value && rippleOptions.value }
275285
{ ...link.linkProps }
276286
>
277287
{ genOverlays(isClickable.value || isActive.value, 'v-list-item') }

packages/vuetify/src/components/VList/list.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { computed, inject, provide, shallowRef } from 'vue'
33

44
// Types
5-
import type { InjectionKey, Ref } from 'vue'
5+
import type { InjectionKey, MaybeRefOrGetter, Ref } from 'vue'
66

77
// Depth
88
export const DepthKey: InjectionKey<Ref<number>> = Symbol.for('vuetify:depth')
@@ -19,14 +19,24 @@ export function useDepth (hasPrepend?: Ref<boolean>) {
1919

2020
// List
2121
export const ListKey: InjectionKey<{
22+
filterable: MaybeRefOrGetter<boolean>
2223
hasPrepend: Ref<boolean>
2324
updateHasPrepend: (value: boolean) => void
2425
}> = Symbol.for('vuetify:list')
2526

26-
export function createList () {
27-
const parent = inject(ListKey, { hasPrepend: shallowRef(false), updateHasPrepend: () => null })
27+
type InjectedListOptions = {
28+
filterable: MaybeRefOrGetter<boolean>
29+
}
30+
31+
export function createList ({ filterable }: InjectedListOptions = { filterable: false }) {
32+
const parent = inject(ListKey, {
33+
filterable: false,
34+
hasPrepend: shallowRef(false),
35+
updateHasPrepend: () => null,
36+
})
2837

2938
const data = {
39+
filterable: parent.filterable || filterable,
3040
hasPrepend: shallowRef(false),
3141
updateHasPrepend: (value: boolean) => {
3242
if (value) data.hasPrepend.value = value

packages/vuetify/src/directives/ripple/index.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ interface RippleOptions {
2525
}
2626

2727
export interface RippleDirectiveBinding extends Omit<DirectiveBinding, 'modifiers' | 'value'> {
28-
value?: boolean | { class: string }
28+
value?: boolean | {
29+
class?: string
30+
keys?: number[]
31+
}
2932
modifiers: {
3033
center?: boolean
3134
circle?: boolean
@@ -157,7 +160,7 @@ const ripples = {
157160
},
158161
}
159162

160-
function isRippleEnabled (value: any): value is true {
163+
function isRippleEnabled (value: any) {
161164
return typeof value === 'undefined' || !!value
162165
}
163166

@@ -249,8 +252,8 @@ function rippleCancelShow (e: MouseEvent | TouchEvent) {
249252

250253
let keyboardRipple = false
251254

252-
function keyboardRippleShow (e: KeyboardEvent) {
253-
if (!keyboardRipple && (e.keyCode === keyCodes.enter || e.keyCode === keyCodes.space)) {
255+
function keyboardRippleShow (e: KeyboardEvent, keys: number[]) {
256+
if (!keyboardRipple && keys.includes(e.keyCode)) {
254257
keyboardRipple = true
255258
rippleShow(e)
256259
}
@@ -270,6 +273,7 @@ function focusRippleHide (e: FocusEvent) {
270273

271274
function updateRipple (el: HTMLElement, binding: RippleDirectiveBinding, wasEnabled: boolean) {
272275
const { value, modifiers } = binding
276+
273277
const enabled = isRippleEnabled(value)
274278
if (!enabled) {
275279
ripples.hide(el)
@@ -279,10 +283,15 @@ function updateRipple (el: HTMLElement, binding: RippleDirectiveBinding, wasEnab
279283
el._ripple.enabled = enabled
280284
el._ripple.centered = modifiers.center
281285
el._ripple.circle = modifiers.circle
282-
if (isObject(value) && value.class) {
283-
el._ripple.class = value.class
286+
287+
const bindingValue = isObject(value) ? value : {}
288+
if (bindingValue.class) {
289+
el._ripple.class = bindingValue.class
284290
}
285291

292+
const allowedKeys = bindingValue.keys ?? [keyCodes.enter, keyCodes.space]
293+
el._ripple.keyDownHandler = (e: KeyboardEvent) => keyboardRippleShow(e, allowedKeys)
294+
286295
if (enabled && !wasEnabled) {
287296
if (modifiers.stop) {
288297
el.addEventListener('touchstart', rippleStop, { passive: true })
@@ -299,7 +308,7 @@ function updateRipple (el: HTMLElement, binding: RippleDirectiveBinding, wasEnab
299308
el.addEventListener('mouseup', rippleHide)
300309
el.addEventListener('mouseleave', rippleHide)
301310

302-
el.addEventListener('keydown', keyboardRippleShow)
311+
el.addEventListener('keydown', e => keyboardRippleShow(e, allowedKeys))
303312
el.addEventListener('keyup', keyboardRippleHide)
304313

305314
el.addEventListener('blur', focusRippleHide)
@@ -319,7 +328,9 @@ function removeListeners (el: HTMLElement) {
319328
el.removeEventListener('touchcancel', rippleHide)
320329
el.removeEventListener('mouseup', rippleHide)
321330
el.removeEventListener('mouseleave', rippleHide)
322-
el.removeEventListener('keydown', keyboardRippleShow)
331+
if (el._ripple?.keyDownHandler) {
332+
el.removeEventListener('keydown', el._ripple.keyDownHandler)
333+
}
323334
el.removeEventListener('keyup', keyboardRippleHide)
324335
el.removeEventListener('dragstart', rippleHide)
325336
el.removeEventListener('blur', focusRippleHide)
@@ -330,8 +341,8 @@ function mounted (el: HTMLElement, binding: RippleDirectiveBinding) {
330341
}
331342

332343
function unmounted (el: HTMLElement) {
333-
delete el._ripple
334344
removeListeners(el)
345+
delete el._ripple
335346
}
336347

337348
function updated (el: HTMLElement, binding: RippleDirectiveBinding) {

packages/vuetify/src/globals.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ declare global {
2424
isTouch?: boolean
2525
showTimer?: number
2626
showTimerCommit?: (() => void) | null
27+
keyDownHandler?: ((e: KeyboardEvent) => void) | null
2728
}
2829
_observe?: Record<number, {
2930
init: boolean

0 commit comments

Comments
 (0)