Skip to content

Commit d40fa46

Browse files
committed
fix: stop buttons opening keyboard on Android
Remove e.preventDefault() from touchend in onTap and all handler sites. preventDefault() on touchend blocks the browser from synthesising mousedown, which prevents focus transfer from the terminal textarea to the button. On Android, the terminal retains focus and the keyboard re-shows on every button press. Safe to remove because: - touchFired guard already prevents double-execution - touch-action: manipulation in CSS handles double-tap zoom - preventDefault() on <button> outside forms is a no-op for click Adds regression test verifying touchend is not defaultPrevented.
1 parent 6b8ad12 commit d40fa46

8 files changed

Lines changed: 20 additions & 17 deletions

File tree

src/controls/combo-picker.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,14 +281,12 @@ export function createComboPicker(): ComboPickerResult {
281281
closeAndFocus()
282282
})
283283

284-
onTap(cancelButton, (event: Event) => {
285-
event.preventDefault()
284+
onTap(cancelButton, () => {
286285
haptic()
287286
closeAndFocus()
288287
})
289288

290-
onTap(sendButton, (event: Event) => {
291-
event.preventDefault()
289+
onTap(sendButton, () => {
292290
haptic()
293291
void submit()
294292
})

src/controls/floating-buttons.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ function createGroupButton(
2525
button.textContent = def.label
2626
button.setAttribute('aria-label', def.description)
2727

28-
onTap(button, (e: Event) => {
29-
e.preventDefault()
28+
onTap(button, () => {
3029
const kbWasOpen = isKeyboardOpen()
3130
haptic()
3231

src/controls/font-size.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,12 @@ export function createFontControls(term: XTerminal, font: FontConfig): FontContr
3131
container.appendChild(btnPlus)
3232
container.appendChild(btnHelp)
3333

34-
onTap(btnMinus, (e: Event) => {
35-
e.preventDefault()
34+
onTap(btnMinus, () => {
3635
haptic()
3736
changeFontSize(term, -2, font)
3837
})
3938

40-
onTap(btnPlus, (e: Event) => {
41-
e.preventDefault()
39+
onTap(btnPlus, () => {
4240
haptic()
4341
changeFontSize(term, 2, font)
4442
})

src/controls/help.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,7 @@ export function createHelpOverlay(
135135
}
136136
})
137137

138-
onTap(helpButton, (e: Event) => {
139-
e.preventDefault()
138+
onTap(helpButton, () => {
140139
haptic()
141140
open()
142141
})

src/drawer/drawer.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ export function createDrawer(
4646
for (const buttonDef of buttons) {
4747
const button = el('button')
4848
button.textContent = buttonDef.label
49-
onTap(button, (e: Event) => {
50-
e.preventDefault()
49+
onTap(button, () => {
5150
const kbWasOpen = isKeyboardOpen()
5251
haptic()
5352
close()

src/toolbar/toolbar.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ function wireButton(
6868
readonly focusIfNeeded: () => void
6969
}) => void,
7070
): void {
71-
onTap(button, (e: Event) => {
72-
e.preventDefault()
71+
onTap(button, () => {
7372
const kbWasOpen = isKeyboardOpen()
7473
haptic()
7574

src/util/tap.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ export function onTap(element: HTMLElement, handler: (e: Event) => void): void {
1010
let touchFired = false
1111

1212
element.addEventListener('touchend', (e: TouchEvent) => {
13-
e.preventDefault()
13+
// No preventDefault — allow the browser to synthesise mousedown/click,
14+
// which transfers focus to the button and away from the terminal textarea.
15+
// Without this, Android re-shows the keyboard when buttons are pressed.
16+
// The touchFired guard below prevents the handler from double-firing.
1417
touchFired = true
1518
handler(e)
1619
setTimeout(() => {

tests/tap.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ describe('onTap', () => {
2727
expect(handler).toHaveBeenCalledOnce()
2828
})
2929

30+
test('touchend does not call preventDefault', () => {
31+
const element = document.createElement('button')
32+
onTap(element, () => {})
33+
const event = new TouchEvent('touchend')
34+
element.dispatchEvent(event)
35+
expect(event.defaultPrevented).toBe(false)
36+
})
37+
3038
test('click fires again after guard timeout', () => {
3139
vi.useFakeTimers()
3240
const element = document.createElement('button')

0 commit comments

Comments
 (0)