Skip to content

Commit b6c2780

Browse files
committed
fix: don't toggle boolean switch widget on Ctrl/Cmd+Enter
reka-ui's SwitchRoot activates on Enter from its own keydown handler, ignoring modifiers. A focused BOOLEAN widget switch therefore flipped every time Ctrl+Enter queued the workflow, silently reverting the value unless the user clicked away first. Veto the resulting model update when the Enter keydown carries Ctrl/Cmd, leaving the queue shortcut and every other switch interaction untouched.
1 parent cff52aa commit b6c2780

3 files changed

Lines changed: 84 additions & 2 deletions

File tree

src/components/ui/switch/Switch.test.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { render, screen } from '@testing-library/vue'
22
import userEvent from '@testing-library/user-event'
3-
import { describe, expect, it, vi } from 'vitest'
3+
import { describe, expect, it, onTestFinished, vi } from 'vitest'
44

55
import Switch from './Switch.vue'
66

@@ -72,4 +72,59 @@ describe('Switch', () => {
7272
expect(onUpdate).not.toHaveBeenCalled()
7373
expect(control).not.toBeChecked()
7474
})
75+
76+
it.for([
77+
['Ctrl', '{Control>}{Enter}{/Control}'],
78+
['Meta', '{Meta>}{Enter}{/Meta}']
79+
])('ignores Enter while %s is held', async ([, keystrokes]) => {
80+
const user = userEvent.setup()
81+
const onUpdate = vi.fn()
82+
83+
render(Switch, {
84+
props: {
85+
modelValue: false,
86+
'onUpdate:modelValue': onUpdate
87+
},
88+
attrs: { 'aria-label': 'Notifications' }
89+
})
90+
91+
const keysSeenByWindow: string[] = []
92+
const recordKeydown = (event: KeyboardEvent) =>
93+
keysSeenByWindow.push(event.key)
94+
window.addEventListener('keydown', recordKeydown)
95+
onTestFinished(() => {
96+
window.removeEventListener('keydown', recordKeydown)
97+
})
98+
99+
const control = screen.getByRole('switch', { name: 'Notifications' })
100+
await user.tab()
101+
expect(control).toHaveFocus()
102+
103+
await user.keyboard(keystrokes)
104+
expect(onUpdate).not.toHaveBeenCalled()
105+
expect(keysSeenByWindow).toContain('Enter')
106+
107+
await user.keyboard('[Enter]')
108+
expect(onUpdate).toHaveBeenCalledWith(true)
109+
})
110+
111+
it('stays interactive after a modified Enter is ignored', async () => {
112+
const user = userEvent.setup()
113+
const onUpdate = vi.fn()
114+
115+
render(Switch, {
116+
props: {
117+
modelValue: false,
118+
'onUpdate:modelValue': onUpdate
119+
},
120+
attrs: { 'aria-label': 'Notifications' }
121+
})
122+
123+
const control = screen.getByRole('switch', { name: 'Notifications' })
124+
await user.tab()
125+
await user.keyboard('{Control>}{Enter}{/Control}')
126+
await user.click(control)
127+
128+
expect(onUpdate).toHaveBeenCalledExactlyOnceWith(true)
129+
})
75130
})

src/components/ui/switch/Switch.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,19 @@ const {
1616
1717
const modelValue = defineModel<boolean>({ default: false })
1818
19+
// SwitchRoot toggles on Enter from its own keydown handler, so `.prevent` on a
20+
// fallthrough listener cannot stop it and `.stop` would swallow the global
21+
// Ctrl/Cmd+Enter shortcut. Veto the resulting update instead.
22+
let toggleRequestedByModifiedEnter = false
23+
24+
function trackModifiedEnter(event: KeyboardEvent) {
25+
if (event.key !== 'Enter' || !(event.ctrlKey || event.metaKey)) return
26+
toggleRequestedByModifiedEnter = true
27+
queueMicrotask(() => (toggleRequestedByModifiedEnter = false))
28+
}
29+
1930
function updateModelValue(value: boolean) {
20-
if (readonly) return
31+
if (readonly || toggleRequestedByModifiedEnter) return
2132
modelValue.value = value
2233
}
2334
</script>
@@ -33,6 +44,7 @@ function updateModelValue(value: boolean) {
3344
customClass
3445
)
3546
"
47+
@keydown="trackModifiedEnter"
3648
@update:model-value="updateModelValue"
3749
>
3850
<span

src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,21 @@ describe('WidgetToggleSwitch Value Binding', () => {
9595
expect(onModelUpdate).toHaveBeenCalledWith(false)
9696
})
9797

98+
it('keeps its value when Ctrl+Enter is pressed while focused', async () => {
99+
const widget = createToggleWidget(false)
100+
const onModelUpdate = vi.fn()
101+
const { user } = mountComponent(widget, false, onModelUpdate)
102+
103+
const control = screen.getByRole('switch')
104+
await user.tab()
105+
expect(control).toHaveFocus()
106+
107+
await user.keyboard('{Control>}{Enter}{/Control}')
108+
109+
expect(onModelUpdate).not.toHaveBeenCalled()
110+
expect(control).not.toBeChecked()
111+
})
112+
98113
it('handles value changes gracefully', async () => {
99114
const widget = createToggleWidget(false)
100115
const onModelUpdate = vi.fn()

0 commit comments

Comments
 (0)