ensure that you can't use Enter to invoke the Switch

And a bunch of keyPress and keyboard related shenanigans
This commit is contained in:
Robin Malfait
2020-10-06 14:00:01 +02:00
parent 6ea5c93457
commit fecd61dff6
6 changed files with 115 additions and 41 deletions
@@ -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(<Switch checked={false} onChange={handleChange} />)
// 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(
@@ -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<TTag extends React.ElementType = typeof DEFAULT_SWITCH_TAG>(
props: Props<
@@ -84,14 +91,18 @@ export function Switch<TTag extends React.ElementType = typeof DEFAULT_SWITCH_TA
)
const handleKeyUp = React.useCallback(
(event: React.KeyboardEvent<HTMLElement>) => {
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<HTMLElement>) => event.preventDefault(),
[]
)
const propsBag = React.useMemo<SwitchRenderPropArg>(() => ({ checked }), [checked])
const propsWeControl = {
id,
@@ -103,6 +114,7 @@ export function Switch<TTag extends React.ElementType = typeof DEFAULT_SWITCH_TA
'aria-labelledby': groupContext?.label?.id,
onClick: handleClick,
onKeyUp: handleKeyUp,
onKeyPress: handleKeyPress,
}
return render({ ...passThroughProps, ...propsWeControl }, propsBag, DEFAULT_SWITCH_TAG)
@@ -4,21 +4,21 @@ import { disposables } from '../utils/disposables'
const d = disposables()
export const Keys: Record<string, Partial<KeyboardEvent>> = {
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<KeyboardEvent>) {
@@ -38,11 +38,19 @@ export async function type(events: Partial<KeyboardEvent>[]) {
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)
})
@@ -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: `<Switch v-model="checked" />`,
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: `<Switch v-model="checked" />`,
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: `<Switch v-model="checked" />`,
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 }
},
})
@@ -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()
},
}
},
@@ -2,21 +2,21 @@ import { nextTick } from 'vue'
import { fireEvent } from '@testing-library/dom'
export const Keys: Record<string, Partial<KeyboardEvent>> = {
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<KeyboardEvent>) {
@@ -36,11 +36,19 @@ export async function type(events: Partial<KeyboardEvent>[]) {
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)
})