diff --git a/packages/@headlessui-react/src/components/switch/switch.test.tsx b/packages/@headlessui-react/src/components/switch/switch.test.tsx index 06ee361..bd1b052 100644 --- a/packages/@headlessui-react/src/components/switch/switch.test.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.test.tsx @@ -156,6 +156,24 @@ describe('Keyboard interactions', () => { }) }) + describe('`Enter` key', () => { + it('should not be possible to use Enter to toggle the Switch', async () => { + const handleChange = jest.fn() + render() + + // Ensure checkbox is off + assertSwitch({ state: SwitchState.Off }) + + // Focus the switch + getSwitch()?.focus() + + // Try to toggle + await press(Keys.Enter) + + expect(handleChange).not.toHaveBeenCalled() + }) + }) + describe('`Tab` key', () => { it('should be possible to tab away from the Switch', async () => { render( diff --git a/packages/@headlessui-react/src/components/switch/switch.tsx b/packages/@headlessui-react/src/components/switch/switch.tsx index 949fecc..4dfcb6a 100644 --- a/packages/@headlessui-react/src/components/switch/switch.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.tsx @@ -55,7 +55,14 @@ const DEFAULT_SWITCH_TAG = 'button' type SwitchRenderPropArg = { checked: boolean } -type SwitchPropsWeControl = 'id' | 'role' | 'tabIndex' | 'aria-checked' | 'onClick' | 'onKeyUp' +type SwitchPropsWeControl = + | 'id' + | 'role' + | 'tabIndex' + | 'aria-checked' + | 'onClick' + | 'onKeyUp' + | 'onKeyPress' export function Switch( props: Props< @@ -84,14 +91,18 @@ export function Switch) => { - if (event.key === Keys.Space) { - event.preventDefault() - toggle() - } + if (event.key !== Keys.Tab) event.preventDefault() + if (event.key === Keys.Space) toggle() }, [toggle] ) + // This is needed so that we can "cancel" the click event when we use the `Enter` key on a button. + const handleKeyPress = React.useCallback( + (event: React.KeyboardEvent) => event.preventDefault(), + [] + ) + const propsBag = React.useMemo(() => ({ checked }), [checked]) const propsWeControl = { id, @@ -103,6 +114,7 @@ export function Switch> = { - Space: { key: ' ' }, - Enter: { key: 'Enter' }, - Escape: { key: 'Escape' }, - Backspace: { key: 'Backspace' }, + Space: { key: ' ', keyCode: 32 }, + Enter: { key: 'Enter', keyCode: 13 }, + Escape: { key: 'Escape', keyCode: 27 }, + Backspace: { key: 'Backspace', keyCode: 8 }, - ArrowUp: { key: 'ArrowUp' }, - ArrowDown: { key: 'ArrowDown' }, + ArrowUp: { key: 'ArrowUp', keyCode: 38 }, + ArrowDown: { key: 'ArrowDown', keyCode: 40 }, - Home: { key: 'Home' }, - End: { key: 'End' }, + Home: { key: 'Home', keyCode: 36 }, + End: { key: 'End', keyCode: 35 }, - PageUp: { key: 'PageUp' }, - PageDown: { key: 'PageDown' }, + PageUp: { key: 'PageUp', keyCode: 33 }, + PageDown: { key: 'PageDown', keyCode: 34 }, - Tab: { key: 'Tab' }, + Tab: { key: 'Tab', keyCode: 9 }, } export function shift(event: Partial) { @@ -38,11 +38,19 @@ export async function type(events: Partial[]) { let element = document.activeElement events.forEach(event => { - const cancelled = !fireEvent.keyDown(element, event) - if (!cancelled && event.key === Keys.Tab.key) { + const cancelled1 = !fireEvent.keyDown(element, event) + + // Special treatment for `Tab` on an element + if (!cancelled1 && event.key === Keys.Tab.key) { element = focusNext(event) } - fireEvent.keyPress(element, event) + + const cancelled2 = !fireEvent.keyPress(element, event) + // Special treatment for `Enter` on a button element + if (!cancelled2 && event.key === Keys.Enter.key && element instanceof HTMLButtonElement) { + fireEvent.click(element) + } + fireEvent.keyUp(element, event) }) diff --git a/packages/@headlessui-vue/src/components/switch/switch.test.tsx b/packages/@headlessui-vue/src/components/switch/switch.test.tsx index 485d7fe..210ffff 100644 --- a/packages/@headlessui-vue/src/components/switch/switch.test.tsx +++ b/packages/@headlessui-vue/src/components/switch/switch.test.tsx @@ -1,4 +1,4 @@ -import { defineComponent, ref, watchEffect } from 'vue' +import { defineComponent, ref, watch } from 'vue' import { render } from '../../test-utils/vue-testing-library' import { Switch, SwitchLabel, SwitchGroup } from './switch' @@ -175,7 +175,7 @@ describe('Keyboard interactions', () => { template: ``, setup() { const checked = ref(false) - watchEffect(() => handleChange(checked.value)) + watch([checked], () => handleChange(checked.value)) return { checked } }, }) @@ -200,6 +200,31 @@ describe('Keyboard interactions', () => { }) }) + describe('`Enter` key', () => { + it('should not be possible to use Enter to toggle the Switch', async () => { + const handleChange = jest.fn() + renderTemplate({ + template: ``, + setup() { + const checked = ref(false) + watch([checked], () => handleChange(checked.value)) + return { checked } + }, + }) + + // Ensure checkbox is off + assertSwitch({ state: SwitchState.Off }) + + // Focus the switch + getSwitch()?.focus() + + // Try to toggle + await press(Keys.Enter) + + expect(handleChange).not.toHaveBeenCalled() + }) + }) + describe('`Tab` key', () => { it('should be possible to tab away from the Switch', async () => { renderTemplate({ @@ -237,7 +262,7 @@ describe('Mouse interactions', () => { template: ``, setup() { const checked = ref(false) - watchEffect(() => handleChange(checked.value)) + watch([checked], () => handleChange(checked.value)) return { checked } }, }) @@ -269,7 +294,7 @@ describe('Mouse interactions', () => { `, setup() { const checked = ref(false) - watchEffect(() => handleChange(checked.value)) + watch([checked], () => handleChange(checked.value)) return { checked } }, }) diff --git a/packages/@headlessui-vue/src/components/switch/switch.ts b/packages/@headlessui-vue/src/components/switch/switch.ts index 4f2c3fe..9fb4508 100644 --- a/packages/@headlessui-vue/src/components/switch/switch.ts +++ b/packages/@headlessui-vue/src/components/switch/switch.ts @@ -70,6 +70,7 @@ export const Switch = defineComponent({ 'aria-labelledby': labelledby.value, onClick: this.handleClick, onKeyUp: this.handleKeyUp, + onKeyPress: this.handleKeyPress, } return render({ @@ -95,10 +96,12 @@ export const Switch = defineComponent({ toggle() }, handleKeyUp(event: KeyboardEvent) { - if (event.key === Keys.Space) { - event.preventDefault() - toggle() - } + if (event.key !== Keys.Tab) event.preventDefault() + if (event.key === Keys.Space) toggle() + }, + // This is needed so that we can "cancel" the click event when we use the `Enter` key on a button. + handleKeyPress(event: KeyboardEvent) { + event.preventDefault() }, } }, diff --git a/packages/@headlessui-vue/src/test-utils/interactions.ts b/packages/@headlessui-vue/src/test-utils/interactions.ts index 5c347c7..78dfcd6 100644 --- a/packages/@headlessui-vue/src/test-utils/interactions.ts +++ b/packages/@headlessui-vue/src/test-utils/interactions.ts @@ -2,21 +2,21 @@ import { nextTick } from 'vue' import { fireEvent } from '@testing-library/dom' export const Keys: Record> = { - Space: { key: ' ' }, - Enter: { key: 'Enter' }, - Escape: { key: 'Escape' }, - Backspace: { key: 'Backspace' }, + Space: { key: ' ', keyCode: 32 }, + Enter: { key: 'Enter', keyCode: 13 }, + Escape: { key: 'Escape', keyCode: 27 }, + Backspace: { key: 'Backspace', keyCode: 8 }, - ArrowUp: { key: 'ArrowUp' }, - ArrowDown: { key: 'ArrowDown' }, + ArrowUp: { key: 'ArrowUp', keyCode: 38 }, + ArrowDown: { key: 'ArrowDown', keyCode: 40 }, - Home: { key: 'Home' }, - End: { key: 'End' }, + Home: { key: 'Home', keyCode: 36 }, + End: { key: 'End', keyCode: 35 }, - PageUp: { key: 'PageUp' }, - PageDown: { key: 'PageDown' }, + PageUp: { key: 'PageUp', keyCode: 33 }, + PageDown: { key: 'PageDown', keyCode: 34 }, - Tab: { key: 'Tab' }, + Tab: { key: 'Tab', keyCode: 9 }, } export function shift(event: Partial) { @@ -36,11 +36,19 @@ export async function type(events: Partial[]) { let element = document.activeElement events.forEach(event => { - const cancelled = !fireEvent.keyDown(element, event) - if (!cancelled && event.key === Keys.Tab.key) { + const cancelled1 = !fireEvent.keyDown(element, event) + + // Special treatment for `Tab` on an element + if (!cancelled1 && event.key === Keys.Tab.key) { element = focusNext(event) } - fireEvent.keyPress(element, event) + + const cancelled2 = !fireEvent.keyPress(element, event) + // Special treatment for `Enter` on a button element + if (!cancelled2 && event.key === Keys.Enter.key && element instanceof HTMLButtonElement) { + fireEvent.click(element) + } + fireEvent.keyUp(element, event) })