@samitouri / QOS-React-2 / commits / 57a5805a9f

[react-ui] Add preventDefault+stopPropagation to Keyboard + update Focus components (#16833)

Dominic Gannaway committed Sep 20, 2019 at 11:21 UTC 57a5805a9f07cbda3fea99b7cca59bd3b2bb8476
6 files changed +124 -141
packages/react-ui/accessibility/src/FocusTable.js
+6 -6
@@ -146,7 +146,7 @@ export function createFocusTable(): Array<React.Component> {
146 function Cell({children}): FocusCellProps {
147 const scopeRef = useRef(null);
148 const keyboard = useKeyboard({
149 - onKeyDown(event: KeyboardEvent): boolean {
149 + onKeyDown(event: KeyboardEvent): void {
150 const currentCell = scopeRef.current;
151 switch (event.key) {
152 case 'UpArrow': {
@@ -162,7 +162,7 @@ export function createFocusTable(): Array<React.Component> {
162 }
163 }
164 }
165 - return false;
165 + return;
166 }
167 case 'DownArrow': {
168 const [cells, rowIndex] = getRowCells(currentCell);
@@ -179,7 +179,7 @@ export function createFocusTable(): Array<React.Component> {
179 }
180 }
181 }
182 - return false;
182 + return;
183 }
184 case 'LeftArrow': {
185 const [cells, rowIndex] = getRowCells(currentCell);
@@ -190,7 +190,7 @@ export function createFocusTable(): Array<React.Component> {
190 triggerNavigateOut(currentCell, 'left');
191 }
192 }
193 - return false;
193 + return;
194 }
195 case 'RightArrow': {
196 const [cells, rowIndex] = getRowCells(currentCell);
@@ -203,10 +203,10 @@ export function createFocusTable(): Array<React.Component> {
203 }
204 }
205 }
206 - return false;
206 + return;
207 }
208 }
209 - return true;
209 + event.continuePropagation();
210 },
211 });
212 return (
packages/react-ui/accessibility/src/TabFocus.js
+43 -20
@@ -53,10 +53,11 @@ function focusElem(elem: null | HTMLElement): void {
53 }
54 }
55
56 -export function focusNext(
56 +function internalFocusNext(
57 scope: ReactScopeMethods,
58 + event?: KeyboardEvent,
59 contain?: boolean,
59 -): boolean {
60 +): void {
61 const [
62 tabbableNodes,
63 firstTabbableElem,
@@ -66,23 +67,31 @@ export function focusNext(
67 ] = getTabbableNodes(scope);
68
69 if (focusedElement === null) {
69 - focusElem(firstTabbableElem);
70 + if (event) {
71 + event.continuePropagation();
72 + }
73 } else if (focusedElement === lastTabbableElem) {
71 - if (contain === true) {
74 + if (contain) {
75 focusElem(firstTabbableElem);
73 - } else {
74 - return true;
76 + if (event) {
77 + event.preventDefault();
78 + }
79 + } else if (event) {
80 + event.continuePropagation();
81 }
82 } else {
83 focusElem((tabbableNodes: any)[currentIndex + 1]);
84 + if (event) {
85 + event.preventDefault();
86 + }
87 }
79 - return false;
88 }
89
82 -export function focusPrevious(
90 +function internalFocusPrevious(
91 scope: ReactScopeMethods,
92 + event?: KeyboardEvent,
93 contain?: boolean,
85 -): boolean {
94 +): void {
95 const [
96 tabbableNodes,
97 firstTabbableElem,
@@ -92,17 +101,32 @@ export function focusPrevious(
101 ] = getTabbableNodes(scope);
102
103 if (focusedElement === null) {
95 - focusElem(firstTabbableElem);
104 + if (event) {
105 + event.continuePropagation();
106 + }
107 } else if (focusedElement === firstTabbableElem) {
97 - if (contain === true) {
108 + if (contain) {
109 focusElem(lastTabbableElem);
99 - } else {
100 - return true;
110 + if (event) {
111 + event.preventDefault();
112 + }
113 + } else if (event) {
114 + event.continuePropagation();
115 }
116 } else {
117 focusElem((tabbableNodes: any)[currentIndex - 1]);
118 + if (event) {
119 + event.preventDefault();
120 + }
121 }
105 - return false;
122 +}
123 +
124 +export function focusPrevious(scope: ReactScopeMethods): void {
125 + internalFocusPrevious(scope);
126 +}
127 +
128 +export function focusNext(scope: ReactScopeMethods): void {
129 + internalFocusNext(scope);
130 }
131
132 export function getNextController(
@@ -137,21 +161,20 @@ export const TabFocusController = React.forwardRef(
161 ({children, contain}: TabFocusControllerProps, ref): React.Node => {
162 const scopeRef = useRef(null);
163 const keyboard = useKeyboard({
140 - onKeyDown(event: KeyboardEvent): boolean {
164 + onKeyDown(event: KeyboardEvent): void {
165 if (event.key !== 'Tab') {
142 - return true;
166 + event.continuePropagation();
167 + return;
168 }
169 const scope = scopeRef.current;
170 if (scope !== null) {
171 if (event.shiftKey) {
147 - return focusPrevious(scope, contain);
172 + internalFocusPrevious(scope, event, contain);
173 } else {
149 - return focusNext(scope, contain);
174 + internalFocusNext(scope, event, contain);
175 }
176 }
152 - return true;
177 },
154 - preventKeys: ['Tab', ['Tab', {shiftKey: true}]],
178 });
179
180 return (
packages/react-ui/accessibility/src/__tests__/TabFocus-test.internal.js
+2 -2
@@ -255,14 +255,14 @@ describe('TabFocusController', () => {
255 firstFocusController,
256 );
257 expect(nextController).toBe(secondFocusController);
258 - ReactTabFocus.focusNext(nextController);
258 + ReactTabFocus.focusFirst(nextController);
259 expect(document.activeElement).toBe(divRef.current);
260
261 const previousController = ReactTabFocus.getPreviousController(
262 nextController,
263 );
264 expect(previousController).toBe(firstFocusController);
265 - ReactTabFocus.focusNext(previousController);
265 + ReactTabFocus.focusFirst(previousController);
266 expect(document.activeElement).toBe(buttonRef.current);
267 });
268 });
packages/react-ui/events/src/dom/Keyboard.js
+24 -85
@@ -24,15 +24,12 @@ type KeyboardEventType =
24
25 type KeyboardProps = {|
26 disabled?: boolean,
27 - onClick?: (e: KeyboardEvent) => ?boolean,
28 - onKeyDown?: (e: KeyboardEvent) => ?boolean,
29 - onKeyUp?: (e: KeyboardEvent) => ?boolean,
30 - preventClick?: boolean,
31 - preventKeys?: PreventKeysArray,
27 + onClick?: (e: KeyboardEvent) => void,
28 + onKeyDown?: (e: KeyboardEvent) => void,
29 + onKeyUp?: (e: KeyboardEvent) => void,
30 |};
31
32 type KeyboardState = {|
35 - defaultPrevented: boolean,
33 isActive: boolean,
34 |};
35
@@ -48,20 +45,11 @@ export type KeyboardEvent = {|
45 target: Element | Document,
46 type: KeyboardEventType,
47 timeStamp: number,
48 + continuePropagation: () => void,
49 + preventDefault: () => void,
50 |};
51
53 -type ModifiersObject = {|
54 - altKey?: boolean,
55 - ctrlKey?: boolean,
56 - metaKey?: boolean,
57 - shiftKey?: boolean,
58 -|};
59 -
60 -type PreventKeysArray = Array<string | Array<string | ModifiersObject>>;
61 -
62 -const isArray = Array.isArray;
52 const targetEventTypes = ['click_active', 'keydown_active', 'keyup'];
64 -const modifiers = ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'];
53
54 /**
55 * Normalization of deprecated HTML5 `key` values
@@ -146,20 +134,31 @@ function createKeyboardEvent(
134 event: ReactDOMResponderEvent,
135 context: ReactDOMResponderContext,
136 type: KeyboardEventType,
149 - defaultPrevented: boolean,
137 ): KeyboardEvent {
138 const nativeEvent = (event: any).nativeEvent;
139 const {altKey, ctrlKey, metaKey, shiftKey} = nativeEvent;
140 let keyboardEvent = {
141 altKey,
142 ctrlKey,
156 - defaultPrevented,
143 + defaultPrevented: nativeEvent.defaultPrevented === true,
144 metaKey,
145 pointerType: 'keyboard',
146 shiftKey,
147 target: event.target,
148 timeStamp: context.getTimeStamp(),
149 type,
150 + // We don't use stopPropagation, as the default behavior
151 + // is to not propagate. Plus, there might be confusion
152 + // using stopPropagation as we don't actually stop
153 + // native propagation from working, but instead only
154 + // allow propagation to the others keyboard responders.
155 + continuePropagation() {
156 + context.continuePropagation();
157 + },
158 + preventDefault() {
159 + keyboardEvent.defaultPrevented = true;
160 + nativeEvent.preventDefault();
161 + },
162 };
163 if (type !== 'keyboard:click') {
164 const key = getEventKey(nativeEvent);
@@ -171,32 +170,18 @@ function createKeyboardEvent(
170
171 function dispatchKeyboardEvent(
172 event: ReactDOMResponderEvent,
174 - listener: KeyboardEvent => ?boolean,
173 + listener: KeyboardEvent => void,
174 context: ReactDOMResponderContext,
175 type: KeyboardEventType,
177 - defaultPrevented: boolean,
176 ): void {
179 - const syntheticEvent = createKeyboardEvent(
180 - event,
181 - context,
182 - type,
183 - defaultPrevented,
184 - );
185 - let shouldPropagate;
186 - const listenerWithReturnValue = e => {
187 - shouldPropagate = listener(e);
188 - };
189 - context.dispatchEvent(syntheticEvent, listenerWithReturnValue, DiscreteEvent);
190 - if (shouldPropagate) {
191 - context.continuePropagation();
192 - }
177 + const syntheticEvent = createKeyboardEvent(event, context, type);
178 + context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
179 }
180
181 const keyboardResponderImpl = {
182 targetEventTypes,
183 getInitialState(): KeyboardState {
184 return {
199 - defaultPrevented: false,
185 isActive: false,
186 };
187 },
@@ -207,71 +192,26 @@ const keyboardResponderImpl = {
192 state: KeyboardState,
193 ): void {
194 const {type} = event;
210 - const nativeEvent: any = event.nativeEvent;
195
196 if (props.disabled) {
197 return;
198 }
199
200 if (type === 'keydown') {
217 - state.defaultPrevented = nativeEvent.defaultPrevented === true;
218 -
219 - const preventKeys = ((props.preventKeys: any): PreventKeysArray);
220 - if (!state.defaultPrevented && isArray(preventKeys)) {
221 - preventKeyLoop: for (let i = 0; i < preventKeys.length; i++) {
222 - const preventKey = preventKeys[i];
223 - let key = preventKey;
224 -
225 - if (isArray(preventKey)) {
226 - key = preventKey[0];
227 - const config = ((preventKey[1]: any): Object);
228 - for (let s = 0; s < modifiers.length; s++) {
229 - const modifier = modifiers[s];
230 - const configModifier = config[modifier];
231 - const eventModifier = nativeEvent[modifier];
232 - if (
233 - (configModifier && !eventModifier) ||
234 - (!configModifier && eventModifier)
235 - ) {
236 - continue preventKeyLoop;
237 - }
238 - }
239 - }
240 -
241 - if (key === getEventKey(nativeEvent)) {
242 - state.defaultPrevented = true;
243 - nativeEvent.preventDefault();
244 - break;
245 - }
246 - }
247 - }
201 state.isActive = true;
202 const onKeyDown = props.onKeyDown;
203 if (onKeyDown != null) {
204 dispatchKeyboardEvent(
205 event,
253 - ((onKeyDown: any): (e: KeyboardEvent) => ?boolean),
206 + ((onKeyDown: any): (e: KeyboardEvent) => void),
207 context,
208 'keyboard:keydown',
256 - state.defaultPrevented,
209 );
210 }
211 } else if (type === 'click' && isVirtualClick(event)) {
260 - if (props.preventClick !== false) {
261 - // 'click' occurs before or after 'keyup', and may need native
262 - // behavior prevented
263 - nativeEvent.preventDefault();
264 - state.defaultPrevented = true;
265 - }
212 const onClick = props.onClick;
213 if (onClick != null) {
268 - dispatchKeyboardEvent(
269 - event,
270 - onClick,
271 - context,
272 - 'keyboard:click',
273 - state.defaultPrevented,
274 - );
214 + dispatchKeyboardEvent(event, onClick, context, 'keyboard:click');
215 }
216 } else if (type === 'keyup') {
217 state.isActive = false;
@@ -279,10 +219,9 @@ const keyboardResponderImpl = {
219 if (onKeyUp != null) {
220 dispatchKeyboardEvent(
221 event,
282 - ((onKeyUp: any): (e: KeyboardEvent) => ?boolean),
222 + ((onKeyUp: any): (e: KeyboardEvent) => void),
223 context,
224 'keyboard:keyup',
285 - state.defaultPrevented,
225 );
226 }
227 }
packages/react-ui/events/src/dom/Press.js
+13 -6
@@ -81,6 +81,13 @@ function isValidKey(e): boolean {
81 );
82 }
83
84 +function handlePreventDefault(preventDefault: ?boolean, e: any): void {
85 + const key = e.key;
86 + if (preventDefault !== false && (key === ' ' || key === 'Enter')) {
87 + e.preventDefault();
88 + }
89 +}
90 +
91 /**
92 * The lack of built-in composition for gesture responders means we have to
93 * selectively ignore callbacks from useKeyboard or useTap if the other is
@@ -155,28 +162,30 @@ export function usePress(props: PressProps) {
162
163 const keyboard = useKeyboard({
164 disabled: disabled || active === 'tap',
158 - preventClick: preventDefault !== false,
159 - preventKeys: preventDefault !== false ? [' ', 'Enter'] : [],
165 onClick(e) {
166 + if (preventDefault !== false) {
167 + e.preventDefault();
168 + }
169 if (active == null && onPress != null) {
170 onPress(createGestureState(e, 'press'));
171 }
172 },
173 onKeyDown(e) {
174 if (active == null && isValidKey(e)) {
175 + handlePreventDefault(preventDefault, e);
176 updateActive('keyboard');
177 +
178 if (onPressStart != null) {
179 onPressStart(createGestureState(e, 'pressstart'));
180 }
181 if (onPressChange != null) {
182 onPressChange(true);
183 }
174 - // stop propagation
175 - return false;
184 }
185 },
186 onKeyUp(e) {
187 if (active === 'keyboard' && isValidKey(e)) {
188 + handlePreventDefault(preventDefault, e);
189 if (onPressChange != null) {
190 onPressChange(false);
191 }
@@ -187,8 +196,6 @@ export function usePress(props: PressProps) {
196 onPress(createGestureState(e, 'press'));
197 }
198 updateActive(null);
190 - // stop propagation
191 - return false;
199 }
200 },
201 });
packages/react-ui/events/src/dom/__tests__/Keyboard-test.internal.js
+36 -22
@@ -41,9 +41,9 @@ describe('Keyboard responder', () => {
41 });
42
43 function renderPropagationTest(propagates) {
44 - const onClickInner = jest.fn(() => propagates);
45 - const onKeyDownInner = jest.fn(() => propagates);
46 - const onKeyUpInner = jest.fn(() => propagates);
44 + const onClickInner = jest.fn(e => propagates && e.continuePropagation());
45 + const onKeyDownInner = jest.fn(e => propagates && e.continuePropagation());
46 + const onKeyUpInner = jest.fn(e => propagates && e.continuePropagation());
47 const onClickOuter = jest.fn();
48 const onKeyDownOuter = jest.fn();
49 const onKeyUpOuter = jest.fn();
@@ -77,7 +77,7 @@ describe('Keyboard responder', () => {
77 };
78 }
79
80 - test('propagates key event when a callback returns true', () => {
80 + test('propagates key event when a continuePropagation() is used', () => {
81 const {
82 onClickInner,
83 onKeyDownInner,
@@ -99,7 +99,7 @@ describe('Keyboard responder', () => {
99 expect(onClickOuter).toBeCalled();
100 });
101
102 - test('does not propagate key event when a callback returns false', () => {
102 + test('does not propagate key event by default', () => {
103 const {
104 onClickInner,
105 onKeyDownInner,
@@ -148,7 +148,9 @@ describe('Keyboard responder', () => {
148 let onClick, ref;
149
150 beforeEach(() => {
151 - onClick = jest.fn();
151 + onClick = jest.fn(e => {
152 + e.preventDefault();
153 + });
154 ref = React.createRef();
155 const Component = () => {
156 const listener = useKeyboard({onClick});
@@ -346,7 +348,7 @@ describe('Keyboard responder', () => {
348 });
349 });
350
349 - describe('preventClick', () => {
351 + describe('preventDefault for onClick', () => {
352 function render(props) {
353 const ref = React.createRef();
354 const Component = () => {
@@ -357,7 +359,7 @@ describe('Keyboard responder', () => {
359 return ref;
360 }
361
360 - test('prevents native click by default', () => {
362 + test('does not prevent native click by default', () => {
363 const onClick = jest.fn();
364 const preventDefault = jest.fn();
365 const ref = render({onClick});
@@ -365,33 +367,33 @@ describe('Keyboard responder', () => {
367 const target = createEventTarget(ref.current);
368 target.virtualclick({preventDefault});
369
368 - expect(preventDefault).toBeCalled();
370 + expect(preventDefault).not.toBeCalled();
371 expect(onClick).toHaveBeenCalledTimes(1);
372 expect(onClick).toHaveBeenCalledWith(
373 expect.objectContaining({
372 - defaultPrevented: true,
374 + defaultPrevented: false,
375 }),
376 );
377 });
378
377 - test('allows native behaviour if false', () => {
378 - const onClick = jest.fn();
379 + test('prevents native behaviour with preventDefault', () => {
380 + const onClick = jest.fn(e => e.preventDefault());
381 const preventDefault = jest.fn();
380 - const ref = render({onClick, preventClick: false});
382 + const ref = render({onClick});
383
384 const target = createEventTarget(ref.current);
385 target.virtualclick({preventDefault});
384 - expect(preventDefault).not.toBeCalled();
386 + expect(preventDefault).toBeCalled();
387 expect(onClick).toHaveBeenCalledTimes(1);
388 expect(onClick).toHaveBeenCalledWith(
389 expect.objectContaining({
388 - defaultPrevented: false,
390 + defaultPrevented: true,
391 }),
392 );
393 });
394 });
395
394 - describe('preventKeys', () => {
396 + describe('preventDefault for onKeyDown', () => {
397 function render(props) {
398 const ref = React.createRef();
399 const Component = () => {
@@ -403,10 +405,14 @@ describe('Keyboard responder', () => {
405 }
406
407 test('key config matches', () => {
406 - const onKeyDown = jest.fn();
408 + const onKeyDown = jest.fn(e => {
409 + if (e.key === 'Tab') {
410 + e.preventDefault();
411 + }
412 + });
413 const preventDefault = jest.fn();
414 const preventDefaultClick = jest.fn();
409 - const ref = render({onKeyDown, preventKeys: ['Tab']});
415 + const ref = render({onKeyDown});
416
417 const target = createEventTarget(ref.current);
418 target.keydown({key: 'Tab', preventDefault});
@@ -424,9 +430,13 @@ describe('Keyboard responder', () => {
430 });
431
432 test('key config matches (modifier keys)', () => {
427 - const onKeyDown = jest.fn();
433 + const onKeyDown = jest.fn(e => {
434 + if (e.key === 'Tab' && e.shiftKey) {
435 + e.preventDefault();
436 + }
437 + });
438 const preventDefault = jest.fn();
429 - const ref = render({onKeyDown, preventKeys: [['Tab', {shiftKey: true}]]});
439 + const ref = render({onKeyDown});
440
441 const target = createEventTarget(ref.current);
442 target.keydown({key: 'Tab', preventDefault, shiftKey: true});
@@ -443,9 +453,13 @@ describe('Keyboard responder', () => {
453 });
454
455 test('key config does not match (modifier keys)', () => {
446 - const onKeyDown = jest.fn();
456 + const onKeyDown = jest.fn(e => {
457 + if (e.key === 'Tab' && e.shiftKey) {
458 + e.preventDefault();
459 + }
460 + });
461 const preventDefault = jest.fn();
448 - const ref = render({onKeyDown, preventKeys: [['Tab', {shiftKey: true}]]});
462 + const ref = render({onKeyDown});
463
464 const target = createEventTarget(ref.current);
465 target.keydown({key: 'Tab', preventDefault, shiftKey: false});