@wordpress/compose 5.18.0 → 5.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,72 +1,25 @@
1
1
  /**
2
- * @typedef {import('react').SyntheticEvent} SyntheticEvent
3
- */
4
- /**
5
- * @callback EventCallback
6
- * @param {SyntheticEvent} event input related event.
7
- */
8
- /**
9
- * @typedef FocusOutsideReactElement
10
- * @property {EventCallback} handleFocusOutside callback for a focus outside event.
11
- */
12
- /**
13
- * @typedef {import('react').MutableRefObject<FocusOutsideReactElement | undefined>} FocusOutsideRef
14
- */
15
- /**
16
- * @typedef {Object} FocusOutsideReturnValue
17
- * @property {EventCallback} onFocus An event handler for focus events.
18
- * @property {EventCallback} onBlur An event handler for blur events.
19
- * @property {EventCallback} onMouseDown An event handler for mouse down events.
20
- * @property {EventCallback} onMouseUp An event handler for mouse up events.
21
- * @property {EventCallback} onTouchStart An event handler for touch start events.
22
- * @property {EventCallback} onTouchEnd An event handler for touch end events.
2
+ * External dependencies
23
3
  */
4
+ import type { FocusEventHandler, MouseEventHandler, TouchEventHandler, FocusEvent } from 'react';
5
+ declare type UseFocusOutsideReturn = {
6
+ onFocus: FocusEventHandler;
7
+ onMouseDown: MouseEventHandler;
8
+ onMouseUp: MouseEventHandler;
9
+ onTouchStart: TouchEventHandler;
10
+ onTouchEnd: TouchEventHandler;
11
+ onBlur: FocusEventHandler;
12
+ };
24
13
  /**
25
14
  * A react hook that can be used to check whether focus has moved outside the
26
15
  * element the event handlers are bound to.
27
16
  *
28
- * @param {EventCallback} onFocusOutside A callback triggered when focus moves outside
29
- * the element the event handlers are bound to.
17
+ * @param onFocusOutside A callback triggered when focus moves outside
18
+ * the element the event handlers are bound to.
30
19
  *
31
- * @return {FocusOutsideReturnValue} An object containing event handlers. Bind the event handlers
32
- * to a wrapping element element to capture when focus moves
33
- * outside that element.
20
+ * @return An object containing event handlers. Bind the event handlers to a
21
+ * wrapping element element to capture when focus moves outside that element.
34
22
  */
