@wordpress/compose 5.17.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +4 -1
  3. package/build/hooks/use-dialog/index.js.map +1 -1
  4. package/build/hooks/use-disabled/index.js +10 -98
  5. package/build/hooks/use-disabled/index.js.map +1 -1
  6. package/build/hooks/use-focus-outside/index.js +15 -56
  7. package/build/hooks/use-focus-outside/index.js.map +1 -1
  8. package/build/hooks/use-focusable-iframe/index.js +5 -2
  9. package/build/hooks/use-focusable-iframe/index.js.map +1 -1
  10. package/build/hooks/use-resize-observer/index.native.js +1 -0
  11. package/build/hooks/use-resize-observer/index.native.js.map +1 -1
  12. package/build-module/hooks/use-dialog/index.js.map +1 -1
  13. package/build-module/hooks/use-disabled/index.js +10 -97
  14. package/build-module/hooks/use-disabled/index.js.map +1 -1
  15. package/build-module/hooks/use-focus-outside/index.js +15 -56
  16. package/build-module/hooks/use-focus-outside/index.js.map +1 -1
  17. package/build-module/hooks/use-focusable-iframe/index.js +5 -2
  18. package/build-module/hooks/use-focusable-iframe/index.js.map +1 -1
  19. package/build-module/hooks/use-resize-observer/index.native.js +1 -0
  20. package/build-module/hooks/use-resize-observer/index.native.js.map +1 -1
  21. package/build-types/hooks/use-dialog/index.d.ts +2 -2
  22. package/build-types/hooks/use-dialog/index.d.ts.map +1 -1
  23. package/build-types/hooks/use-disabled/index.d.ts +4 -1
  24. package/build-types/hooks/use-disabled/index.d.ts.map +1 -1
  25. package/build-types/hooks/use-focus-outside/index.d.ts +16 -63
  26. package/build-types/hooks/use-focus-outside/index.d.ts.map +1 -1
  27. package/build-types/hooks/use-focusable-iframe/index.d.ts +6 -2
  28. package/build-types/hooks/use-focusable-iframe/index.d.ts.map +1 -1
  29. package/package.json +8 -8
  30. package/src/higher-order/with-global-events/test/index.js +18 -29
  31. package/src/higher-order/with-state/test/index.js +13 -26
  32. package/src/hooks/use-dialog/index.ts +3 -4
  33. package/src/hooks/use-disabled/index.ts +81 -0
  34. package/src/hooks/use-disabled/test/index.js +4 -36
  35. package/src/hooks/use-focus-outside/{index.js → index.ts} +55 -62
  36. package/src/hooks/use-focus-outside/test/index.js +112 -84
  37. package/src/hooks/use-focusable-iframe/{index.js → index.ts} +8 -3
  38. package/src/hooks/use-instance-id/test/index.js +9 -16
  39. package/src/hooks/use-media-query/test/index.js +33 -53
  40. package/src/hooks/use-resize-observer/index.native.js +5 -1
  41. package/src/hooks/use-resize-observer/test/index.native.js +18 -22
  42. package/src/hooks/use-viewport-match/test/index.js +78 -78
  43. package/tsconfig.tsbuildinfo +1 -1
  44. package/src/hooks/use-disabled/index.js +0 -197
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import TestRenderer from 'react-test-renderer';
4
+ import { render, screen } from '@testing-library/react';
5
5
 
