Skip to content

Commit af03276

Browse files
authored
[react-events] Adds preventKeys support to Keyboard responder (#16642)
1 parent f705e2b commit af03276

File tree

2 files changed

+174
-3
lines changed

2 files changed

+174
-3
lines changed

packages/react-events/src/dom/Keyboard.js

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type KeyboardProps = {
2222
disabled: boolean,
2323
onKeyDown: (e: KeyboardEvent) => void,
2424
onKeyUp: (e: KeyboardEvent) => void,
25+
preventKeys: Array<string>,
2526
};
2627

2728
type KeyboardEvent = {|
@@ -36,9 +37,12 @@ type KeyboardEvent = {|
3637
target: Element | Document,
3738
type: KeyboardEventType,
3839
timeStamp: number,
40+
defaultPrevented: boolean,
3941
|};
4042

41-
const targetEventTypes = ['keydown', 'keyup'];
43+
const isArray = Array.isArray;
44+
const targetEventTypes = ['keydown_active', 'keyup'];
45+
const modifiers = ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'];
4246

4347
/**
4448
* Normalization of deprecated HTML5 `key` values
@@ -107,7 +111,7 @@ function isFunction(obj): boolean {
107111
return typeof obj === 'function';
108112
}
109113

110-
function getEventKey(nativeEvent): string {
114+
function getEventKey(nativeEvent: Object): string {
111115
const nativeKey = nativeEvent.key;
112116
if (nativeKey) {
113117
// Normalize inconsistent values reported by browsers due to
@@ -128,6 +132,7 @@ function createKeyboardEvent(
128132
context: ReactDOMResponderContext,
129133
type: KeyboardEventType,
130134
target: Document | Element,
135+
defaultPrevented: boolean,
131136
): KeyboardEvent {
132137
const nativeEvent = (event: any).nativeEvent;
133138
const {
@@ -143,6 +148,7 @@ function createKeyboardEvent(
143148
return {
144149
altKey,
145150
ctrlKey,
151+
defaultPrevented,
146152
isComposing,
147153
key: getEventKey(nativeEvent),
148154
location,
@@ -161,8 +167,15 @@ function dispatchKeyboardEvent(
161167
context: ReactDOMResponderContext,
162168
type: KeyboardEventType,
163169
target: Element | Document,
170+
defaultPrevented: boolean,
164171
): void {
165-
const syntheticEvent = createKeyboardEvent(event, context, type, target);
172+
const syntheticEvent = createKeyboardEvent(
173+
event,
174+
context,
175+
type,
176+
target,
177+
defaultPrevented,
178+
);
166179
context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
167180
}
168181

@@ -174,11 +187,39 @@ const keyboardResponderImpl = {
174187
props: KeyboardProps,
175188
): void {
176189
const {responderTarget, type} = event;
190+
const nativeEvent: any = event.nativeEvent;
177191

178192
if (props.disabled) {
179193
return;
180194
}
195+
let defaultPrevented = nativeEvent.defaultPrevented === true;
181196
if (type === 'keydown') {
197+
const preventKeys = props.preventKeys;
198+
if (!defaultPrevented && isArray(preventKeys)) {
199+
preventKeyLoop: for (let i = 0; i < preventKeys.length; i++) {
200+
const preventKey = preventKeys[i];
201+
let key = preventKey;
202+
203+
if (isArray(preventKey)) {
204+
key = preventKey[0];
205+
const config = ((preventKey[1]: any): Object);
206+
for (let s = 0; s < modifiers.length; s++) {
207+
const modifier = modifiers[s];
208+
if (
209+
(config[modifier] && !nativeEvent[modifier]) ||
210+
(!config[modifier] && nativeEvent[modifier])
211+
) {
212+
continue preventKeyLoop;
213+
}
214+
}
215+
}
216+
if (key === getEventKey(nativeEvent)) {
217+
defaultPrevented = true;
218+
nativeEvent.preventDefault();
219+
break;
220+
}
221+
}
222+
}
182223
const onKeyDown = props.onKeyDown;
183224
if (isFunction(onKeyDown)) {
184225
dispatchKeyboardEvent(
@@ -187,6 +228,7 @@ const keyboardResponderImpl = {
187228
context,
188229
'keydown',
189230
((responderTarget: any): Element | Document),
231+
defaultPrevented,
190232
);
191233
}
192234
} else if (type === 'keyup') {
@@ -198,6 +240,7 @@ const keyboardResponderImpl = {
198240
context,
199241
'keyup',
200242
((responderTarget: any): Element | Document),
243+
defaultPrevented,
201244
);
202245
}
203246
}

packages/react-events/src/dom/__tests__/Keyboard-test.internal.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,134 @@ describe('Keyboard event responder', () => {
9292
});
9393
});
9494

95+
describe('preventKeys', () => {
96+
it('onKeyDown is default prevented', () => {
97+
const onKeyDown = jest.fn();
98+
const ref = React.createRef();
99+
const Component = () => {
100+
const listener = useKeyboard({
101+
onKeyDown,
102+
preventKeys: ['Tab'],
103+
});
104+
return <div ref={ref} listeners={listener} />;
105+
};
106+
ReactDOM.render(<Component />, container);
107+
108+
const preventDefault = jest.fn();
109+
const target = createEventTarget(ref.current);
110+
target.keydown({key: 'Tab', preventDefault});
111+
expect(onKeyDown).toHaveBeenCalledTimes(1);
112+
expect(preventDefault).toBeCalled();
113+
expect(onKeyDown).toHaveBeenCalledWith(
114+
expect.objectContaining({
115+
key: 'Tab',
116+
type: 'keydown',
117+
defaultPrevented: true,
118+
}),
119+
);
120+
});
121+
122+
it('onKeyDown is default prevented (falsy modifier keys)', () => {
123+
let onKeyDown = jest.fn();
124+
let ref = React.createRef();
125+
let Component = () => {
126+
const listener = useKeyboard({
127+
onKeyDown,
128+
preventKeys: [['Tab', {metaKey: false}]],
129+
});
130+
return <div ref={ref} listeners={listener} />;
131+
};
132+
ReactDOM.render(<Component />, container);
133+
134+
let preventDefault = jest.fn();
135+
let target = createEventTarget(ref.current);
136+
target.keydown({key: 'Tab', preventDefault, metaKey: true});
137+
expect(onKeyDown).toHaveBeenCalledTimes(1);
138+
expect(preventDefault).not.toBeCalled();
139+
expect(onKeyDown).toHaveBeenCalledWith(
140+
expect.objectContaining({
141+
key: 'Tab',
142+
type: 'keydown',
143+
defaultPrevented: false,
144+
}),
145+
);
146+
147+
onKeyDown = jest.fn();
148+
ref = React.createRef();
149+
Component = () => {
150+
const listener = useKeyboard({
151+
onKeyDown,
152+
preventKeys: [['Tab', {metaKey: true}]],
153+
});
154+
return <div ref={ref} listeners={listener} />;
155+
};
156+
ReactDOM.render(<Component />, container);
157+
158+
preventDefault = jest.fn();
159+
target = createEventTarget(ref.current);
160+
target.keydown({key: 'Tab', preventDefault, metaKey: false});
161+
expect(onKeyDown).toHaveBeenCalledTimes(1);
162+
expect(preventDefault).not.toBeCalled();
163+
expect(onKeyDown).toHaveBeenCalledWith(
164+
expect.objectContaining({
165+
key: 'Tab',
166+
type: 'keydown',
167+
defaultPrevented: false,
168+
}),
169+
);
170+
});
171+
172+
it('onKeyDown is default prevented (truthy modifier keys)', () => {
173+
let onKeyDown = jest.fn();
174+
let ref = React.createRef();
175+
let Component = () => {
176+
const listener = useKeyboard({
177+
onKeyDown,
178+
preventKeys: [['Tab', {metaKey: true}]],
179+
});
180+
return <div ref={ref} listeners={listener} />;
181+
};
182+
ReactDOM.render(<Component />, container);
183+
184+
let preventDefault = jest.fn();
185+
let target = createEventTarget(ref.current);
186+
target.keydown({key: 'Tab', preventDefault, metaKey: true});
187+
expect(onKeyDown).toHaveBeenCalledTimes(1);
188+
expect(preventDefault).toBeCalled();
189+
expect(onKeyDown).toHaveBeenCalledWith(
190+
expect.objectContaining({
191+
key: 'Tab',
192+
type: 'keydown',
193+
defaultPrevented: true,
194+
}),
195+
);
196+
197+
onKeyDown = jest.fn();
198+
ref = React.createRef();
199+
Component = () => {
200+
const listener = useKeyboard({
201+
onKeyDown,
202+
preventKeys: [['Tab', {metaKey: false}]],
203+
});
204+
return <div ref={ref} listeners={listener} />;
205+
};
206+
ReactDOM.render(<Component />, container);
207+
208+
preventDefault = jest.fn();
209+
target = createEventTarget(ref.current);
210+
target.keydown({key: 'Tab', preventDefault, metaKey: false});
211+
expect(onKeyDown).toHaveBeenCalledTimes(1);
212+
expect(preventDefault).toBeCalled();
213+
expect(onKeyDown).toHaveBeenCalledWith(
214+
expect.objectContaining({
215+
key: 'Tab',
216+
type: 'keydown',
217+
defaultPrevented: true,
218+
}),
219+
);
220+
});
221+
});
222+
95223
describe('onKeyUp', () => {
96224
let onKeyDown, onKeyUp, ref;
97225

0 commit comments

Comments
 (0)