@wordpress/compose 5.17.0 → 5.18.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,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
  } );
@@ -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,7 +1,8 @@
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
  * WordPress dependencies
@@ -12,38 +13,24 @@ import { Component } from '@wordpress/element';
12
13
  * Internal dependencies
13
14
  */
14
15
  import useFocusOutside from '../';
15
- import ReactDOM from 'react-dom';
16
16
 
17
- let wrapper, onFocusOutside;
17
+ let onFocusOutside;
18
18
 
19
19
  describe( 'useFocusOutside', () => {
20
20
  let origHasFocus;
21
21
 
22
22
  const FocusOutsideComponent = ( { onFocusOutside: callback } ) => (
23
23
  <div { ...useFocusOutside( callback ) }>
24
- <input />
24
+ <input type="text" />
25
25
  <input type="button" />
26
26
  </div>
27
27
  );
28
28
 
29
- // This is needed because TestUtils does not accept a stateless component.
30
- // anything run through a HOC ends up as a stateless component.
31
- const getTestComponent = ( WrappedComponent, props ) => {
32
- class TestComponent extends Component {
33
- render() {
34
- return <WrappedComponent { ...props } />;
35
- }
29
+ class TestComponent extends Component {
30
+ render() {
31
+ return <FocusOutsideComponent { ...this.props } />;
36
32
  }
37
- return <TestComponent />;
38
- };
39
-
40
- const simulateEvent = ( event, index = 0 ) => {
41
- const element = TestUtils.scryRenderedDOMComponentsWithTag(
42
- wrapper,
43
- 'input'
44
- );
45
- TestUtils.Simulate[ event ]( element[ index ] );
46
- };
33
+ }
47
34
 
48
35
  beforeEach( () => {
49
36
  // Mock document.hasFocus() to always be true for testing
@@ -52,9 +39,6 @@ describe( 'useFocusOutside', () => {
52
39
  document.hasFocus = () => true;
53
40
 
54
41
  onFocusOutside = jest.fn();
55
- wrapper = TestUtils.renderIntoDocument(
56
- getTestComponent( FocusOutsideComponent, { onFocusOutside } )
57
- );
58
42
  } );
59
43
 
60
44
  afterEach( () => {
@@ -62,24 +46,31 @@ describe( 'useFocusOutside', () => {
62
46
  } );
63
47
 
64
48
  it( 'should not call handler if focus shifts to element within component', () => {
65
- simulateEvent( 'focus' );
66
- simulateEvent( 'blur' );
67
- simulateEvent( 'focus', 1 );
49
+ render( <TestComponent onFocusOutside={ onFocusOutside } /> );
50
+
51
+ const input = screen.getByRole( 'textbox' );
52
+ const button = screen.getByRole( 'button' );
53
+
54
+ input.focus();
55
+ input.blur();
56
+ button.focus();
68
57
 
69
58
  jest.runAllTimers();
70
59
 
71
60
  expect( onFocusOutside ).not.toHaveBeenCalled();
72
61
  } );
73
62
 
74
- it( 'should not call handler if focus transitions via click to button', () => {
75
- simulateEvent( 'focus' );
76
- simulateEvent( 'mouseDown', 1 );
77
- simulateEvent( 'blur' );
63
+ it( 'should not call handler if focus transitions via click to button', async () => {
64
+ const user = userEvent.setup( {
65
+ advanceTimers: jest.advanceTimersByTime,
66
+ } );
67
+ render( <TestComponent onFocusOutside={ onFocusOutside } /> );
68
+
69
+ const input = screen.getByRole( 'textbox' );
70
+ const button = screen.getByRole( 'button' );
78
71
 
79
- // In most browsers, the input at index 1 would receive a focus event
80
- // at this point, but this is not guaranteed, which is the intention of
81
- // the normalization behavior tested here.
82
- simulateEvent( 'mouseUp', 1 );
72
+ input.focus();
73
+ await user.click( button );
83
74
 
84
75
  jest.runAllTimers();
85
76
 
@@ -87,8 +78,11 @@ describe( 'useFocusOutside', () => {
87
78
  } );
88
79
 
89
80
  it( 'should call handler if focus doesn’t shift to element within component', () => {
90
- simulateEvent( 'focus' );
91
- simulateEvent( 'blur' );
81
+ render( <TestComponent onFocusOutside={ onFocusOutside } /> );
82
+
83
+ const input = screen.getByRole( 'textbox' );
84
+ input.focus();
85
+ input.blur();
92
86
 
93
87
  jest.runAllTimers();
94
88
 
@@ -96,12 +90,15 @@ describe( 'useFocusOutside', () => {
96
90
  } );
97
91
 
98
92
  it( 'should not call handler if focus shifts outside the component when the document does not have focus', () => {
93
+ render( <TestComponent onFocusOutside={ onFocusOutside } /> );
94
+
99
95
  // Force document.hasFocus() to return false to simulate the window/document losing focus
100
96
  // See https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus.
101
97
  document.hasFocus = () => false;
102
98
 
103
- simulateEvent( 'focus' );
104
- simulateEvent( 'blur' );
99
+ const input = screen.getByRole( 'textbox' );
100
+ input.focus();
101
+ input.blur();
105
102
 
106
103
  jest.runAllTimers();
107
104
 
@@ -109,14 +106,16 @@ describe( 'useFocusOutside', () => {
109
106
  } );
110
107
 
111
108
  it( 'should cancel check when unmounting while queued', () => {
112
- simulateEvent( 'focus' );
113
- simulateEvent( 'input' );
114
-
115
- ReactDOM.unmountComponentAtNode(
116
- // eslint-disable-next-line react/no-find-dom-node
117
- ReactDOM.findDOMNode( wrapper ).parentNode
109
+ const { rerender } = render(
110
+ <TestComponent onFocusOutside={ onFocusOutside } />
118
111
  );
119
112
 
113
+ const input = screen.getByRole( 'textbox' );
114
+ input.focus();
115
+ input.blur();
116
+
117
+ rerender( <div /> );
118
+
120
119
  jest.runAllTimers();
121
120
 
122
121
  expect( onFocusOutside ).not.toHaveBeenCalled();
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { create, act } from 'react-test-renderer';
4
+ import { render } from '@testing-library/react';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
@@ -14,23 +14,16 @@ describe( 'useInstanceId', () => {
14
14
  };
15
15
 
16
16
  it( 'should manage ids', async () => {
17
- let test0;
17
+ const { container, rerender } = render( <TestComponent /> );
18
18
 
19
- await act( async () => {
20
- test0 = create( <TestComponent /> );
21
- } );
19
+ expect( container ).toHaveTextContent( '0' );
22
20
 
23
- expect( test0.toJSON() ).toBe( '0' );
21
+ rerender(
22
+ <div>
23
+ <TestComponent />
24
+ </div>
25
+ );
24
26
 
25
- let test1;
26
-
27
- await act( async () => {
28
- test1 = create( <TestComponent /> );
29
- } );
30
-
31
- expect( test1.toJSON() ).toBe( '1' );
32
-
33
- test0.unmount();
34
- test1.unmount();
27
+ expect( container ).toHaveTextContent( '1' );
35
28
  } );
36
29
  } );
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { create, act } from 'react-test-renderer';
4
+ import { render, act } from '@testing-library/react';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
@@ -33,18 +33,6 @@ describe( 'useMediaQuery', () => {
33
33
  return `useMediaQuery: ${ queryResult }`;
34
34
  };
35
35
 
36
- it( 'should return true when query matches on the first render', async () => {
37
- global.matchMedia.mockReturnValue( {
38
- addListener,
39
- removeListener,
40
- matches: true,
41
- } );
42
-
43
- const root = create( <TestComponent query="(min-width: 782px)" /> );
44
-
45
- expect( root.toJSON() ).toBe( 'useMediaQuery: true' );
46
- } );
47
-
48
36
  it( 'should return true when query matches', async () => {
49
37
  global.matchMedia.mockReturnValue( {
50
38
  addListener,
@@ -52,17 +40,14 @@ describe( 'useMediaQuery', () => {
52
40
  matches: true,
53
41
  } );
54
42
 
55
- let root;
43
+ const { container, unmount } = render(
44
+ <TestComponent query="(min-width: 782px)" />
45
+ );
56
46
 
57
- await act( async () => {
58
- root = create( <TestComponent query="(min-width: 782px)" /> );
59
- } );
47
+ expect( container ).toHaveTextContent( 'useMediaQuery: true' );
60
48
 
61
- expect( root.toJSON() ).toBe( 'useMediaQuery: true' );
49
+ unmount();
62
50
 
63
- await act( async () => {
64
- root.unmount();
65
- } );
66
51
  expect( removeListener ).toHaveBeenCalled();
67
52
  } );
68
53
 
@@ -90,24 +75,23 @@ describe( 'useMediaQuery', () => {
90
75
  matches: false,
91
76
  } );
92
77
 
93
- let root, updateMatchFunction;
94
- await act( async () => {
95
- root = create( <TestComponent query="(min-width: 782px)" /> );
96
- } );
97
- expect( root.toJSON() ).toBe( 'useMediaQuery: true' );
78
+ const { container, unmount } = render(
79
+ <TestComponent query="(min-width: 782px)" />
80
+ );
81
+
82
+ expect( container ).toHaveTextContent( 'useMediaQuery: true' );
98
83
 
84
+ let updateMatchFunction;
99
85
  await act( async () => {
100
86
  updateMatchFunction = addListener.mock.calls[ 0 ][ 0 ];
101
87
  updateMatchFunction();
102
88
  } );
103
- expect( root.toJSON() ).toBe( 'useMediaQuery: false' );
104
89
 
105
- await act( async () => {
106
- root.unmount();
107
- } );
108
- expect( removeListener.mock.calls ).toEqual( [
109
- [ updateMatchFunction ],
110
- ] );
90
+ expect( container ).toHaveTextContent( 'useMediaQuery: false' );
91
+
92
+ unmount();
93
+
94
+ expect( removeListener ).toHaveBeenCalledWith( updateMatchFunction );
111
95
  } );
112
96
 
113
97
  it( 'should return false when the query does not matches', async () => {
@@ -116,15 +100,15 @@ describe( 'useMediaQuery', () => {
116
100
  removeListener,
117
101
  matches: false,
118
102
  } );
119
- let root;
120
- await act( async () => {
121
- root = create( <TestComponent query="(min-width: 782px)" /> );
122
- } );
123
- expect( root.toJSON() ).toBe( 'useMediaQuery: false' );
124
103
 
125
- await act( async () => {
126
- root.unmount();
127
- } );
104
+ const { container, unmount } = render(
105
+ <TestComponent query="(min-width: 782px)" />
106
+ );
107
+
108
+ expect( container ).toHaveTextContent( 'useMediaQuery: false' );
109
+
110
+ unmount();
111
+
128
112
  expect( removeListener ).toHaveBeenCalled();
129
113
  } );
130
114
 
@@ -134,21 +118,17 @@ describe( 'useMediaQuery', () => {
134
118
  removeListener,
135
119
  matches: false,
136
120
  } );
137
- let root;
138
- await act( async () => {
139
- root = create( <TestComponent /> );
140
- } );
121
+
122
+ const { container, rerender, unmount } = render( <TestComponent /> );
123
+
141
124
  // Query will be case to a boolean to simplify the return type.
142
- expect( root.toJSON() ).toBe( 'useMediaQuery: false' );
125
+ expect( container ).toHaveTextContent( 'useMediaQuery: false' );
143
126
 
144
- await act( async () => {
145
- root.update( <TestComponent query={ false } /> );
146
- } );
147
- expect( root.toJSON() ).toBe( 'useMediaQuery: false' );
127
+ rerender( <TestComponent query={ false } /> );
148
128
 
149
- await act( async () => {
150
- root.unmount();
151
- } );
129
+ expect( container ).toHaveTextContent( 'useMediaQuery: false' );
130
+
131
+ unmount();
152
132
  expect( global.matchMedia ).not.toHaveBeenCalled();
153
133
  expect( addListener ).not.toHaveBeenCalled();
154
134
  expect( removeListener ).not.toHaveBeenCalled();
@@ -47,7 +47,11 @@ const useResizeObserver = () => {
47
47
  }, [] );
48
48
 
49
49
  const observer = (
50
- <View style={ StyleSheet.absoluteFill } onLayout={ onLayout } />
50
+ <View
51
+ testID="resize-observer"
52
+ style={ StyleSheet.absoluteFill }
53
+ onLayout={ onLayout }
54
+ />
51
55
  );
52
56
 
53
57
  return [ observer, measurements ];
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { create, act } from 'react-test-renderer';
4
+ import { render, fireEvent } from 'test/helpers';
5
5
  import { View } from 'react-native';
6
6
 
7
7
  /**
@@ -13,36 +13,32 @@ const TestComponent = ( { onLayout } ) => {
13
13
  const [ resizeObserver, sizes ] = useResizeObserver();
14
14
 
15
15
  return (
16
- <View sizes={ sizes } onLayout={ onLayout }>
16
+ <View testID="test-component" sizes={ sizes } onLayout={ onLayout }>
17
17
  { resizeObserver }
18
18
  </View>
19
19
  );
20
20
  };
21
21
 
22
- const renderWithOnLayout = ( component ) => {
23
- const testComponent = create( component );
24
-
25
- const mockNativeEvent = {
26
- nativeEvent: {
27
- layout: {
28
- width: 300,
29
- height: 500,
22
+ describe( 'useResizeObserver()', () => {
23
+ it( 'should return "{ width: 300, height: 500 }"', () => {
24
+ const mockNativeEvent = {
25
+ nativeEvent: {
26
+ layout: {
27
+ width: 300,
28
+ height: 500,
29
+ },
30
30
  },
31
- },
32
- };
33
-
34
- act( () => {
35
- testComponent.toJSON().children[ 0 ].props.onLayout( mockNativeEvent );
36
- } );
31
+ };
37
32
 
38
- return testComponent.toJSON();
39
- };
33
+ const { getByTestId } = render(
34
+ <TestComponent onLayout={ mockNativeEvent } />
35
+ );
40
36
 
41
- describe( 'useResizeObserver()', () => {
42
- it( 'should return "{ width: 300, height: 500 }"', () => {
43
- const component = renderWithOnLayout( <TestComponent /> );
37
+ const resizeObserver = getByTestId( 'resize-observer' );
38
+ fireEvent( resizeObserver, 'layout', mockNativeEvent );
44
39
 
45
- expect( component.props.sizes ).toMatchObject( {
40
+ const testComponent = getByTestId( 'test-component' );
41
+ expect( testComponent.props.sizes ).toMatchObject( {
46
42
  width: 300,
47
43
  height: 500,
48
44
  } );