diff --git a/packages/react-events/src/dom/Keyboard.js b/packages/react-events/src/dom/Keyboard.js index f6926059681fc..00f4ffb16a880 100644 --- a/packages/react-events/src/dom/Keyboard.js +++ b/packages/react-events/src/dom/Keyboard.js @@ -22,6 +22,7 @@ type KeyboardProps = { disabled: boolean, onKeyDown: (e: KeyboardEvent) => void, onKeyUp: (e: KeyboardEvent) => void, + preventKeys: Array, }; type KeyboardEvent = {| @@ -36,9 +37,12 @@ type KeyboardEvent = {| target: Element | Document, type: KeyboardEventType, timeStamp: number, + defaultPrevented: boolean, |}; -const targetEventTypes = ['keydown', 'keyup']; +const isArray = Array.isArray; +const targetEventTypes = ['keydown_active', 'keyup']; +const modifiers = ['altKey', 'ctrlKey', 'metaKey', 'shiftKey']; /** * Normalization of deprecated HTML5 `key` values @@ -107,7 +111,7 @@ function isFunction(obj): boolean { return typeof obj === 'function'; } -function getEventKey(nativeEvent): string { +function getEventKey(nativeEvent: Object): string { const nativeKey = nativeEvent.key; if (nativeKey) { // Normalize inconsistent values reported by browsers due to @@ -128,6 +132,7 @@ function createKeyboardEvent( context: ReactDOMResponderContext, type: KeyboardEventType, target: Document | Element, + defaultPrevented: boolean, ): KeyboardEvent { const nativeEvent = (event: any).nativeEvent; const { @@ -143,6 +148,7 @@ function createKeyboardEvent( return { altKey, ctrlKey, + defaultPrevented, isComposing, key: getEventKey(nativeEvent), location, @@ -161,8 +167,15 @@ function dispatchKeyboardEvent( context: ReactDOMResponderContext, type: KeyboardEventType, target: Element | Document, + defaultPrevented: boolean, ): void { - const syntheticEvent = createKeyboardEvent(event, context, type, target); + const syntheticEvent = createKeyboardEvent( + event, + context, + type, + target, + defaultPrevented, + ); context.dispatchEvent(syntheticEvent, listener, DiscreteEvent); } @@ -174,11 +187,39 @@ const keyboardResponderImpl = { props: KeyboardProps, ): void { const {responderTarget, type} = event; + const nativeEvent: any = event.nativeEvent; if (props.disabled) { return; } + let defaultPrevented = nativeEvent.defaultPrevented === true; if (type === 'keydown') { + const preventKeys = props.preventKeys; + if (!defaultPrevented && isArray(preventKeys)) { + preventKeyLoop: for (let i = 0; i < preventKeys.length; i++) { + const preventKey = preventKeys[i]; + let key = preventKey; + + if (isArray(preventKey)) { + key = preventKey[0]; + const config = ((preventKey[1]: any): Object); + for (let s = 0; s < modifiers.length; s++) { + const modifier = modifiers[s]; + if ( + (config[modifier] && !nativeEvent[modifier]) || + (!config[modifier] && nativeEvent[modifier]) + ) { + continue preventKeyLoop; + } + } + } + if (key === getEventKey(nativeEvent)) { + defaultPrevented = true; + nativeEvent.preventDefault(); + break; + } + } + } const onKeyDown = props.onKeyDown; if (isFunction(onKeyDown)) { dispatchKeyboardEvent( @@ -187,6 +228,7 @@ const keyboardResponderImpl = { context, 'keydown', ((responderTarget: any): Element | Document), + defaultPrevented, ); } } else if (type === 'keyup') { @@ -198,6 +240,7 @@ const keyboardResponderImpl = { context, 'keyup', ((responderTarget: any): Element | Document), + defaultPrevented, ); } } diff --git a/packages/react-events/src/dom/__tests__/Keyboard-test.internal.js b/packages/react-events/src/dom/__tests__/Keyboard-test.internal.js index f5688b56c62aa..412aaa84b98d2 100644 --- a/packages/react-events/src/dom/__tests__/Keyboard-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Keyboard-test.internal.js @@ -92,6 +92,134 @@ describe('Keyboard event responder', () => { }); }); + describe('preventKeys', () => { + it('onKeyDown is default prevented', () => { + const onKeyDown = jest.fn(); + const ref = React.createRef(); + const Component = () => { + const listener = useKeyboard({ + onKeyDown, + preventKeys: ['Tab'], + }); + return
; + }; + ReactDOM.render(, container); + + const preventDefault = jest.fn(); + const target = createEventTarget(ref.current); + target.keydown({key: 'Tab', preventDefault}); + expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(preventDefault).toBeCalled(); + expect(onKeyDown).toHaveBeenCalledWith( + expect.objectContaining({ + key: 'Tab', + type: 'keydown', + defaultPrevented: true, + }), + ); + }); + + it('onKeyDown is default prevented (falsy modifier keys)', () => { + let onKeyDown = jest.fn(); + let ref = React.createRef(); + let Component = () => { + const listener = useKeyboard({ + onKeyDown, + preventKeys: [['Tab', {metaKey: false}]], + }); + return
; + }; + ReactDOM.render(, container); + + let preventDefault = jest.fn(); + let target = createEventTarget(ref.current); + target.keydown({key: 'Tab', preventDefault, metaKey: true}); + expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(preventDefault).not.toBeCalled(); + expect(onKeyDown).toHaveBeenCalledWith( + expect.objectContaining({ + key: 'Tab', + type: 'keydown', + defaultPrevented: false, + }), + ); + + onKeyDown = jest.fn(); + ref = React.createRef(); + Component = () => { + const listener = useKeyboard({ + onKeyDown, + preventKeys: [['Tab', {metaKey: true}]], + }); + return
; + }; + ReactDOM.render(, container); + + preventDefault = jest.fn(); + target = createEventTarget(ref.current); + target.keydown({key: 'Tab', preventDefault, metaKey: false}); + expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(preventDefault).not.toBeCalled(); + expect(onKeyDown).toHaveBeenCalledWith( + expect.objectContaining({ + key: 'Tab', + type: 'keydown', + defaultPrevented: false, + }), + ); + }); + + it('onKeyDown is default prevented (truthy modifier keys)', () => { + let onKeyDown = jest.fn(); + let ref = React.createRef(); + let Component = () => { + const listener = useKeyboard({ + onKeyDown, + preventKeys: [['Tab', {metaKey: true}]], + }); + return
; + }; + ReactDOM.render(, container); + + let preventDefault = jest.fn(); + let target = createEventTarget(ref.current); + target.keydown({key: 'Tab', preventDefault, metaKey: true}); + expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(preventDefault).toBeCalled(); + expect(onKeyDown).toHaveBeenCalledWith( + expect.objectContaining({ + key: 'Tab', + type: 'keydown', + defaultPrevented: true, + }), + ); + + onKeyDown = jest.fn(); + ref = React.createRef(); + Component = () => { + const listener = useKeyboard({ + onKeyDown, + preventKeys: [['Tab', {metaKey: false}]], + }); + return
; + }; + ReactDOM.render(, container); + + preventDefault = jest.fn(); + target = createEventTarget(ref.current); + target.keydown({key: 'Tab', preventDefault, metaKey: false}); + expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(preventDefault).toBeCalled(); + expect(onKeyDown).toHaveBeenCalledWith( + expect.objectContaining({ + key: 'Tab', + type: 'keydown', + defaultPrevented: true, + }), + ); + }); + }); + describe('onKeyUp', () => { let onKeyDown, onKeyUp, ref;