6
6
  /**
7
7
  * WordPress dependencies
@@ -30,8 +30,6 @@ jest.mock( '../listener', () => {
30
30
  } );
31
31
 
32
32
  describe( 'withGlobalEvents', () => {
33
- let wrapper;
34
-
35
33
  class OriginalComponent extends Component {
36
34
  handleResize( event ) {
37
35
  this.props.onResize( event );
@@ -50,51 +48,42 @@ describe( 'withGlobalEvents', () => {
50
48
  jest.clearAllMocks();
51
49
  } );
52
50
 
53
- afterEach( () => {
54
- if ( wrapper ) {
55
- wrapper.unmount();
56
- wrapper = null;
57
- }
58
- } );
59
-
60
- function mountEnhancedComponent( props = {} ) {
51
+ it( 'renders with original component', () => {
61
52
  const EnhancedComponent = withGlobalEvents( {
62
53
  resize: 'handleResize',
63
54
  } )( OriginalComponent );
64
55
 
65
- props.ref = () => {};
66
-
67
- wrapper = TestRenderer.create(
68
- <EnhancedComponent { ...props }>Hello</EnhancedComponent>
69
- );
70
- }
71
-
72
- it( 'renders with original component', () => {
73
- mountEnhancedComponent();
56
+ render( <EnhancedComponent ref={ () => {} }>Hello</EnhancedComponent> );
74
57
 
75
58
  expect( console ).toHaveWarned();
76
- expect( wrapper.root.findByType( 'div' ).children[ 0 ] ).toBe(
77
- 'Hello'
78
- );
59
+ expect( screen.getByText( 'Hello' ) ).toBeVisible();
79
60
  } );
80
61
 
81
62
  it( 'binds events from passed object', () => {
82
- mountEnhancedComponent();
63
+ const EnhancedComponent = withGlobalEvents( {
64
+ resize: 'handleResize',
65
+ } )( OriginalComponent );
83
66
 
84
- // Get the HOC wrapper instance.
85
- const hocInstance =
86
- wrapper.root.findByType( OriginalComponent ).parent.instance;
67
+ render( <EnhancedComponent ref={ () => {} }>Hello</EnhancedComponent> );
87
68
 
88
69
  expect( Listener._instance.add ).toHaveBeenCalledWith(
89
70
  'resize',
90
- hocInstance
71
+ // If not `undefined`, then we consider handlers were properly bound to the wrapper component.
72
+ expect.any( Object )
91
73
  );
92
74
  } );
93
75
 
94
76
  it( 'handles events', () => {
77
+ const EnhancedComponent = withGlobalEvents( {
78
+ resize: 'handleResize',
79
+ } )( OriginalComponent );
95
80
  const onResize = jest.fn();
96
81
 
97
- mountEnhancedComponent( { onResize } );
82
+ render(
83
+ <EnhancedComponent ref={ () => {} } onResize={ onResize }>
84
+ Hello
85
+ </EnhancedComponent>
86
+ );
98
87
 
99
88
  const event = { type: 'resize' };
100
89
 
@@ -1,31 +1,19 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import TestUtils from 'react-dom/test-utils';
4
+ import { render, screen } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
5
6
 
6
7
  /**
7
8
  * Internal dependencies
8
9
  */
9
10
  import withState from '../';
10
11
 
