Skip to content

Commit efaaa5d

Browse files
J-SekKaelWD
andauthored
fix(VTextField): avoid infinite focus loop (#21628)
fixes #21626 Co-authored-by: Kael <[email protected]>
1 parent 63da3a1 commit efaaa5d

File tree

3 files changed

+71
-3
lines changed

3 files changed

+71
-3
lines changed

packages/vuetify/src/components/VTextField/VTextField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export const VTextField = genericComponent<VTextFieldSlots>()({
105105

106106
nextTick(() => {
107107
if (inputRef.value !== document.activeElement) {
108-
inputRef.value?.focus()
108+
nextTick(() => inputRef.value?.focus())
109109
}
110110
})
111111
}

packages/vuetify/src/components/VTextField/__tests__/VTextField.spec.browser.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
// Components
12
import { VTextField } from '../VTextField'
3+
import { VBtn } from '@/components/VBtn'
4+
import { VMenu } from '@/components/VMenu'
25

36
// Utilities
4-
import { generate, render, userEvent } from '@test'
7+
import { commands, generate, render, screen, userEvent, wait } from '@test'
58
import { cloneVNode } from 'vue'
69

710
const variants = ['underlined', 'outlined', 'filled', 'solo', 'plain'] as const
@@ -60,6 +63,46 @@ describe('VTextField', () => {
6063
expect(element).toHaveTextContent('Error!')
6164
})
6265

66+
it('does not trigger infinite loop when autofilled by password manager', async () => {
67+
render(() => (
68+
<div>
69+
<VTextField label="username" name="username" type="email" />
70+
<VTextField label="password" name="password" type="password" />
71+
<VBtn>
72+
Some button
73+
<VMenu activator="parent">
74+
<div class="my-menu-content">Some text in menu</div>
75+
</VMenu>
76+
</VBtn>
77+
</div>
78+
))
79+
80+
const input1 = screen.getByCSS('input[name="username"]') as HTMLInputElement
81+
const input2 = screen.getByCSS('input[name="password"]') as HTMLInputElement
82+
83+
await commands.abortAfter(5000, 'VTextField infinite loop detection')
84+
85+
input1.focus()
86+
input1.value = 'my username'
87+
input1.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste' }))
88+
input1.dispatchEvent(new Event('change', { bubbles: true }))
89+
90+
input2.focus()
91+
input2.value = 'my password'
92+
input2.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste' }))
93+
input2.dispatchEvent(new Event('change', { bubbles: true }))
94+
95+
await wait(100)
96+
const button = screen.getByCSS('.v-btn')
97+
await userEvent.click(button)
98+
await wait(100)
99+
100+
const menuContent = screen.getByCSS('.my-menu-content')
101+
expect(menuContent).toBeVisible()
102+
103+
await commands.clearAbortTimeout()
104+
})
105+
63106
it('handles multiple options in validate-on prop', async () => {
64107
const rule = vi.fn(v => v?.length > 5 || 'Error!')
65108

packages/vuetify/test/setup/browser-commands.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,32 @@ async function setFocusEmulationEnabled (ctx: BrowserCommandContext) {
6868
return ctx.browser.sendCommand('Emulation.setFocusEmulationEnabled', { enabled: true })
6969
}
7070

71-
export const commands = { drag, scroll, isDisplayed, percySnapshot, waitStable, setFocusEmulationEnabled }
71+
let abortTimeout: ReturnType<typeof setTimeout>
72+
function abortAfter (ctx: BrowserCommandContext, delay: number, name: string) {
73+
abortTimeout = setTimeout(async () => {
74+
// eslint-disable-next-line no-console
75+
console.error(`[Error] Test timeout: Aborting after ${delay}ms for ${name} in ${ctx.testPath}`)
76+
// eslint-disable-next-line no-console
77+
console.error('[Warning] "chrome" process might still be running and require manual shutdown.')
78+
process.exitCode = 1
79+
await ctx.project.vitest.exit(true)
80+
}, delay)
81+
}
82+
83+
function clearAbortTimeout (ctx: BrowserCommandContext) {
84+
clearTimeout(abortTimeout)
85+
}
86+
87+
export const commands = {
88+
drag,
89+
scroll,
90+
isDisplayed,
91+
percySnapshot,
92+
waitStable,
93+
setFocusEmulationEnabled,
94+
abortAfter,
95+
clearAbortTimeout,
96+
}
7297

7398
export type CustomCommands = {
7499
[k in keyof typeof commands]: typeof commands[k] extends (ctx: any, ...args: infer A) => any

0 commit comments

Comments
 (0)