From 1fb9216276bf4f9c2d785131d4cd2826794ae6cc Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 24 Aug 2022 16:57:25 -0700 Subject: [PATCH] Fix usePress with HTML input elements --- .../@react-aria/interactions/src/usePress.ts | 42 +++++++-- .../interactions/test/usePress.test.js | 91 ++++++++++++++++++- 2 files changed, 124 insertions(+), 9 deletions(-) diff --git a/packages/@react-aria/interactions/src/usePress.ts b/packages/@react-aria/interactions/src/usePress.ts index 64bd621d1d5..aad8a37d33e 100644 --- a/packages/@react-aria/interactions/src/usePress.ts +++ b/packages/@react-aria/interactions/src/usePress.ts @@ -229,7 +229,7 @@ export function usePress(props: PressHookProps): PressResult { let pressProps: DOMAttributes = { onKeyDown(e) { if (isValidKeyboardEvent(e.nativeEvent, e.currentTarget) && e.currentTarget.contains(e.target as Element)) { - if (shouldPreventDefaultKeyboard(e.target as Element)) { + if (shouldPreventDefaultKeyboard(e.target as Element, e.key)) { e.preventDefault(); } e.stopPropagation(); @@ -290,7 +290,7 @@ export function usePress(props: PressHookProps): PressResult { let onKeyUp = (e: KeyboardEvent) => { if (state.isPressed && isValidKeyboardEvent(e, state.target)) { - if (shouldPreventDefaultKeyboard(e.target as Element)) { + if (shouldPreventDefaultKeyboard(e.target as Element, e.key)) { e.preventDefault(); } e.stopPropagation(); @@ -674,15 +674,14 @@ function isHTMLAnchorLink(target: Element): boolean { function isValidKeyboardEvent(event: KeyboardEvent, currentTarget: Element): boolean { const {key, code} = event; const element = currentTarget as HTMLElement; - const {tagName, isContentEditable} = element; const role = element.getAttribute('role'); // Accessibility for keyboards. Space and Enter only. // "Spacebar" is for IE 11 return ( (key === 'Enter' || key === ' ' || key === 'Spacebar' || code === 'Space') && - (tagName !== 'INPUT' && - tagName !== 'TEXTAREA' && - isContentEditable !== true) && + !((element instanceof HTMLInputElement && !isValidInputKey(element, key)) || + element instanceof HTMLTextAreaElement || + element.isContentEditable) && // A link with a valid href should be handled natively, // unless it also has role='button' and was triggered using Space. (!isHTMLAnchorLink(element) || (role === 'button' && key !== 'Enter')) && @@ -774,8 +773,35 @@ function shouldPreventDefault(target: Element) { return !(target instanceof HTMLElement) || !target.draggable; } -function shouldPreventDefaultKeyboard(target: Element) { - return !((target.tagName === 'INPUT' || target.tagName === 'BUTTON') && (target as HTMLButtonElement | HTMLInputElement).type === 'submit'); +function shouldPreventDefaultKeyboard(target: Element, key: string) { + if (target instanceof HTMLInputElement) { + return !isValidInputKey(target, key); + } + + if (target instanceof HTMLButtonElement) { + return target.type !== 'submit'; + } + + return true; +} + +const nonTextInputTypes = new Set([ + 'checkbox', + 'radio', + 'range', + 'color', + 'file', + 'image', + 'button', + 'submit', + 'reset' +]); + +function isValidInputKey(target: HTMLInputElement, key: string) { + // Only space should toggle checkboxes and radios, not enter. + return target.type === 'checkbox' || target.type === 'radio' + ? key === ' ' + : nonTextInputTypes.has(target.type); } function isVirtualPointerEvent(event: PointerEvent) { diff --git a/packages/@react-aria/interactions/test/usePress.test.js b/packages/@react-aria/interactions/test/usePress.test.js index 4f11e07ce83..1ef45d4d8c5 100644 --- a/packages/@react-aria/interactions/test/usePress.test.js +++ b/packages/@react-aria/interactions/test/usePress.test.js @@ -22,7 +22,7 @@ import {usePress} from '../'; function Example(props) { let {elementType: ElementType = 'div', style, draggable, ...otherProps} = props; let {pressProps} = usePress(otherProps); - return test; + return {ElementType !== 'input' ? 'test' : undefined}; } function pointerEvent(type, opts) { @@ -2182,6 +2182,95 @@ describe('usePress', function () { expect(events).toEqual([]); }); + + it('should fire press events on checkboxes but not prevent default', function () { + let events = []; + let addEvent = (e) => events.push(e); + let {getByRole} = render( + addEvent({type: 'presschange', pressed})} + onPress={addEvent} + onPressUp={addEvent} /> + ); + + let el = getByRole('checkbox'); + fireEvent.keyDown(el, {key: 'Enter'}); + fireEvent.keyUp(el, {key: 'Enter'}); + + // Enter key handled should do nothing on a checkbox + expect(events).toEqual([]); + + let allow = fireEvent.keyDown(el, {key: ' '}); + expect(allow).toBeTruthy(); + expect(events).toEqual([ + { + type: 'pressstart', + target: el, + pointerType: 'keyboard', + ctrlKey: false, + metaKey: false, + shiftKey: false, + altKey: false + }, + { + type: 'presschange', + pressed: true + } + ]); + + allow = fireEvent.keyUp(el, {key: ' '}); + expect(allow).toBeTruthy(); + expect(events).toEqual([ + { + type: 'pressstart', + target: el, + pointerType: 'keyboard', + ctrlKey: false, + metaKey: false, + shiftKey: false, + altKey: false + }, + { + type: 'presschange', + pressed: true + }, + { + type: 'pressup', + target: el, + pointerType: 'keyboard', + ctrlKey: false, + metaKey: false, + shiftKey: false, + altKey: false + }, + { + type: 'pressend', + target: el, + pointerType: 'keyboard', + ctrlKey: false, + metaKey: false, + shiftKey: false, + altKey: false + }, + { + type: 'presschange', + pressed: false + }, + { + type: 'press', + target: el, + pointerType: 'keyboard', + ctrlKey: false, + metaKey: false, + shiftKey: false, + altKey: false + } + ]); + }); }); describe('virtual click events', function () {