35
- export default function useFocusOutside(onFocusOutside: EventCallback): FocusOutsideReturnValue;
36
- export type FocusNormalizedButton = HTMLButtonElement | HTMLLinkElement | HTMLInputElement;
37
- export type SyntheticEvent = import('react').SyntheticEvent;
38
- export type EventCallback = (event: SyntheticEvent) => any;
39
- export type FocusOutsideReactElement = {
40
- /**
41
- * callback for a focus outside event.
42
- */
43
- handleFocusOutside: EventCallback;
44
- };
45
- export type FocusOutsideRef = import('react').MutableRefObject<FocusOutsideReactElement | undefined>;
46
- export type FocusOutsideReturnValue = {
47
- /**
48
- * An event handler for focus events.
49
- */
50
- onFocus: EventCallback;
51
- /**
52
- * An event handler for blur events.
53
- */
54
- onBlur: EventCallback;
55
- /**
56
- * An event handler for mouse down events.
57
- */
58
- onMouseDown: EventCallback;
59
- /**
60
- * An event handler for mouse up events.
61
- */
62
- onMouseUp: EventCallback;
63
- /**
64
- * An event handler for touch start events.
65
- */
66
- onTouchStart: EventCallback;
67
- /**
68
- * An event handler for touch end events.
69
- */
70
- onTouchEnd: EventCallback;
71
- };
23
+ export default function useFocusOutside(onFocusOutside: (event: FocusEvent) => void): UseFocusOutsideReturn;
24
+ export {};
72
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-focus-outside/index.js"],"names":[],"mappings":"AAgDA;;GAEG;AAEH;;;GAGG;AAEH;;;GAGG;AAEH;;GAEG;AAEH;;;;;;;;GAQG;AAEH;;;;;;;;;;GAUG;AACH,wDAPW,aAAa,GAGZ,uBAAuB,CAqGlC;oCA1KY,iBAAiB,GAAG,eAAe,GAAG,gBAAgB;6BAmCtD,OAAO,OAAO,EAAE,cAAc;oCAKhC,cAAc;;;;;wBAKX,aAAa;;8BAId,OAAO,OAAO,EAAE,gBAAgB,CAAC,wBAAwB,GAAG,SAAS,CAAC;;;;;aAKrE,aAAa;;;;YACb,aAAa;;;;iBACb,aAAa;;;;eACb,aAAa;;;;kBACb,aAAa;;;;gBACb,aAAa"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-focus-outside/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EACX,iBAAiB,EAEjB,iBAAiB,EACjB,iBAAiB,EACjB,UAAU,EAGV,MAAM,OAAO,CAAC;AAqDf,aAAK,qBAAqB,GAAG;IAC5B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,WAAW,EAAE,iBAAiB,CAAC;IAC/B,SAAS,EAAE,iBAAiB,CAAC;IAC7B,YAAY,EAAE,iBAAiB,CAAC;IAChC,UAAU,EAAE,iBAAiB,CAAC;IAC9B,MAAM,EAAE,iBAAiB,CAAC;CAC1B,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,OAAO,UAAU,eAAe,CACtC,cAAc,EAAE,CAAE,KAAK,EAAE,UAAU,KAAM,IAAI,GAC3C,qBAAqB,CA4FvB"}
@@ -1,8 +1,12 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import type { RefCallback } from 'react';
1
5
  /**
2
6
  * Dispatches a bubbling focus event when the iframe receives focus. Use
3
7
  * `onFocus` as usual on the iframe or a parent element.
4
8
  *
5
- * @return {Object} Ref to pass to the iframe.
9
+ * @return Ref to pass to the iframe.
6
10
  */
7
- export default function useFocusableIframe(): Object;
11
+ export default function useFocusableIframe(): RefCallback<HTMLIFrameElement>;
8
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-focusable-iframe/index.js"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,8CAFY,MAAM,CAwBjB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-focusable-iframe/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAOzC;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,IAAI,WAAW,CAAE,iBAAiB,CAAE,CAsB7E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/compose",
3
- "version": "5.18.0",
3
+ "version": "5.19.0",
4
4
  "description": "WordPress higher-order components (HOCs).",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -31,12 +31,12 @@