11
- /**
12
- * WordPress dependencies
13
- */
14
- import { Component } from '@wordpress/element';
15
-
16
- // This is needed because TestUtils does not accept a stateless component.
17
- // anything run through a HOC ends up as a stateless component.
18
- const getTestComponent = ( WrappedComponent ) => {
19
- class TestComponent extends Component {
20
- render() {
21
- return <WrappedComponent { ...this.props } />;
22
- }
23
- }
24
- return <TestComponent />;
25
- };
26
-
27
12
  describe( 'withState', () => {
28
- it( 'should pass initial state and allow updates', () => {
13
+ it( 'should pass initial state and allow updates', async () => {
14
+ const user = userEvent.setup( {
15
+ advanceTimers: jest.advanceTimersByTime,
16
+ } );
29
17
  const EnhancedComponent = withState( {
30
18
  count: 0,
31
19
  } )( ( { count, setState } ) => (
@@ -38,16 +26,15 @@ describe( 'withState', () => {
38
26
  </button>
39
27
  ) );
40
28
 
41
- const wrapper = TestUtils.renderIntoDocument(
42
- getTestComponent( EnhancedComponent )
43
- );
29
+ render( <EnhancedComponent /> );
44
30
 
45
- const buttonElement = () =>
46
- TestUtils.findRenderedDOMComponentWithTag( wrapper, 'button' );
31
+ const button = screen.getByRole( 'button' );
47
32
 
48
33
  expect( console ).toHaveWarned();
49
- expect( buttonElement().outerHTML ).toBe( '<button>0</button>' );
50
- TestUtils.Simulate.click( buttonElement() );
51
- expect( buttonElement().outerHTML ).toBe( '<button>1</button>' );
34
+ expect( button ).toHaveTextContent( '0' );
35
+
36
+ await user.click( button );
37
+
38
+ expect( button ).toHaveTextContent( '1' );
52
39
  } );
53
40
  } );
@@ -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
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { debounce } from '../../utils/debounce';
5
+ import useRefEffect from '../use-ref-effect';
6
+
7
+ /**
8
+ * In some circumstances, such as block previews, all focusable DOM elements
9
+ * (input fields, links, buttons, etc.) need to be disabled. This hook adds the
10
+ * behavior to disable nested DOM elements to the returned ref.
11
+ *
12
+ * If you can, prefer the use of the inert HTML attribute.
13
+ *
14
+ * @param {Object} config Configuration object.
15
+ * @param {boolean=} config.isDisabled Whether the element should be disabled.
16
+ * @return {import('react').RefCallback<HTMLElement>} Element Ref.
17
+ *
18
+ * @example
19
+ * ```js
20
+ * import { useDisabled } from '@wordpress/compose';
21
+ *
22
+ * const DisabledExample = () => {
23
+ * const disabledRef = useDisabled();
24
+ * return (
25
+ * <div ref={ disabledRef }>
26
+ * <a href="#">This link will have tabindex set to -1</a>
27
+ * <input placeholder="This input will have the disabled attribute added to it." type="text" />
28
+ * </div>
29
+ * );
30
+ * };
31
+ * ```
32
+ */
33
+ export default function useDisabled( {
34
+ isDisabled: isDisabledProp = false,
35
+ } = {} ) {
36
+ return useRefEffect(
37
+ ( node ) => {
38
+ if ( isDisabledProp ) {
39
+ return;
40
+ }
41
+
42
+ /** A variable keeping track of the previous updates in order to restore them. */
43
+ const updates: Function[] = [];
44
+ const disable = () => {
45
+ node.childNodes.forEach( ( child ) => {
46
+ if ( ! ( child instanceof HTMLElement ) ) {
47
+ return;
48
+ }
49
+ if ( ! child.getAttribute( 'inert' ) ) {
50
+ child.setAttribute( 'inert', 'true' );
51
+ updates.push( () => {
52
+ child.removeAttribute( 'inert' );
53
+ } );
54
+ }
55
+ } );
56
+ };
57
+
58
+ // Debounce re-disable since disabling process itself will incur
59
+ // additional mutations which should be ignored.
60
+ const debouncedDisable = debounce( disable, 0, {
61
+ leading: true,
62
+ } );
63
+ disable();
64
+
65
+ /** @type {MutationObserver | undefined} */
66
+ const observer = new window.MutationObserver( debouncedDisable );
67
+ observer.observe( node, {
68
+ childList: true,
69
+ } );
70
+
71
+ return () => {
72
+ if ( observer ) {
73
+ observer.disconnect();
74
+ }
75
+ debouncedDisable.cancel();
76
+ updates.forEach( ( update ) => update() );
77
+ };
78
+ },
79
+ [ isDisabledProp ]
80
+ );
81
+ }
@@ -13,36 +13,6 @@ import { forwardRef } from '@wordpress/element';
13
13
  */
14
14
  import useDisabled from '../';
15
15
 
16
- jest.mock( '@wordpress/dom', () => {
17
- const focus = jest.requireActual( '../../../../../dom/src' ).focus;
18
-
19
- return {
20
- focus: {
21
- ...focus,
22
- focusable: {
23
- ...focus.focusable,
24
- find( context ) {
25
- // In JSDOM, all elements have zero'd widths and height.
26
- // This is a metric for focusable's `isVisible`, so find
27
- // and apply an arbitrary non-zero width.
28
- Array.from( context.querySelectorAll( '*' ) ).forEach(
29
- ( element ) => {
30
- Object.defineProperties( element, {
31
- offsetWidth: {
32
- get: () => 1,
33
- configurable: true,
34
- },
35
- } );
36
- }
37
- );
38
-
39
- return focus.focusable.find( ...arguments );
40
- },
41
- },
42
- },
43
- };
44
- } );
45
-
46
16
  jest.useRealTimers();
47
17
 
48
18
  describe( 'useDisabled', () => {
@@ -69,11 +39,9 @@ describe( 'useDisabled', () => {
69
39
  const link = screen.getByRole( 'link' );
70
40
  const p = container.querySelector( 'p' );
71
41
 
72
- expect( input.hasAttribute( 'disabled' ) ).toBe( true );
73
- expect( link.getAttribute( 'tabindex' ) ).toBe( '-1' );
74
- expect( p.getAttribute( 'contenteditable' ) ).toBe( 'false' );
75
- expect( p.hasAttribute( 'tabindex' ) ).toBe( false );
76
- expect( p.hasAttribute( 'disabled' ) ).toBe( false );
42
+ expect( input ).toHaveAttribute( 'inert', 'true' );
43
+ expect( link ).toHaveAttribute( 'inert', 'true' );
44
+ expect( p ).toHaveAttribute( 'inert', 'true' );
77
45
  } );
78
46
 
79
47
  it( 'will disable an element rendered in an update to the component', async () => {
@@ -86,7 +54,7 @@ describe( 'useDisabled', () => {
86
54
 
87
55
  const button = screen.getByText( 'Button' );
88
56
  await waitFor( () => {
89
- expect( button.hasAttribute( 'disabled' ) ).toBe( true );
57
+ expect( button ).toHaveAttribute( 'inert', 'true' );
90
58
  } );
91
59
  } );
92
60
  } );
@@ -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();