Skip to content

Commit

Permalink
[react-events] Adds preventKeys support to Keyboard responder (#16642)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm authored Sep 4, 2019
1 parent f705e2b commit af03276
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 3 deletions.
49 changes: 46 additions & 3 deletions packages/react-events/src/dom/Keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type KeyboardProps = {
disabled: boolean,
onKeyDown: (e: KeyboardEvent) => void,
onKeyUp: (e: KeyboardEvent) => void,
preventKeys: Array<string>,
};

type KeyboardEvent = {|
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -128,6 +132,7 @@ function createKeyboardEvent(
context: ReactDOMResponderContext,
type: KeyboardEventType,
target: Document | Element,
defaultPrevented: boolean,
): KeyboardEvent {
const nativeEvent = (event: any).nativeEvent;
const {
Expand All @@ -143,6 +148,7 @@ function createKeyboardEvent(
return {
altKey,
ctrlKey,
defaultPrevented,
isComposing,
key: getEventKey(nativeEvent),
location,
Expand All @@ -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);
}

Expand All @@ -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(
Expand All @@ -187,6 +228,7 @@ const keyboardResponderImpl = {
context,
'keydown',
((responderTarget: any): Element | Document),
defaultPrevented,
);
}
} else if (type === 'keyup') {
Expand All @@ -198,6 +240,7 @@ const keyboardResponderImpl = {
context,
'keyup',
((responderTarget: any): Element | Document),
defaultPrevented,
);
}
}
Expand Down
128 changes: 128 additions & 0 deletions packages/react-events/src/dom/__tests__/Keyboard-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, 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 <div ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, 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 <div ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, 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 <div ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, 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 <div ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, 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;

Expand Down

0 comments on commit af03276

Please sign in to comment.