31
31
  "dependencies": {
32
32
  "@babel/runtime": "^7.16.0",
33
33
  "@types/mousetrap": "^1.6.8",
34
- "@wordpress/deprecated": "^3.20.0",
35
- "@wordpress/dom": "^3.20.0",
36
- "@wordpress/element": "^4.18.0",
37
- "@wordpress/is-shallow-equal": "^4.20.0",
38
- "@wordpress/keycodes": "^3.20.0",
39
- "@wordpress/priority-queue": "^2.20.0",
34
+ "@wordpress/deprecated": "^3.21.0",
35
+ "@wordpress/dom": "^3.21.0",
36
+ "@wordpress/element": "^4.19.0",
37
+ "@wordpress/is-shallow-equal": "^4.21.0",
38
+ "@wordpress/keycodes": "^3.21.0",
39
+ "@wordpress/priority-queue": "^2.21.0",
40
40
  "change-case": "^4.1.2",
41
41
  "clipboard": "^2.0.8",
42
42
  "mousetrap": "^1.6.5",
@@ -48,5 +48,5 @@
48
48
  "publishConfig": {
49
49
  "access": "public"
50
50
  },
51
- "gitHead": "a2ff0e6471c88436dad0287beb88d1729aa6f5dd"
51
+ "gitHead": "511f4cc1f0138641bc4394bc1cf36e833109c791"
52
52
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import type { KeyboardEvent, RefCallback, SyntheticEvent } from 'react';
4
+ import type { RefCallback, SyntheticEvent } from 'react';
5
5
 
6
6
  /**
7
7
  * WordPress dependencies
@@ -17,7 +17,6 @@ import useFocusOnMount from '../use-focus-on-mount';
17
17
  import useFocusReturn from '../use-focus-return';
18
18
  import useFocusOutside from '../use-focus-outside';
19
19
  import useMergeRefs from '../use-merge-refs';
20
- import type { FocusOutsideReturnValue } from '../use-focus-outside';
21
20
 
22
21
  type DialogOptions = {
23
22
  focusOnMount?: Parameters< typeof useFocusOnMount >[ 0 ];
@@ -35,7 +34,7 @@ type DialogOptions = {
35
34
 
36
35
  type useDialogReturn = [
37
36
  RefCallback< HTMLElement >,
38
- FocusOutsideReturnValue & Pick< HTMLElement, 'tabIndex' >
37
+ ReturnType< typeof useFocusOutside > & Pick< HTMLElement, 'tabIndex' >
39
38
  ];
40
39
 
41
40
  /**
@@ -64,7 +63,7 @@ function useDialog( options: DialogOptions ): useDialogReturn {
64
63
  currentOptions.current.onClose();
65
64
  }
66
65
  } );
67
- const closeOnEscapeRef = useCallback( ( node ) => {
66
+ const closeOnEscapeRef = useCallback( ( node: HTMLElement ) => {
68
67
  if ( ! node ) {
69
68
  return;
70
69
  }
@@ -1,3 +1,16 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import type {
5
+ FocusEventHandler,
6
+ EventHandler,
7
+ MouseEventHandler,
8
+ TouchEventHandler,
9
+ FocusEvent,
10
+ MouseEvent,
11
+ TouchEvent,
12
+ } from 'react';
13
+
1
14
  /**
2
15
  * WordPress dependencies
3
16
  */
@@ -6,28 +19,32 @@ import { useCallback, useEffect, useRef } from '@wordpress/element';
6
19
  /**
7
20
  * Input types which are classified as button types, for use in considering
8
21
  * whether element is a (focus-normalized) button.
9
- *
10
- * @type {string[]}
11
22
  */
12
23
  const INPUT_BUTTON_TYPES = [ 'button', 'submit' ];
13
24
 
14
25
  /**
15
- * @typedef {HTMLButtonElement | HTMLLinkElement | HTMLInputElement} FocusNormalizedButton
26
+ * List of HTML button elements subject to focus normalization
27
+ *
28
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
16
29
  */
30
+ type FocusNormalizedButton =
31
+ | HTMLButtonElement
32
+ | HTMLLinkElement
33
+ | HTMLInputElement;
17
34
 
18
- // Disable reason: Rule doesn't support predicate return types.
19
- /* eslint-disable jsdoc/valid-types */
20
35
  /**
21
36
  * Returns true if the given element is a button element subject to focus
22
37
  * normalization, or false otherwise.
23
38
  *
24
39
  * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
25
40
  *
26
- * @param {EventTarget} eventTarget The target from a mouse or touch event.
41
+ * @param eventTarget The target from a mouse or touch event.
27
42
  *
28
- * @return {eventTarget is FocusNormalizedButton} Whether element is a button.
43
+ * @return Whether the element is a button element subject to focus normalization.
29
44
  */
30
- function isFocusNormalizedButton( eventTarget ) {
45
+ function isFocusNormalizedButton(
46
+ eventTarget: EventTarget
47
+ ): eventTarget is FocusNormalizedButton {
31
48
  if ( ! ( eventTarget instanceof window.HTMLElement ) ) {
32
49
  return false;
33
50
  }
@@ -38,54 +55,35 @@ function isFocusNormalizedButton( eventTarget ) {
38
55
 
39
56
  case 'INPUT':
40
57
  return INPUT_BUTTON_TYPES.includes(
41
- /** @type {HTMLInputElement} */ ( eventTarget ).type
58
+ ( eventTarget as HTMLInputElement ).type
42
59
  );
43
60
  }
44
61
 
45
62
  return false;
46
63
  }
47
- /* eslint-enable jsdoc/valid-types */
48
-
49
- /**
50
- * @typedef {import('react').SyntheticEvent} SyntheticEvent
51
- */
52
64
 
53
- /**
54
- * @callback EventCallback
55
- * @param {SyntheticEvent} event input related event.
56
- */
57
-
58
- /**
59
- * @typedef FocusOutsideReactElement
60
- * @property {EventCallback} handleFocusOutside callback for a focus outside event.
61
- */
62
-
63
- /**
64
- * @typedef {import('react').MutableRefObject<FocusOutsideReactElement | undefined>} FocusOutsideRef
65
- */
66
-
67
- /**
68
- * @typedef {Object} FocusOutsideReturnValue
69
- * @property {EventCallback} onFocus An event handler for focus events.
70
- * @property {EventCallback} onBlur An event handler for blur events.
71
- * @property {EventCallback} onMouseDown An event handler for mouse down events.
72
- * @property {EventCallback} onMouseUp An event handler for mouse up events.
73
- * @property {EventCallback} onTouchStart An event handler for touch start events.
74
- * @property {EventCallback} onTouchEnd An event handler for touch end events.
75
- */
65
+ type UseFocusOutsideReturn = {
66
+ onFocus: FocusEventHandler;
67
+ onMouseDown: MouseEventHandler;
68
+ onMouseUp: MouseEventHandler;
69
+ onTouchStart: TouchEventHandler;
70
+ onTouchEnd: TouchEventHandler;
71
+ onBlur: FocusEventHandler;
72
+ };
76
73
 
77
74
  /**
78
75
  * A react hook that can be used to check whether focus has moved outside the
79
76
  * element the event handlers are bound to.
80
77
  *
81
- * @param {EventCallback} onFocusOutside A callback triggered when focus moves outside
82
- * the element the event handlers are bound to.
78
+ * @param onFocusOutside A callback triggered when focus moves outside
79
+ * the element the event handlers are bound to.
83
80
  *
84
- * @return {FocusOutsideReturnValue} An object containing event handlers. Bind the event handlers
85
- * to a wrapping element element to capture when focus moves
86
- * outside that element.
81
+ * @return An object containing event handlers. Bind the event handlers to a
82
+ * wrapping element element to capture when focus moves outside that element.
87
83
  */
88
- export default function useFocusOutside( onFocusOutside ) {
84
+ export default function useFocusOutside(
85
+ onFocusOutside: ( event: FocusEvent ) => void
86
+ ): UseFocusOutsideReturn {
89
87
  const currentOnFocusOutside = useRef( onFocusOutside );
90
88
  useEffect( () => {
91
89
  currentOnFocusOutside.current = onFocusOutside;
@@ -93,10 +91,7 @@ export default function useFocusOutside( onFocusOutside ) {
93
91
 
94
92
  const preventBlurCheck = useRef( false );
95
93
 
96
- /**
97
- * @type {import('react').MutableRefObject<number | undefined>}
98
- */
99
- const blurCheckTimeoutId = useRef();
94
+ const blurCheckTimeoutId = useRef< number | undefined >();
100
95
 
101
96
  /**
102
97
  * Cancel a blur check timeout.
@@ -124,20 +119,20 @@ export default function useFocusOutside( onFocusOutside ) {
124
119
  * button elements when clicked, while others do. The logic here
125
120
  * intends to normalize this as treating click on buttons as focus.
126
121
  *
122
+ * @param event
127
123
  * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
128
- *
129
- * @param {SyntheticEvent} event Event for mousedown or mouseup.
130
124
  */
131
- const normalizeButtonFocus = useCallback( ( event ) => {
132
- const { type, target } = event;
133
- const isInteractionEnd = [ 'mouseup', 'touchend' ].includes( type );
134
-
135
- if ( isInteractionEnd ) {
136
- preventBlurCheck.current = false;
137
- } else if ( isFocusNormalizedButton( target ) ) {
138
- preventBlurCheck.current = true;
139
- }
140
- }, [] );
125
+ const normalizeButtonFocus: EventHandler< MouseEvent | TouchEvent > =
126
+ useCallback( ( event ) => {
127
+ const { type, target } = event;
128
+ const isInteractionEnd = [ 'mouseup', 'touchend' ].includes( type );
129
+
130
+ if ( isInteractionEnd ) {
131
+ preventBlurCheck.current = false;
132
+ } else if ( isFocusNormalizedButton( target ) ) {
133
+ preventBlurCheck.current = true;
134
+ }
135
+ }, [] );
141
136
 
142
137
  /**
143
138
  * A callback triggered when a blur event occurs on the element the handler
@@ -145,10 +140,8 @@ export default function useFocusOutside( onFocusOutside ) {
145
140
  *
146
141
  * Calls the `onFocusOutside` callback in an immediate timeout if focus has
147
142
  * move outside the bound element and is still within the document.
148
- *
149
- * @param {SyntheticEvent} event Blur event.
150
143
  */
151
- const queueBlurCheck = useCallback( ( event ) => {
144
+ const queueBlurCheck: FocusEventHandler = useCallback( ( event ) => {
152
145
  // React does not allow using an event reference asynchronously
153
146
  // due to recycling behavior, except when explicitly persisted.
154
147
  event.persist();
@@ -4,120 +4,149 @@
4
4
  import { render, screen } from '@testing-library/react';
5
5
  import userEvent from '@testing-library/user-event';
6
6
 
7
- /**
8
- * WordPress dependencies
9
- */
10
- import { Component } from '@wordpress/element';
11
-
12
7
  /**
13
8
  * Internal dependencies
14
9
  */
15
10
  import useFocusOutside from '../';
16
11
 
17
- let onFocusOutside;
18
-
19
- describe( 'useFocusOutside', () => {
20
- let origHasFocus;
21
-
22
- const FocusOutsideComponent = ( { onFocusOutside: callback } ) => (
12
+ const FocusOutsideComponent = ( { onFocusOutside: callback } ) => (
13
+ <div>
14
+ { /* Wrapper */ }
23
15
  <div { ...useFocusOutside( callback ) }>
24
16
  <input type="text" />
25
- <input type="button" />
17
+ <button>Button inside the wrapper</button>
18
+ <iframe title="test-iframe">
19
+ <button>Inside the iframe</button>
20
+ </iframe>
26
21
  </div>
27
- );
28
22
 
29
- class TestComponent extends Component {
30
- render() {
31
- return <FocusOutsideComponent { ...this.props } />;
32
- }
33
- }
23
+ <button>Button outside the wrapper</button>
24
+ </div>
25
+ );
34
26
 
35
- beforeEach( () => {
36
- // Mock document.hasFocus() to always be true for testing
37
- // note: we overide this for some tests.
38
- origHasFocus = document.hasFocus;
39
- document.hasFocus = () => true;
40
-
41
- onFocusOutside = jest.fn();
42
- } );
43
-
44
- afterEach( () => {
45
- document.hasFocus = origHasFocus;
46
- } );
47
-
48
- it( 'should not call handler if focus shifts to element within component', () => {
49
- render( <TestComponent onFocusOutside={ onFocusOutside } /> );
27
+ describe( 'useFocusOutside', () => {
28
+ it( 'should not call handler if focus shifts to element within component', async () => {
29
+ const mockOnFocusOutside = jest.fn();
30
+ const user = userEvent.setup( {
31
+ advanceTimers: jest.advanceTimersByTime,
32
+ } );
50
33
 
51
- const input = screen.getByRole( 'textbox' );
52
- const button = screen.getByRole( 'button' );
34
+ render(
35
+ <FocusOutsideComponent onFocusOutside={ mockOnFocusOutside } />
36
+ );
53
37
 
54
- input.focus();
55
- input.blur();
56
- button.focus();
38
+ // Tab through the interactive elements inside the wrapper,
39
+ // causing multiple focus/blur events.
40
+ await user.tab();
41
+ expect( screen.getByRole( 'textbox' ) ).toHaveFocus();
57
42
 
58
- jest.runAllTimers();
43
+ await user.tab();
44
+ expect(
45
+ screen.getByRole( 'button', { name: 'Button inside the wrapper' } )
46
+ ).toHaveFocus();
59
47
 
60
- expect( onFocusOutside ).not.toHaveBeenCalled();
48
+ expect( mockOnFocusOutside ).not.toHaveBeenCalled();
61
49
  } );
62
50
 
63
51
  it( 'should not call handler if focus transitions via click to button', async () => {
52
+ const mockOnFocusOutside = jest.fn();
64
53
  const user = userEvent.setup( {
65
54
  advanceTimers: jest.advanceTimersByTime,
66
55
  } );
67
- render( <TestComponent onFocusOutside={ onFocusOutside } /> );
68
-
69
- const input = screen.getByRole( 'textbox' );
70
- const button = screen.getByRole( 'button' );
71
56
 
72
- input.focus();
73
- await user.click( button );
57
+ render(
58
+ <FocusOutsideComponent onFocusOutside={ mockOnFocusOutside } />
59
+ );
74
60
 
75
- jest.runAllTimers();
61
+ // Click the input and the button, causing multiple focus/blur events.
62
+ await user.click( screen.getByRole( 'textbox' ) );
63
+ await user.click(
64
+ screen.getByRole( 'button', { name: 'Button inside the wrapper' } )
65
+ );
76
66
 
77
- expect( onFocusOutside ).not.toHaveBeenCalled();
67
+ expect( mockOnFocusOutside ).not.toHaveBeenCalled();
78
68
  } );
79
69
 
80
- it( 'should call handler if focus doesn’t shift to element within component', () => {
81
- render( <TestComponent onFocusOutside={ onFocusOutside } /> );
70
+ it( 'should call handler if focus shifts to element outside component', async () => {
71
+ const mockOnFocusOutside = jest.fn();
72
+ const user = userEvent.setup( {
73
+ advanceTimers: jest.advanceTimersByTime,
74
+ } );
75
+
76
+ render(
77
+ <FocusOutsideComponent onFocusOutside={ mockOnFocusOutside } />
78
+ );
82
79
 
83
- const input = screen.getByRole( 'textbox' );
84
- input.focus();
85
- input.blur();
80
+ // Click and focus button inside the wrapper
81
+ await user.click(
82
+ screen.getByRole( 'button', { name: 'Button inside the wrapper' } )
83
+ );
86
84
 
87
- jest.runAllTimers();
85
+ expect( mockOnFocusOutside ).not.toHaveBeenCalled();
88
86
 
89
- expect( onFocusOutside ).toHaveBeenCalled();
90
- } );
87
+ // Click and focus button outside the wrapper
88
+ await user.click(
89
+ screen.getByRole( 'button', { name: 'Button outside the wrapper' } )
90
+ );
91
91
 
92
- it( 'should not call handler if focus shifts outside the component when the document does not have focus', () => {
93
- render( <TestComponent onFocusOutside={ onFocusOutside } /> );
92
+ expect( mockOnFocusOutside ).toHaveBeenCalled();
93
+ } );
94
94
 
95
+ it( 'should not call handler if focus shifts outside the component when the document does not have focus', async () => {
95
96
  // Force document.hasFocus() to return false to simulate the window/document losing focus
96
97
  // See https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus.
97
- document.hasFocus = () => false;
98
+ const mockedDocumentHasFocus = jest
99
+ .spyOn( document, 'hasFocus' )
100
+ .mockImplementation( () => false );
101
+ const mockOnFocusOutside = jest.fn();
102
+ const user = userEvent.setup( {
103
+ advanceTimers: jest.advanceTimersByTime,
104
+ } );
98
105
 
99
- const input = screen.getByRole( 'textbox' );
100
- input.focus();
101
- input.blur();
106
+ render(
107
+ <FocusOutsideComponent onFocusOutside={ mockOnFocusOutside } />
108
+ );
102
109
 
103
- jest.runAllTimers();
110
+ // Click and focus button inside the wrapper, then click and focus
111
+ // a button outside the wrapper.
112
+ await user.click(
113
+ screen.getByRole( 'button', { name: 'Button inside the wrapper' } )
114
+ );
115
+ await user.click(
116
+ screen.getByRole( 'button', { name: 'Button outside the wrapper' } )
117
+ );
118
+
119
+ // The handler is not called thanks to the mocked return value of
120
+ // `document.hasFocus()`
121
+ expect( mockOnFocusOutside ).not.toHaveBeenCalled();
104
122
 
105
- expect( onFocusOutside ).not.toHaveBeenCalled();
123
+ // Restore the `document.hasFocus()` function to its original implementation.
124
+ mockedDocumentHasFocus.mockRestore();
106
125
  } );
107
126
 
108
- it( 'should cancel check when unmounting while queued', () => {
109
- const { rerender } = render(
110
- <TestComponent onFocusOutside={ onFocusOutside } />
127
+ it( 'should cancel check when unmounting while queued', async () => {
128
+ const mockOnFocusOutside = jest.fn();
129
+ const user = userEvent.setup( {
130
+ advanceTimers: jest.advanceTimersByTime,
131
+ } );
132
+
133
+ const { unmount } = render(
134
+ <FocusOutsideComponent onFocusOutside={ mockOnFocusOutside } />
111
135
  );
112
136
 
113
- const input = screen.getByRole( 'textbox' );
114
- input.focus();
115
- input.blur();
137
+ // Click and focus button inside the wrapper.
138
+ const button = screen.getByRole( 'button', {
139
+ name: 'Button inside the wrapper',
140
+ } );
141
+ await user.click( button );
116
142
 
117
- rerender( <div /> );
143
+ // Simulate a blur event and the wrapper unmounting while the blur event
144
+ // handler is queued
145
+ button.blur();
146
+ unmount();
118
147
 
119
148
  jest.runAllTimers();
120
149
 
121
- expect( onFocusOutside ).not.toHaveBeenCalled();
150
+ expect( mockOnFocusOutside ).not.toHaveBeenCalled();
122
151
  } );
123
152
  } );
@@ -1,3 +1,8 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import type { RefCallback } from 'react';
5
+
1
6
  /**
2
7
  * Internal dependencies
3
8
  */
@@ -7,9 +12,9 @@ import useRefEffect from '../use-ref-effect';
7
12
  * Dispatches a bubbling focus event when the iframe receives focus. Use
8
13
  * `onFocus` as usual on the iframe or a parent element.
9
14
  *
10
- * @return {Object} Ref to pass to the iframe.
15
+ * @return Ref to pass to the iframe.
11
16
  */
12
- export default function useFocusableIframe() {
17
+ export default function useFocusableIframe(): RefCallback< HTMLIFrameElement > {
13
18
  return useRefEffect( ( element ) => {
14
19
  const { ownerDocument } = element;
15
20
  if ( ! ownerDocument ) return;
@@ -22,7 +27,7 @@ export default function useFocusableIframe() {
22
27
  */
23
28
  function checkFocus() {
24
29
  if ( ownerDocument && ownerDocument.activeElement === element ) {
25
- /** @type {HTMLElement} */ ( element ).focus();
30
+ ( element as HTMLElement ).focus();
26
31
  }
27
32
  }
28
33