Skip to content

Commit 30bce20

Browse files
zernoniaclaudeyan-ad
authored
fix: only consume Enter/Space keydown when no modifier key is held (#2762)
* fix: only consume Enter/Space keydown when no modifier key is held CellTrigger variants (Calendar, RangeCalendar, MonthPicker, YearPicker, MonthRangePicker, YearRangePicker) and ListboxRoot called preventDefault + stopPropagation on every keydown they were bound to, blocking parents from listening for combos like Ctrl+Enter (#2400). These keys are only "handled" by the component when no modifier is held: plain Enter/Space selects, but Ctrl/Cmd/Alt+Enter is meant as a parent shortcut. Guard the handlers so modifier combos bubble untouched (no preventDefault, no stopPropagation, no selection), while plain Enter/Space stay fully consumed — preventing form submission and page scroll. The redundant `@keydown.enter.prevent` template modifiers are dropped now that preventDefault lives in the handler. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(Calendar): clean up document listener in finally block Ensures the keydown listener is always removed even if an awaited keyboard step throws, preventing leaks into later tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Yanuar <yan-ad@users.noreply.github.com>
1 parent 8567787 commit 30bce20

8 files changed

Lines changed: 65 additions & 5 deletions

File tree

packages/core/src/Calendar/Calendar.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,42 @@ describe('calendar', async () => {
518518
expect(heading).toHaveTextContent('December 1979')
519519
})
520520

521+
it('stops propagation for plain Enter but lets Ctrl+Enter bubble to parent listeners', async () => {
522+
const { getByTestId, user } = setup({
523+
calendarProps: {
524+
modelValue: calendarDate,
525+
},
526+
})
527+
528+
const cell = getByTestId('date-1-20')
529+
cell.focus()
530+
expect(cell).toHaveFocus()
531+
532+
let plainEnterBubbled = false
533+
let ctrlEnterBubbled = false
534+
const handler = (e: KeyboardEvent) => {
535+
if (e.key === 'Enter') {
536+
if (e.ctrlKey)
537+
ctrlEnterBubbled = true
538+
else
539+
plainEnterBubbled = true
540+
}
541+
}
542+
document.addEventListener('keydown', handler)
543+
try {
544+
await user.keyboard(kbd.ENTER)
545+
await user.keyboard(`{Control>}${kbd.ENTER}{/Control}`)
546+
}
547+
finally {
548+
document.removeEventListener('keydown', handler)
549+
}
550+
551+
// plain Enter is handled by the cell (selection) → propagation stopped
552+
expect(plainEnterBubbled).toBe(false)
553+
// Ctrl+Enter is not handled by the cell → bubbles so parents can react
554+
expect(ctrlEnterBubbled).toBe(true)
555+
})
556+
521557
it('handles unavailable dates appropriately', async () => {
522558
const { getByTestId, user } = setup({
523559
calendarProps: {

packages/core/src/Calendar/CalendarCellTrigger.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ function handleClick() {
111111
function handleArrowKey(e: KeyboardEvent) {
112112
if (isDisabled.value)
113113
return
114+
// Modifier combos on Enter/Space (e.g. Ctrl+Enter) are not handled by the cell —
115+
// let them bubble so parent listeners can react (e.g. submit a form).
116+
if ((e.code === kbd.ENTER || e.code === kbd.SPACE_CODE) && (e.ctrlKey || e.metaKey || e.altKey))
117+
return
114118
e.preventDefault()
115119
e.stopPropagation()
116120
const parentElement = rootContext.parentElement.value!
@@ -188,7 +192,6 @@ function handleArrowKey(e: KeyboardEvent) {
188192
:tabindex="isFocusedDate ? 0 : isOutsideView || isDisabled ? undefined : -1"
189193
@click="handleClick"
190194
@keydown.up.down.left.right.space.enter="handleArrowKey"
191-
@keydown.enter.prevent
192195
>
193196
<slot
194197
:day-value="dayValue"

packages/core/src/Listbox/ListboxRoot.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ function highlightItem(value: T) {
188188
189189
function onKeydownEnter(event: KeyboardEvent) {
190190
if (highlightedElement.value && highlightedElement.value.isConnected) {
191+
// Modifier combos (e.g. Ctrl+Enter) are not handled here —
192+
// let them bubble so parent listeners can react (e.g. submit a form).
193+
if (event.ctrlKey || event.metaKey || event.altKey)
194+
return
195+
191196
event.preventDefault()
192197
event.stopPropagation()
193198

packages/core/src/MonthPicker/MonthPickerCellTrigger.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ function handleClick() {
8686
function handleArrowKey(e: KeyboardEvent) {
8787
if (isDisabled.value)
8888
return
89+
// Modifier combos on Enter/Space (e.g. Ctrl+Enter) are not handled by the cell —
90+
// let them bubble so parent listeners can react (e.g. submit a form).
91+
if ((e.code === kbd.ENTER || e.code === kbd.SPACE_CODE) && (e.ctrlKey || e.metaKey || e.altKey))
92+
return
8993
e.preventDefault()
9094
e.stopPropagation()
9195
const parentElement = rootContext.parentElement.value!
@@ -199,7 +203,6 @@ function handleArrowKey(e: KeyboardEvent) {
199203
:tabindex="isFocusedMonth ? 0 : isDisabled ? undefined : -1"
200204
@click="handleClick"
201205
@keydown.up.down.left.right.space.enter.page-up.page-down="handleArrowKey"
202-
@keydown.enter.prevent
203206
>
204207
<slot
205208
:month-value="shortMonthValue"

packages/core/src/MonthRangePicker/MonthRangePickerCellTrigger.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ function handleFocus() {
165165
function handleArrowKey(e: KeyboardEvent) {
166166
if (isDisabled.value)
167167
return
168+
// Modifier combos on Enter/Space (e.g. Ctrl+Enter) are not handled by the cell —
169+
// let them bubble so parent listeners can react (e.g. submit a form).
170+
if ((e.code === kbd.ENTER || e.code === kbd.SPACE_CODE) && (e.ctrlKey || e.metaKey || e.altKey))
171+
return
168172
e.preventDefault()
169173
e.stopPropagation()
170174
const parentElement = rootContext.parentElement.value!
@@ -289,7 +293,6 @@ function handleArrowKey(e: KeyboardEvent) {
289293
@focusin="handleFocus"
290294
@mouseenter="handleFocus"
291295
@keydown.up.down.left.right.space.enter.page-up.page-down="handleArrowKey"
292-
@keydown.enter.prevent
293296
>
294297
<slot
295298
:month-value="shortMonthValue"

packages/core/src/RangeCalendar/RangeCalendarCellTrigger.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ function handleFocus() {
187187
function handleArrowKey(e: KeyboardEvent) {
188188
if (isDisabled.value)
189189
return
190+
// Modifier combos on Enter/Space (e.g. Ctrl+Enter) are not handled by the cell —
191+
// let them bubble so parent listeners can react (e.g. submit a form).
192+
if ((e.code === kbd.ENTER || e.code === kbd.SPACE_CODE) && (e.ctrlKey || e.metaKey || e.altKey))
193+
return
190194
e.preventDefault()
191195
e.stopPropagation()
192196
const parentElement = rootContext.parentElement.value!

packages/core/src/YearPicker/YearPickerCellTrigger.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ function handleClick() {
8585
function handleArrowKey(e: KeyboardEvent) {
8686
if (isDisabled.value)
8787
return
88+
// Modifier combos on Enter/Space (e.g. Ctrl+Enter) are not handled by the cell —
89+
// let them bubble so parent listeners can react (e.g. submit a form).
90+
if ((e.code === kbd.ENTER || e.code === kbd.SPACE_CODE) && (e.ctrlKey || e.metaKey || e.altKey))
91+
return
8892
e.preventDefault()
8993
e.stopPropagation()
9094
const parentElement = rootContext.parentElement.value!
@@ -203,7 +207,6 @@ function handleArrowKey(e: KeyboardEvent) {
203207
:tabindex="isFocusedYear ? 0 : isDisabled ? undefined : -1"
204208
@click="handleClick"
205209
@keydown.up.down.left.right.space.enter.page-up.page-down="handleArrowKey"
206-
@keydown.enter.prevent
207210
>
208211
<slot
209212
:year-value="yearValue"

packages/core/src/YearRangePicker/YearRangePickerCellTrigger.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ function handleFocus() {
165165
function handleArrowKey(e: KeyboardEvent) {
166166
if (isDisabled.value)
167167
return
168+
// Modifier combos on Enter/Space (e.g. Ctrl+Enter) are not handled by the cell —
169+
// let them bubble so parent listeners can react (e.g. submit a form).
170+
if ((e.code === kbd.ENTER || e.code === kbd.SPACE_CODE) && (e.ctrlKey || e.metaKey || e.altKey))
171+
return
168172
e.preventDefault()
169173
e.stopPropagation()
170174
const parentElement = rootContext.parentElement.value!
@@ -291,7 +295,6 @@ function handleArrowKey(e: KeyboardEvent) {
291295
@focusin="handleFocus"
292296
@mouseenter="handleFocus"
293297
@keydown.up.down.left.right.space.enter.page-up.page-down="handleArrowKey"
294-
@keydown.enter.prevent
295298
>
296299
<slot
297300
:year-value="yearValue"

0 commit comments

Comments
 (0)