@samitouri / QOS-React-2 / commits / 997154bcc7

[Flare] Add FocusWithin responder (#16152)

FocusWithin is implemented as a separate responder, which keeps both focus responders simple and allows for easier composition of behaviours.

Nicolas Gallagher committed Jul 17, 2019 at 15:17 UTC 997154bcc7091f93733090497c714e5806424168
3 files changed +676 -173
packages/react-events/docs/FocusWithin.md new
+57
@@ -0,0 +1,57 @@
1 +# FocusWithin
2 +
3 +The `FocusWithin` module responds to focus and blur events on its child. Focus events
4 +are dispatched for all input types, with the exception of `onFocusVisibleChange`
5 +which is only dispatched when focusing with a keyboard.
6 +
7 +Focus events do not propagate between `FocusWithin` event responders.
8 +
9 +```js
10 +// Example
11 +const Button = (props) => {
12 + const [ focusWithin, updateFocusWithin ] = useState(false);
13 + const [ focusWithinVisible, updateFocusWithinVisible ] = useState(false);
14 +
15 + return (
16 + <FocusWithin
17 + onFocusWithinChange={updateFocusWithin}
18 + onFocusWithinVisibleChange={updateFocusWithinVisible}
19 + >
20 + <button
21 + children={props.children}
22 + style={{
23 + ...(focusWithin && focusWithinStyles),
24 + ...(focusWithinVisible && focusWithinVisibleStyles)
25 + }}
26 + >
27 + </FocusWithin>
28 + );
29 +};
30 +```
31 +
32 +## Types
33 +
34 +```js
35 +type FocusEvent = {
36 + target: Element,
37 + type: 'focuswithinchange' | 'focuswithinvisiblechange'
38 +}
39 +```
40 +
41 +## Props
42 +
43 +### disabled: boolean = false
44 +
45 +Disables all `FocusWithin` events.
46 +
47 +### onFocusWithinChange: boolean => void
48 +
49 +Called once the element or a descendant receives focus, and once focus moves
50 +outside of the element.
51 +
52 +### onFocusWithinVisibleChange: boolean => void
53 +
54 +Called once the element or a descendant is focused following keyboard
55 +navigation, and once focus moves outside of the element. This can be used to
56 +display focus styles only when the keyboard is being used to focus within the
57 +element's subtree.
packages/react-events/src/dom/Focus.js
+301 -117
@@ -17,6 +17,24 @@ import type {
17 import React from 'react';
18 import {DiscreteEvent} from 'shared/ReactTypes';
19
20 +/**
21 + * Types
22 + */
23 +
24 +type FocusEvent = {|
25 + target: Element | Document,
26 + type: FocusEventType | FocusWithinEventType,
27 + pointerType: PointerType,
28 + timeStamp: number,
29 +|};
30 +
31 +type FocusState = {
32 + focusTarget: null | Element | Document,
33 + isFocused: boolean,
34 + isFocusVisible: boolean,
35 + pointerType: PointerType,
36 +};
37 +
38 type FocusProps = {
39 disabled: boolean,
40 onBlur: (e: FocusEvent) => void,
@@ -25,21 +43,21 @@ type FocusProps = {
43 onFocusVisibleChange: boolean => void,
44 };
45
28 -type FocusState = {
29 - focusTarget: null | Element | Document,
30 - isFocused: boolean,
31 - isLocalFocusVisible: boolean,
32 - pointerType: PointerType,
46 +type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
47 +
48 +type FocusWithinProps = {
49 + disabled: boolean,
50 + onFocusWithinChange: boolean => void,
51 + onFocusWithinVisibleChange: boolean => void,
52 };
53
35 -type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
54 +type FocusWithinEventType = 'focuswithinvisiblechange' | 'focuswithinchange';
55
37 -type FocusEvent = {|
38 - target: Element | Document,
39 - type: FocusEventType,
40 - pointerType: PointerType,
41 - timeStamp: number,
42 -|};
56 +/**
57 + * Shared between Focus and FocusWithin
58 + */
59 +
60 +let isGlobalFocusVisible = true;
61
62 const isMac =
63 typeof window !== 'undefined' && window.navigator != null
@@ -70,7 +88,7 @@ if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
88
89 function createFocusEvent(
90 context: ReactDOMResponderContext,
73 - type: FocusEventType,
91 + type: FocusEventType | FocusWithinEventType,
92 target: Element | Document,
93 pointerType: PointerType,
94 ): FocusEvent {
@@ -82,7 +100,90 @@ function createFocusEvent(
100 };
101 }
102
85 -function dispatchFocusInEvents(
103 +function handleRootPointerEvent(
104 + event: ReactDOMResponderEvent,
105 + context: ReactDOMResponderContext,
106 + state: FocusState,
107 + callback: boolean => void,
108 +): void {
109 + const {type, target} = event;
110 + // Ignore a Safari quirks where 'mousemove' is dispatched on the 'html'
111 + // element when the window blurs.
112 + if (type === 'mousemove' && target.nodeName === 'HTML') {
113 + return;
114 + }
115 +
116 + isGlobalFocusVisible = false;
117 +
118 + // Focus should stop being visible if a pointer is used on the element
119 + // after it was focused using a keyboard.
120 + const focusTarget = state.focusTarget;
121 + if (
122 + focusTarget !== null &&
123 + context.isTargetWithinNode(event.target, focusTarget) &&
124 + (type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
125 + ) {
126 + callback(false);
127 + }
128 +}
129 +
130 +function handleRootEvent(
131 + event: ReactDOMResponderEvent,
132 + context: ReactDOMResponderContext,
133 + state: FocusState,
134 + callback: boolean => void,
135 +): void {
136 + const {type} = event;
137 +
138 + switch (type) {
139 + case 'mousemove':
140 + case 'mousedown':
141 + case 'mouseup': {
142 + state.pointerType = 'mouse';
143 + handleRootPointerEvent(event, context, state, callback);
144 + break;
145 + }
146 + case 'pointermove':
147 + case 'pointerdown':
148 + case 'pointerup': {
149 + // $FlowFixMe: Flow doesn't know about PointerEvents
150 + const nativeEvent = ((event.nativeEvent: any): PointerEvent);
151 + state.pointerType = nativeEvent.pointerType;
152 + handleRootPointerEvent(event, context, state, callback);
153 + break;
154 + }
155 + case 'touchmove':
156 + case 'touchstart':
157 + case 'touchend': {
158 + state.pointerType = 'touch';
159 + handleRootPointerEvent(event, context, state, callback);
160 + break;
161 + }
162 +
163 + case 'keydown':
164 + case 'keyup': {
165 + const nativeEvent = event.nativeEvent;
166 + if (
167 + nativeEvent.key === 'Tab' &&
168 + !(
169 + nativeEvent.metaKey ||
170 + (!isMac && nativeEvent.altKey) ||
171 + nativeEvent.ctrlKey
172 + )
173 + ) {
174 + state.pointerType = 'keyboard';
175 + isGlobalFocusVisible = true;
176 + }
177 + break;
178 + }
179 + }
180 +}
181 +
182 +/**
183 + * Focus Responder
184 + */
185 +
186 +function dispatchFocusEvents(
187 context: ReactDOMResponderContext,
188 props: FocusProps,
189 state: FocusState,
@@ -110,21 +211,12 @@ function dispatchFocusInEvents(
211 );
212 context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
213 }
113 - if (props.onFocusVisibleChange && state.isLocalFocusVisible) {
114 - const listener = () => {
115 - props.onFocusVisibleChange(true);
116 - };
117 - const syntheticEvent = createFocusEvent(
118 - context,
119 - 'focusvisiblechange',
120 - target,
121 - pointerType,
122 - );
123 - context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
214 + if (state.isFocusVisible) {
215 + dispatchFocusVisibleChangeEvent(context, props, state, true);
216 }
217 }
218
127 -function dispatchFocusOutEvents(
219 +function dispatchBlurEvents(
220 context: ReactDOMResponderContext,
221 props: FocusProps,
222 state: FocusState,
@@ -152,19 +244,22 @@ function dispatchFocusOutEvents(
244 );
245 context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
246 }
155 - dispatchFocusVisibleOutEvent(context, props, state);
247 + if (state.isFocusVisible) {
248 + dispatchFocusVisibleChangeEvent(context, props, state, false);
249 + }
250 }
251
158 -function dispatchFocusVisibleOutEvent(
252 +function dispatchFocusVisibleChangeEvent(
253 context: ReactDOMResponderContext,
254 props: FocusProps,
255 state: FocusState,
256 + value: boolean,
257 ) {
258 const pointerType = state.pointerType;
259 const target = ((state.focusTarget: any): Element | Document);
165 - if (props.onFocusVisibleChange && state.isLocalFocusVisible) {
260 + if (props.onFocusVisibleChange) {
261 const listener = () => {
167 - props.onFocusVisibleChange(false);
262 + props.onFocusVisibleChange(value);
263 };
264 const syntheticEvent = createFocusEvent(
265 context,
@@ -173,49 +268,19 @@ function dispatchFocusVisibleOutEvent(
268 pointerType,
269 );
270 context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
176 - state.isLocalFocusVisible = false;
271 }
272 }
273
180 -function unmountResponder(
274 +function unmountFocusResponder(
275 context: ReactDOMResponderContext,
276 props: FocusProps,
277 state: FocusState,
184 -): void {
278 +) {
279 if (state.isFocused) {
186 - dispatchFocusOutEvents(context, props, state);
187 - }
188 -}
189 -
190 -function handleRootPointerEvent(
191 - event: ReactDOMResponderEvent,
192 - context: ReactDOMResponderContext,
193 - props: FocusProps,
194 - state: FocusState,
195 -): void {
196 - const {type, target} = event;
197 - // Ignore a Safari quirks where 'mousemove' is dispatched on the 'html'
198 - // element when the window blurs.
199 - if (type === 'mousemove' && target.nodeName === 'HTML') {
200 - return;
201 - }
202 -
203 - isGlobalFocusVisible = false;
204 -
205 - // Focus should stop being visible if a pointer is used on the element
206 - // after it was focused using a keyboard.
207 - const focusTarget = state.focusTarget;
208 - if (
209 - focusTarget !== null &&
210 - context.isTargetWithinNode(event.target, focusTarget) &&
211 - (type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
212 - ) {
213 - dispatchFocusVisibleOutEvent(context, props, state);
280 + dispatchBlurEvents(context, props, state);
281 }
282 }
283
217 -let isGlobalFocusVisible = true;
218 -
284 const FocusResponder: ReactDOMEventResponder = {
285 displayName: 'Focus',
286 targetEventTypes,
@@ -224,7 +289,7 @@ const FocusResponder: ReactDOMEventResponder = {
289 return {
290 focusTarget: null,
291 isFocused: false,
227 - isLocalFocusVisible: false,
292 + isFocusVisible: false,
293 pointerType: '',
294 };
295 },
@@ -238,7 +303,7 @@ const FocusResponder: ReactDOMEventResponder = {
303
304 if (props.disabled) {
305 if (state.isFocused) {
241 - dispatchFocusOutEvents(context, props, state);
306 + dispatchBlurEvents(context, props, state);
307 state.isFocused = false;
308 state.focusTarget = null;
309 }
@@ -247,23 +312,21 @@ const FocusResponder: ReactDOMEventResponder = {
312
313 switch (type) {
314 case 'focus': {
250 - if (!state.isFocused) {
251 - // Limit focus events to the direct child of the event component.
252 - // Browser focus is not expected to bubble.
253 - state.focusTarget = event.responderTarget;
254 - if (state.focusTarget === target) {
255 - state.isFocused = true;
256 - state.isLocalFocusVisible = isGlobalFocusVisible;
257 - dispatchFocusInEvents(context, props, state);
258 - }
315 + state.focusTarget = event.responderTarget;
316 + // Limit focus events to the direct child of the event component.
317 + // Browser focus is not expected to bubble.
318 + if (!state.isFocused && state.focusTarget === target) {
319 + state.isFocused = true;
320 + state.isFocusVisible = isGlobalFocusVisible;
321 + dispatchFocusEvents(context, props, state);
322 }
323 break;
324 }
325 case 'blur': {
326 if (state.isFocused) {
264 - dispatchFocusOutEvents(context, props, state);
327 + dispatchBlurEvents(context, props, state);
328 + state.isFocusVisible = isGlobalFocusVisible;
329 state.isFocused = false;
266 - state.focusTarget = null;
330 }
331 break;
332 }
@@ -275,69 +338,190 @@ const FocusResponder: ReactDOMEventResponder = {
338 props: FocusProps,
339 state: FocusState,
340 ): void {
278 - const {type} = event;
279 -
280 - switch (type) {
281 - case 'mousemove':
282 - case 'mousedown':
283 - case 'mouseup': {
284 - state.pointerType = 'mouse';
285 - handleRootPointerEvent(event, context, props, state);
286 - break;
341 + handleRootEvent(event, context, state, isFocusVisible => {
342 + if (state.isFocusVisible !== isFocusVisible) {
343 + state.isFocusVisible = isFocusVisible;
344 + dispatchFocusVisibleChangeEvent(context, props, state, isFocusVisible);
345 }
288 - case 'pointermove':
289 - case 'pointerdown':
290 - case 'pointerup': {
291 - // $FlowFixMe: Flow doesn't know about PointerEvents
292 - const nativeEvent = ((event.nativeEvent: any): PointerEvent);
293 - state.pointerType = nativeEvent.pointerType;
294 - handleRootPointerEvent(event, context, props, state);
295 - break;
346 + });
347 + },
348 + onUnmount(
349 + context: ReactDOMResponderContext,
350 + props: FocusProps,
351 + state: FocusState,
352 + ) {
353 + unmountFocusResponder(context, props, state);
354 + },
355 + onOwnershipChange(
356 + context: ReactDOMResponderContext,
357 + props: FocusProps,
358 + state: FocusState,
359 + ) {
360 + unmountFocusResponder(context, props, state);
361 + },
362 +};
363 +
364 +export const Focus = React.unstable_createEvent(FocusResponder);
365 +
366 +export function useFocus(props: FocusProps): void {
367 + React.unstable_useEvent(Focus, props);
368 +}
369 +
370 +/**
371 + * FocusWithin Responder
372 + */
373 +
374 +function dispatchFocusWithinChangeEvent(
375 + context: ReactDOMResponderContext,
376 + props: FocusWithinProps,
377 + state: FocusState,
378 + value: boolean,
379 +) {
380 + const pointerType = state.pointerType;
381 + const target = ((state.focusTarget: any): Element | Document);
382 + if (props.onFocusWithinChange) {
383 + const listener = function() {
384 + props.onFocusWithinChange(value);
385 + };
386 + const syntheticEvent = createFocusEvent(
387 + context,
388 + 'focuswithinchange',
389 + target,
390 + pointerType,
391 + );
392 + context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
393 + }
394 + if (state.isFocusVisible) {
395 + dispatchFocusWithinVisibleChangeEvent(context, props, state, value);
396 + }
397 +}
398 +
399 +function dispatchFocusWithinVisibleChangeEvent(
400 + context: ReactDOMResponderContext,
401 + props: FocusWithinProps,
402 + state: FocusState,
403 + value: boolean,
404 +) {
405 + const pointerType = state.pointerType;
406 + const target = ((state.focusTarget: any): Element | Document);
407 + if (props.onFocusWithinVisibleChange) {
408 + const listener = function() {
409 + props.onFocusWithinVisibleChange(value);
410 + };
411 + const syntheticEvent = createFocusEvent(
412 + context,
413 + 'focuswithinvisiblechange',
414 + target,
415 + pointerType,
416 + );
417 + context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
418 + }
419 +}
420 +
421 +function unmountFocusWithinResponder(
422 + context: ReactDOMResponderContext,
423 + props: FocusWithinProps,
424 + state: FocusState,
425 +) {
426 + if (state.isFocused) {
427 + dispatchFocusWithinChangeEvent(context, props, state, false);
428 + }
429 +}
430 +
431 +const FocusWithinResponder: ReactDOMEventResponder = {
432 + displayName: 'FocusWithin',
433 + targetEventTypes,
434 + rootEventTypes,
435 + getInitialState(): FocusState {
436 + return {
437 + focusTarget: null,
438 + isFocused: false,
439 + isFocusVisible: false,
440 + pointerType: '',
441 + };
442 + },
443 + onEvent(
444 + event: ReactDOMResponderEvent,
445 + context: ReactDOMResponderContext,
446 + props: FocusWithinProps,
447 + state: FocusState,
448 + ): void {
449 + const {nativeEvent, type} = event;
450 + const relatedTarget = (nativeEvent: any).relatedTarget;
451 +
452 + if (props.disabled) {
453 + if (state.isFocused) {
454 + dispatchFocusWithinChangeEvent(context, props, state, false);
455 + state.isFocused = false;
456 + state.focusTarget = null;
457 }
297 - case 'touchmove':
298 - case 'touchstart':
299 - case 'touchend': {
300 - state.pointerType = 'touch';
301 - handleRootPointerEvent(event, context, props, state);
458 + return;
459 + }
460 +
461 + switch (type) {
462 + case 'focus': {
463 + state.focusTarget = event.responderTarget;
464 + // Limit focus events to the direct child of the event component.
465 + // Browser focus is not expected to bubble.
466 + if (!state.isFocused) {
467 + state.isFocused = true;
468 + state.isFocusVisible = isGlobalFocusVisible;
469 + dispatchFocusWithinChangeEvent(context, props, state, true);
470 + }
471 + if (!state.isFocusVisible && isGlobalFocusVisible) {
472 + state.isFocusVisible = isGlobalFocusVisible;
473 + dispatchFocusWithinVisibleChangeEvent(context, props, state, true);
474 + }
475 break;
476 }
304 -
305 - case 'keydown':
306 - case 'keyup': {
307 - const nativeEvent = event.nativeEvent;
477 + case 'blur': {
478 if (
309 - nativeEvent.key === 'Tab' &&
310 - !(
311 - nativeEvent.metaKey ||
312 - (!isMac && nativeEvent.altKey) ||
313 - nativeEvent.ctrlKey
314 - )
479 + state.isFocused &&
480 + !context.isTargetWithinEventResponderScope(relatedTarget)
481 ) {
316 - state.pointerType = 'keyboard';
317 - isGlobalFocusVisible = true;
482 + dispatchFocusWithinChangeEvent(context, props, state, false);
483 + state.isFocused = false;
484 }
485 break;
486 }
487 }
488 },
489 + onRootEvent(
490 + event: ReactDOMResponderEvent,
491 + context: ReactDOMResponderContext,
492 + props: FocusWithinProps,
493 + state: FocusState,
494 + ): void {
495 + handleRootEvent(event, context, state, isFocusVisible => {
496 + if (state.isFocusVisible !== isFocusVisible) {
497 + state.isFocusVisible = isFocusVisible;
498 + dispatchFocusWithinVisibleChangeEvent(
499 + context,
500 + props,
501 + state,
502 + isFocusVisible,
503 + );
504 + }
505 + });
506 + },
507 onUnmount(
508 context: ReactDOMResponderContext,
325 - props: FocusProps,
509 + props: FocusWithinProps,
510 state: FocusState,
511 ) {
328 - unmountResponder(context, props, state);
512 + unmountFocusWithinResponder(context, props, state);
513 },
514 onOwnershipChange(
515 context: ReactDOMResponderContext,
332 - props: FocusProps,
516 + props: FocusWithinProps,
517 state: FocusState,
518 ) {
335 - unmountResponder(context, props, state);
519 + unmountFocusWithinResponder(context, props, state);
520 },
521 };
522
339 -export const Focus = React.unstable_createEvent(FocusResponder);
523 +export const FocusWithin = React.unstable_createEvent(FocusWithinResponder);
524
341 -export function useFocus(props: FocusProps): void {
342 - React.unstable_useEvent(Focus, props);
525 +export function useFocusWithin(props: FocusWithinProps): void {
526 + React.unstable_useEvent(FocusWithin, props);
527 }
packages/react-events/src/dom/__tests__/Focus-test.internal.js
+318 -56
@@ -13,10 +13,16 @@ let React;
13 let ReactFeatureFlags;
14 let ReactDOM;
15 let Focus;
16 +let FocusWithin;
17
17 -const createFocusEvent = type => {
18 - const event = document.createEvent('Event');
19 - event.initEvent(type, true, true);
18 +const createEvent = (type, data) => {
19 + const event = document.createEvent('CustomEvent');
20 + event.initCustomEvent(type, true, true);
21 + if (data != null) {
22 + Object.entries(data).forEach(([key, value]) => {
23 + event[key] = value;
24 + });
25 + }
26 return event;
27 };
28
@@ -28,23 +34,13 @@ const createKeyboardEvent = (type, data) => {
34 });
35 };
36
31 -const createPointerEvent = (type, data) => {
32 - const event = document.createEvent('CustomEvent');
33 - event.initCustomEvent(type, true, true);
34 - if (data != null) {
35 - Object.entries(data).forEach(([key, value]) => {
36 - event[key] = value;
37 - });
38 - }
39 - return event;
40 -};
41 -
37 const modulesInit = () => {
38 ReactFeatureFlags = require('shared/ReactFeatureFlags');
39 ReactFeatureFlags.enableFlareAPI = true;
40 React = require('react');
41 ReactDOM = require('react-dom');
42 Focus = require('react-events/focus').Focus;
43 + FocusWithin = require('react-events/focus').FocusWithin;
44 };
45
46 describe('Focus event responder', () => {
@@ -80,8 +76,8 @@ describe('Focus event responder', () => {
76 });
77
78 it('prevents custom events being dispatched', () => {
83 - ref.current.dispatchEvent(createFocusEvent('focus'));
84 - ref.current.dispatchEvent(createFocusEvent('blur'));
79 + ref.current.dispatchEvent(createEvent('focus'));
80 + ref.current.dispatchEvent(createEvent('blur'));
81 expect(onFocus).not.toBeCalled();
82 expect(onBlur).not.toBeCalled();
83 });
@@ -102,8 +98,8 @@ describe('Focus event responder', () => {
98 });
99
100 it('is called after "blur" event', () => {
105 - ref.current.dispatchEvent(createFocusEvent('focus'));
106 - ref.current.dispatchEvent(createFocusEvent('blur'));
101 + ref.current.dispatchEvent(createEvent('focus'));
102 + ref.current.dispatchEvent(createEvent('blur'));
103 expect(onBlur).toHaveBeenCalledTimes(1);
104 });
105 });
@@ -128,50 +124,50 @@ describe('Focus event responder', () => {
124 beforeEach(componentInit);
125
126 it('is called after "focus" event', () => {
131 - ref.current.dispatchEvent(createFocusEvent('focus'));
127 + ref.current.dispatchEvent(createEvent('focus'));
128 expect(onFocus).toHaveBeenCalledTimes(1);
129 });
130
131 it('is not called if descendants of target receive focus', () => {
132 const target = innerRef.current;
137 - target.dispatchEvent(createFocusEvent('focus'));
133 + target.dispatchEvent(createEvent('focus'));
134 expect(onFocus).not.toBeCalled();
135 });
136
137 it('is called with the correct pointerType using pointer events', () => {
138 // Pointer mouse
139 ref.current.dispatchEvent(
144 - createPointerEvent('pointerdown', {
140 + createEvent('pointerdown', {
141 pointerType: 'mouse',
142 }),
143 );
148 - ref.current.dispatchEvent(createFocusEvent('focus'));
144 + ref.current.dispatchEvent(createEvent('focus'));
145 expect(onFocus).toHaveBeenCalledTimes(1);
146 expect(onFocus).toHaveBeenCalledWith(
147 expect.objectContaining({pointerType: 'mouse'}),
148 );
153 - ref.current.dispatchEvent(createFocusEvent('blur'));
149 + ref.current.dispatchEvent(createEvent('blur'));
150
151 // Pointer touch
152 ref.current.dispatchEvent(
157 - createPointerEvent('pointerdown', {
153 + createEvent('pointerdown', {
154 pointerType: 'touch',
155 }),
156 );
161 - ref.current.dispatchEvent(createFocusEvent('focus'));
157 + ref.current.dispatchEvent(createEvent('focus'));
158 expect(onFocus).toHaveBeenCalledTimes(2);
159 expect(onFocus).toHaveBeenCalledWith(
160 expect.objectContaining({pointerType: 'touch'}),
161 );
166 - ref.current.dispatchEvent(createFocusEvent('blur'));
162 + ref.current.dispatchEvent(createEvent('blur'));
163
164 // Pointer pen
165 ref.current.dispatchEvent(
170 - createPointerEvent('pointerdown', {
166 + createEvent('pointerdown', {
167 pointerType: 'pen',
168 }),
169 );
174 - ref.current.dispatchEvent(createFocusEvent('focus'));
170 + ref.current.dispatchEvent(createEvent('focus'));
171 expect(onFocus).toHaveBeenCalledTimes(3);
172 expect(onFocus).toHaveBeenCalledWith(
173 expect.objectContaining({pointerType: 'pen'}),
@@ -180,17 +176,17 @@ describe('Focus event responder', () => {
176
177 it('is called with the correct pointerType without pointer events', () => {
178 // Mouse
183 - ref.current.dispatchEvent(createPointerEvent('mousedown'));
184 - ref.current.dispatchEvent(createFocusEvent('focus'));
179 + ref.current.dispatchEvent(createEvent('mousedown'));
180 + ref.current.dispatchEvent(createEvent('focus'));
181 expect(onFocus).toHaveBeenCalledTimes(1);
182 expect(onFocus).toHaveBeenCalledWith(
183 expect.objectContaining({pointerType: 'mouse'}),
184 );
189 - ref.current.dispatchEvent(createFocusEvent('blur'));
185 + ref.current.dispatchEvent(createEvent('blur'));
186
187 // Touch
192 - ref.current.dispatchEvent(createPointerEvent('touchstart'));
193 - ref.current.dispatchEvent(createFocusEvent('focus'));
188 + ref.current.dispatchEvent(createEvent('touchstart'));
189 + ref.current.dispatchEvent(createEvent('focus'));
190 expect(onFocus).toHaveBeenCalledTimes(2);
191 expect(onFocus).toHaveBeenCalledWith(
192 expect.objectContaining({pointerType: 'touch'}),
@@ -200,11 +196,11 @@ describe('Focus event responder', () => {
196 it('is called with the correct pointerType using a keyboard', () => {
197 // Keyboard tab
198 ref.current.dispatchEvent(
203 - createPointerEvent('keydown', {
199 + createEvent('keydown', {
200 key: 'Tab',
201 }),
202 );
207 - ref.current.dispatchEvent(createFocusEvent('focus'));
203 + ref.current.dispatchEvent(createEvent('focus'));
204 expect(onFocus).toHaveBeenCalledTimes(1);
205 expect(onFocus).toHaveBeenCalledWith(
206 expect.objectContaining({pointerType: 'keyboard'}),
@@ -219,12 +215,12 @@ describe('Focus event responder', () => {
215 componentInit();
216
217 ref.current.dispatchEvent(
222 - createPointerEvent('keydown', {
218 + createEvent('keydown', {
219 key: 'Tab',
220 altKey: true,
221 }),
222 );
227 - ref.current.dispatchEvent(createFocusEvent('focus'));
223 + ref.current.dispatchEvent(createEvent('focus'));
224 expect(onFocus).toHaveBeenCalledTimes(1);
225 expect(onFocus).toHaveBeenCalledWith(
226 expect.objectContaining({
@@ -237,38 +233,51 @@ describe('Focus event responder', () => {
233 });
234
235 describe('onFocusChange', () => {
240 - let onFocusChange, ref;
236 + let onFocusChange, ref, innerRef;
237
238 beforeEach(() => {
239 onFocusChange = jest.fn();
240 ref = React.createRef();
241 + innerRef = React.createRef();
242 const element = (
243 <Focus onFocusChange={onFocusChange}>
247 - <div ref={ref} />
244 + <div ref={ref}>
245 + <div ref={innerRef} />
246 + </div>
247 </Focus>
248 );
249 ReactDOM.render(element, container);
250 });
251
252 it('is called after "blur" and "focus" events', () => {
254 - ref.current.dispatchEvent(createFocusEvent('focus'));
253 + ref.current.dispatchEvent(createEvent('focus'));
254 expect(onFocusChange).toHaveBeenCalledTimes(1);
255 expect(onFocusChange).toHaveBeenCalledWith(true);
257 - ref.current.dispatchEvent(createFocusEvent('blur'));
256 + ref.current.dispatchEvent(createEvent('blur'));
257 expect(onFocusChange).toHaveBeenCalledTimes(2);
258 expect(onFocusChange).toHaveBeenCalledWith(false);
259 });
260 +
261 + it('is not called after "blur" and "focus" events on descendants', () => {
262 + innerRef.current.dispatchEvent(createEvent('focus'));
263 + expect(onFocusChange).toHaveBeenCalledTimes(0);
264 + innerRef.current.dispatchEvent(createEvent('blur'));
265 + expect(onFocusChange).toHaveBeenCalledTimes(0);
266 + });
267 });
268
269 describe('onFocusVisibleChange', () => {
264 - let onFocusVisibleChange, ref;
270 + let onFocusVisibleChange, ref, innerRef;
271
272 beforeEach(() => {
273 onFocusVisibleChange = jest.fn();
274 ref = React.createRef();
275 + innerRef = React.createRef();
276 const element = (
277 <Focus onFocusVisibleChange={onFocusVisibleChange}>
271 - <div ref={ref} />
278 + <div ref={ref}>
279 + <div ref={innerRef} />
280 + </div>
281 </Focus>
282 );
283 ReactDOM.render(element, container);
@@ -277,10 +286,12 @@ describe('Focus event responder', () => {
286 it('is called after "focus" and "blur" if keyboard navigation is active', () => {
287 // use keyboard first
288 container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'}));
280 - ref.current.dispatchEvent(createFocusEvent('focus'));
289 + ref.current.dispatchEvent(createEvent('focus'));
290 expect(onFocusVisibleChange).toHaveBeenCalledTimes(1);
291 expect(onFocusVisibleChange).toHaveBeenCalledWith(true);
283 - ref.current.dispatchEvent(createFocusEvent('blur'));
292 + ref.current.dispatchEvent(
293 + createEvent('blur', {relatedTarget: container}),
294 + );
295 expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
296 expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
297 });
@@ -288,23 +299,37 @@ describe('Focus event responder', () => {
299 it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
300 // use keyboard first
301 container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'}));
291 - ref.current.dispatchEvent(createFocusEvent('focus'));
302 + ref.current.dispatchEvent(createEvent('focus'));
303 expect(onFocusVisibleChange).toHaveBeenCalledTimes(1);
304 expect(onFocusVisibleChange).toHaveBeenCalledWith(true);
305 // then use pointer on the target, focus should no longer be visible
295 - ref.current.dispatchEvent(createPointerEvent('pointerdown'));
306 + ref.current.dispatchEvent(createEvent('pointerdown'));
307 expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
308 expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
309 // onFocusVisibleChange should not be called again
299 - ref.current.dispatchEvent(createFocusEvent('blur'));
310 + ref.current.dispatchEvent(
311 + createEvent('blur', {relatedTarget: container}),
312 + );
313 expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
314 });
315
316 it('is not called after "focus" and "blur" events without keyboard', () => {
304 - ref.current.dispatchEvent(createPointerEvent('pointerdown'));
305 - ref.current.dispatchEvent(createFocusEvent('focus'));
306 - container.dispatchEvent(createPointerEvent('pointerdown'));
307 - ref.current.dispatchEvent(createFocusEvent('blur'));
317 + ref.current.dispatchEvent(createEvent('pointerdown'));
318 + ref.current.dispatchEvent(createEvent('focus'));
319 + container.dispatchEvent(createEvent('pointerdown'));
320 + ref.current.dispatchEvent(
321 + createEvent('blur', {relatedTarget: container}),
322 + );
323 + expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
324 + });
325 +
326 + it('is not called after "blur" and "focus" events on descendants', () => {
327 + container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'}));
328 + innerRef.current.dispatchEvent(createEvent('focus'));
329 + expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
330 + innerRef.current.dispatchEvent(
331 + createEvent('blur', {relatedTarget: container}),
332 + );
333 expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
334 });
335 });
@@ -336,10 +361,10 @@ describe('Focus event responder', () => {
361
362 ReactDOM.render(element, container);
363
339 - outerRef.current.dispatchEvent(createFocusEvent('focus'));
340 - outerRef.current.dispatchEvent(createFocusEvent('blur'));
341 - innerRef.current.dispatchEvent(createFocusEvent('focus'));
342 - innerRef.current.dispatchEvent(createFocusEvent('blur'));
364 + outerRef.current.dispatchEvent(createEvent('focus'));
365 + outerRef.current.dispatchEvent(createEvent('blur'));
366 + innerRef.current.dispatchEvent(createEvent('focus'));
367 + innerRef.current.dispatchEvent(createEvent('blur'));
368 expect(events).toEqual([
369 'outer: onFocus',
370 'outer: onFocusChange',
@@ -357,3 +382,240 @@ describe('Focus event responder', () => {
382 expect(Focus.responder.displayName).toBe('Focus');
383 });
384 });
385 +
386 +describe('FocusWithin event responder', () => {
387 + let container;
388 +
389 + beforeEach(() => {
390 + jest.resetModules();
391 + modulesInit();
392 +
393 + container = document.createElement('div');
394 + document.body.appendChild(container);
395 + });
396 +
397 + afterEach(() => {
398 + ReactDOM.render(null, container);
399 + document.body.removeChild(container);
400 + container = null;
401 + });
402 +
403 + describe('disabled', () => {
404 + let onFocusWithinChange, onFocusWithinVisibleChange, ref;
405 +
406 + beforeEach(() => {
407 + onFocusWithinChange = jest.fn();
408 + onFocusWithinVisibleChange = jest.fn();
409 + ref = React.createRef();
410 + const element = (
411 + <FocusWithin
412 + disabled={true}
413 + onFocusWithinChange={onFocusWithinChange}
414 + onFocusWithinVisibleChange={onFocusWithinVisibleChange}>
415 + <div ref={ref} />
416 + </FocusWithin>
417 + );
418 + ReactDOM.render(element, container);
419 + });
420 +
421 + it('prevents custom events being dispatched', () => {
422 + ref.current.dispatchEvent(createEvent('focus'));
423 + ref.current.dispatchEvent(createEvent('blur'));
424 + expect(onFocusWithinChange).not.toBeCalled();
425 + expect(onFocusWithinVisibleChange).not.toBeCalled();
426 + });
427 + });
428 +
429 + describe('onFocusWithinChange', () => {
430 + let onFocusWithinChange, ref, innerRef, innerRef2;
431 +
432 + beforeEach(() => {
433 + onFocusWithinChange = jest.fn();
434 + ref = React.createRef();
435 + innerRef = React.createRef();
436 + innerRef2 = React.createRef();
437 + const element = (
438 + <FocusWithin onFocusWithinChange={onFocusWithinChange}>
439 + <div ref={ref}>
440 + <div ref={innerRef} />
441 + <div ref={innerRef2} />
442 + </div>
443 + </FocusWithin>
444 + );
445 + ReactDOM.render(element, container);
446 + });
447 +
448 + it('is called after "blur" and "focus" events on focus target', () => {
449 + ref.current.dispatchEvent(createEvent('focus'));
450 + expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
451 + expect(onFocusWithinChange).toHaveBeenCalledWith(true);
452 + ref.current.dispatchEvent(
453 + createEvent('blur', {relatedTarget: container}),
454 + );
455 + expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
456 + expect(onFocusWithinChange).toHaveBeenCalledWith(false);
457 + });
458 +
459 + it('is called after "blur" and "focus" events on descendants', () => {
460 + innerRef.current.dispatchEvent(createEvent('focus'));
461 + expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
462 + expect(onFocusWithinChange).toHaveBeenCalledWith(true);
463 + innerRef.current.dispatchEvent(
464 + createEvent('blur', {relatedTarget: container}),
465 + );
466 + expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
467 + expect(onFocusWithinChange).toHaveBeenCalledWith(false);
468 + });
469 +
470 + it('is only called once when focus moves within and outside the subtree', () => {
471 + // focus shifts into subtree
472 + innerRef.current.dispatchEvent(createEvent('focus'));
473 + expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
474 + expect(onFocusWithinChange).toHaveBeenCalledWith(true);
475 + // focus moves around subtree
476 + innerRef.current.dispatchEvent(
477 + createEvent('blur', {relatedTarget: innerRef2.current}),
478 + );
479 + innerRef2.current.dispatchEvent(createEvent('focus'));
480 + innerRef2.current.dispatchEvent(
481 + createEvent('blur', {relatedTarget: ref.current}),
482 + );
483 + ref.current.dispatchEvent(createEvent('focus'));
484 + ref.current.dispatchEvent(
485 + createEvent('blur', {relatedTarget: innerRef.current}),
486 + );
487 + expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
488 + // focus shifts outside subtree
489 + innerRef.current.dispatchEvent(
490 + createEvent('blur', {relatedTarget: container}),
491 + );
492 + expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
493 + expect(onFocusWithinChange).toHaveBeenCalledWith(false);
494 + });
495 + });
496 +
497 + describe('onFocusWithinVisibleChange', () => {
498 + let onFocusWithinVisibleChange, ref, innerRef, innerRef2;
499 +
500 + beforeEach(() => {
501 + onFocusWithinVisibleChange = jest.fn();
502 + ref = React.createRef();
503 + innerRef = React.createRef();
504 + innerRef2 = React.createRef();
505 + const element = (
506 + <FocusWithin onFocusWithinVisibleChange={onFocusWithinVisibleChange}>
507 + <div ref={ref}>
508 + <div ref={innerRef} />
509 + <div ref={innerRef2} />
510 + </div>
511 + </FocusWithin>
512 + );
513 + ReactDOM.render(element, container);
514 + });
515 +
516 + it('is called after "focus" and "blur" on focus target if keyboard was used', () => {
517 + // use keyboard first
518 + container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'}));
519 + ref.current.dispatchEvent(createEvent('focus'));
520 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
521 + expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
522 + ref.current.dispatchEvent(
523 + createEvent('blur', {relatedTarget: container}),
524 + );
525 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
526 + expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
527 + });
528 +
529 + it('is called after "focus" and "blur" on descendants if keyboard was used', () => {
530 + // use keyboard first
531 + container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'}));
532 + innerRef.current.dispatchEvent(createEvent('focus'));
533 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
534 + expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
535 + innerRef.current.dispatchEvent(
536 + createEvent('blur', {relatedTarget: container}),
537 + );
538 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
539 + expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
540 + });
541 +
542 + it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
543 + // use keyboard first
544 + ref.current.dispatchEvent(createEvent('focus'));
545 + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'}));
546 + ref.current.dispatchEvent(
547 + createEvent('blur', {relatedTarget: innerRef.current}),
548 + );
549 + innerRef.current.dispatchEvent(createEvent('focus'));
550 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
551 + expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
552 + // then use pointer on the next target, focus should no longer be visible
553 + innerRef2.current.dispatchEvent(createEvent('pointerdown'));
554 + innerRef.current.dispatchEvent(
555 + createEvent('blur', {relatedTarget: innerRef2.current}),
556 + );
557 + innerRef2.current.dispatchEvent(createEvent('focus'));
558 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
559 + expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
560 + // then use keyboard again
561 + innerRef2.current.dispatchEvent(
562 + createKeyboardEvent('keydown', {key: 'Tab', shiftKey: true}),
563 + );
564 + innerRef2.current.dispatchEvent(
565 + createEvent('blur', {relatedTarget: innerRef.current}),
566 + );
567 + innerRef.current.dispatchEvent(createEvent('focus'));
568 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(3);
569 + expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
570 + // then use pointer on the target, focus should no longer be visible
571 + innerRef.current.dispatchEvent(createEvent('pointerdown'));
572 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4);
573 + expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
574 + // onFocusVisibleChange should not be called again
575 + innerRef.current.dispatchEvent(
576 + createEvent('blur', {relatedTarget: container}),
577 + );
578 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4);
579 + });
580 +
581 + it('is not called after "focus" and "blur" events without keyboard', () => {
582 + innerRef.current.dispatchEvent(createEvent('pointerdown'));
583 + innerRef.current.dispatchEvent(createEvent('focus'));
584 + container.dispatchEvent(createEvent('pointerdown'));
585 + innerRef.current.dispatchEvent(
586 + createEvent('blur', {relatedTarget: container}),
587 + );
588 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(0);
589 + });
590 +
591 + it('is only called once when focus moves within and outside the subtree', () => {
592 + // focus shifts into subtree
593 + innerRef.current.dispatchEvent(createEvent('focus'));
594 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
595 + expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
596 + // focus moves around subtree
597 + innerRef.current.dispatchEvent(
598 + createEvent('blur', {relatedTarget: innerRef2.current}),
599 + );
600 + innerRef2.current.dispatchEvent(createEvent('focus'));
601 + innerRef2.current.dispatchEvent(
602 + createEvent('blur', {relatedTarget: ref.current}),
603 + );
604 + ref.current.dispatchEvent(createEvent('focus'));
605 + ref.current.dispatchEvent(
606 + createEvent('blur', {relatedTarget: innerRef.current}),
607 + );
608 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
609 + // focus shifts outside subtree
610 + innerRef.current.dispatchEvent(
611 + createEvent('blur', {relatedTarget: container}),
612 + );
613 + expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
614 + expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
615 + });
616 + });
617 +
618 + it('expect displayName to show up for event component', () => {
619 + expect(FocusWithin.responder.displayName).toBe('FocusWithin');
620 + });
621 +});