@wordpress/compose 8.1.0 → 8.1.2-next.v.202606191442.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 (148) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +46 -42
  3. package/build/higher-order/pure/index.cjs +15 -0
  4. package/build/higher-order/pure/index.cjs.map +3 -3
  5. package/build/hooks/use-constrained-tabbing/index.cjs +3 -9
  6. package/build/hooks/use-constrained-tabbing/index.cjs.map +3 -3
  7. package/build/hooks/use-debounce/index.cjs +1 -1
  8. package/build/hooks/use-debounce/index.cjs.map +3 -3
  9. package/build/hooks/use-disabled/index.cjs.map +2 -2
  10. package/build/hooks/use-dragging/index.cjs +27 -22
  11. package/build/hooks/use-dragging/index.cjs.map +4 -4
  12. package/build/hooks/use-dragging/types.cjs +19 -0
  13. package/build/hooks/use-dragging/types.cjs.map +7 -0
  14. package/build/hooks/use-drop-zone/index.cjs +2 -5
  15. package/build/hooks/use-drop-zone/index.cjs.map +3 -3
  16. package/build/hooks/use-drop-zone/types.cjs +19 -0
  17. package/build/hooks/use-drop-zone/types.cjs.map +7 -0
  18. package/build/hooks/use-fixed-window-list/index.cjs +10 -8
  19. package/build/hooks/use-fixed-window-list/index.cjs.map +3 -3
  20. package/build/hooks/use-focus-return/index.cjs +12 -9
  21. package/build/hooks/use-focus-return/index.cjs.map +3 -3
  22. package/build/hooks/use-isomorphic-layout-effect/index.cjs +1 -1
  23. package/build/hooks/use-isomorphic-layout-effect/index.cjs.map +1 -1
  24. package/build/hooks/use-keyboard-shortcut/index.cjs +1 -4
  25. package/build/hooks/use-keyboard-shortcut/index.cjs.map +3 -3
  26. package/build/hooks/use-media-query/index.cjs +2 -2
  27. package/build/hooks/use-media-query/index.cjs.map +2 -2
  28. package/build/hooks/use-merge-refs/index.cjs +28 -8
  29. package/build/hooks/use-merge-refs/index.cjs.map +2 -2
  30. package/build/hooks/use-reduced-motion/index.cjs +1 -1
  31. package/build/hooks/use-reduced-motion/index.cjs.map +2 -2
  32. package/build/hooks/use-throttle/index.cjs +1 -1
  33. package/build/hooks/use-throttle/index.cjs.map +3 -3
  34. package/build/hooks/use-viewport-match/index.cjs +3 -6
  35. package/build/hooks/use-viewport-match/index.cjs.map +3 -3
  36. package/build/hooks/use-warn-on-change/index.cjs +3 -10
  37. package/build/hooks/use-warn-on-change/index.cjs.map +3 -3
  38. package/build/utils/subscribe-delegated-listener/index.cjs.map +1 -1
  39. package/build-module/higher-order/pure/index.mjs +5 -0
  40. package/build-module/higher-order/pure/index.mjs.map +2 -2
  41. package/build-module/hooks/use-constrained-tabbing/index.mjs +3 -9
  42. package/build-module/hooks/use-constrained-tabbing/index.mjs.map +3 -3
  43. package/build-module/hooks/use-debounce/index.mjs +1 -1
  44. package/build-module/hooks/use-debounce/index.mjs.map +3 -3
  45. package/build-module/hooks/use-disabled/index.mjs.map +2 -2
  46. package/build-module/hooks/use-dragging/index.mjs +27 -22
  47. package/build-module/hooks/use-dragging/index.mjs.map +4 -4
  48. package/build-module/hooks/use-dragging/types.mjs +1 -0
  49. package/build-module/hooks/use-dragging/types.mjs.map +7 -0
  50. package/build-module/hooks/use-drop-zone/index.mjs +2 -5
  51. package/build-module/hooks/use-drop-zone/index.mjs.map +3 -3
  52. package/build-module/hooks/use-drop-zone/types.mjs +1 -0
  53. package/build-module/hooks/use-drop-zone/types.mjs.map +7 -0
  54. package/build-module/hooks/use-fixed-window-list/index.mjs +10 -8
  55. package/build-module/hooks/use-fixed-window-list/index.mjs.map +3 -3
  56. package/build-module/hooks/use-focus-return/index.mjs +12 -9
  57. package/build-module/hooks/use-focus-return/index.mjs.map +3 -3
  58. package/build-module/hooks/use-isomorphic-layout-effect/index.mjs +1 -1
  59. package/build-module/hooks/use-isomorphic-layout-effect/index.mjs.map +1 -1
  60. package/build-module/hooks/use-keyboard-shortcut/index.mjs +1 -4
  61. package/build-module/hooks/use-keyboard-shortcut/index.mjs.map +3 -3
  62. package/build-module/hooks/use-media-query/index.mjs +2 -2
  63. package/build-module/hooks/use-media-query/index.mjs.map +2 -2
  64. package/build-module/hooks/use-merge-refs/index.mjs +28 -8
  65. package/build-module/hooks/use-merge-refs/index.mjs.map +2 -2
  66. package/build-module/hooks/use-reduced-motion/index.mjs +1 -1
  67. package/build-module/hooks/use-reduced-motion/index.mjs.map +2 -2
  68. package/build-module/hooks/use-throttle/index.mjs +1 -1
  69. package/build-module/hooks/use-throttle/index.mjs.map +3 -3
  70. package/build-module/hooks/use-viewport-match/index.mjs +3 -6
  71. package/build-module/hooks/use-viewport-match/index.mjs.map +3 -3
  72. package/build-module/hooks/use-warn-on-change/index.mjs +3 -10
  73. package/build-module/hooks/use-warn-on-change/index.mjs.map +3 -3
  74. package/build-module/utils/subscribe-delegated-listener/index.mjs.map +1 -1
  75. package/build-types/higher-order/pure/index.d.ts.map +1 -1
  76. package/build-types/hooks/use-constrained-tabbing/index.d.ts +2 -2
  77. package/build-types/hooks/use-constrained-tabbing/index.d.ts.map +1 -1
  78. package/build-types/hooks/use-debounce/index.d.ts +7 -6
  79. package/build-types/hooks/use-debounce/index.d.ts.map +1 -1
  80. package/build-types/hooks/use-disabled/index.d.ts +1 -1
  81. package/build-types/hooks/use-disabled/index.d.ts.map +1 -1
  82. package/build-types/hooks/use-dragging/index.d.ts +9 -13
  83. package/build-types/hooks/use-dragging/index.d.ts.map +1 -1
  84. package/build-types/hooks/use-dragging/types.d.ts +11 -0
  85. package/build-types/hooks/use-dragging/types.d.ts.map +1 -0
  86. package/build-types/hooks/use-drop-zone/index.d.ts +4 -21
  87. package/build-types/hooks/use-drop-zone/index.d.ts.map +1 -1
  88. package/build-types/hooks/use-drop-zone/types.d.ts +35 -0
  89. package/build-types/hooks/use-drop-zone/types.d.ts.map +1 -0
  90. package/build-types/hooks/use-fixed-window-list/index.d.ts +22 -26
  91. package/build-types/hooks/use-fixed-window-list/index.d.ts.map +1 -1
  92. package/build-types/hooks/use-focus-return/index.d.ts +3 -3
  93. package/build-types/hooks/use-focus-return/index.d.ts.map +1 -1
  94. package/build-types/hooks/use-isomorphic-layout-effect/index.d.ts.map +1 -1
  95. package/build-types/hooks/use-keyboard-shortcut/index.d.ts +29 -23
  96. package/build-types/hooks/use-keyboard-shortcut/index.d.ts.map +1 -1
  97. package/build-types/hooks/use-media-query/index.d.ts +1 -1
  98. package/build-types/hooks/use-media-query/index.d.ts.map +1 -1
  99. package/build-types/hooks/use-merge-refs/index.d.ts +7 -5
  100. package/build-types/hooks/use-merge-refs/index.d.ts.map +1 -1
  101. package/build-types/hooks/use-reduced-motion/index.d.ts +1 -1
  102. package/build-types/hooks/use-reduced-motion/index.d.ts.map +1 -1
  103. package/build-types/hooks/use-throttle/index.d.ts +8 -6
  104. package/build-types/hooks/use-throttle/index.d.ts.map +1 -1
  105. package/build-types/hooks/use-viewport-match/index.d.ts +3 -3
  106. package/build-types/hooks/use-viewport-match/index.d.ts.map +1 -1
  107. package/build-types/hooks/use-warn-on-change/index.d.ts +7 -7
  108. package/build-types/hooks/use-warn-on-change/index.d.ts.map +1 -1
  109. package/package.json +21 -12
  110. package/src/higher-order/pure/index.tsx +6 -0
  111. package/src/higher-order/pure/test/index.js +14 -81
  112. package/src/hooks/use-constrained-tabbing/{index.js → index.ts} +7 -11
  113. package/src/hooks/use-debounce/{index.js → index.ts} +11 -6
  114. package/src/hooks/use-disabled/index.ts +3 -1
  115. package/src/hooks/use-dragging/index.ts +78 -0
  116. package/src/hooks/use-dragging/types.ts +11 -0
  117. package/src/hooks/use-drop-zone/README.md +1 -1
  118. package/src/hooks/use-drop-zone/{index.js → index.ts} +17 -31
  119. package/src/hooks/use-drop-zone/types.ts +34 -0
  120. package/src/hooks/use-fixed-window-list/{index.js → index.ts} +46 -37
  121. package/src/hooks/use-focus-return/{index.js → index.ts} +26 -21
  122. package/src/hooks/use-keyboard-shortcut/{index.js → index.ts} +43 -28
  123. package/src/hooks/use-media-query/index.ts +10 -3
  124. package/src/hooks/use-media-query/test/ssr.js +47 -0
  125. package/src/hooks/use-merge-refs/index.ts +49 -18
  126. package/src/hooks/use-merge-refs/test/index.js +288 -0
  127. package/src/hooks/use-reduced-motion/{index.js → index.ts} +2 -2
  128. package/src/hooks/use-throttle/{index.js → index.ts} +12 -6
  129. package/src/hooks/use-viewport-match/{index.js → index.ts} +33 -26
  130. package/src/hooks/use-warn-on-change/index.ts +42 -0
  131. package/src/higher-order/with-network-connectivity/README.md +0 -20
  132. package/src/higher-order/with-network-connectivity/index.native.js +0 -19
  133. package/src/higher-order/with-preferred-color-scheme/index.native.js +0 -40
  134. package/src/hooks/use-constrained-tabbing/index.native.js +0 -14
  135. package/src/hooks/use-dragging/index.js +0 -74
  136. package/src/hooks/use-focus-outside/index.native.js +0 -170
  137. package/src/hooks/use-keyboard-shortcut/index.native.js +0 -2
  138. package/src/hooks/use-network-connectivity/index.native.js +0 -59
  139. package/src/hooks/use-network-connectivity/test/index.native.js +0 -87
  140. package/src/hooks/use-preferred-color-scheme/index.android.js +0 -30
  141. package/src/hooks/use-preferred-color-scheme/index.ios.js +0 -13
  142. package/src/hooks/use-preferred-color-scheme-style/index.native.js +0 -33
  143. package/src/hooks/use-resize-observer/index.native.js +0 -1
  144. package/src/hooks/use-resize-observer/legacy/index.native.js +0 -59
  145. package/src/hooks/use-resize-observer/legacy/test/index.native.js +0 -46
  146. package/src/hooks/use-warn-on-change/index.js +0 -43
  147. package/src/index.native.js +0 -44
  148. /package/src/hooks/use-isomorphic-layout-effect/{index.js → index.ts} +0 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+
5
+ /**
6
+ * External dependencies
7
+ */
8
+ import { renderToString } from 'react-dom/server';
9
+
10
+ /**
11
+ * Internal dependencies
12
+ */
13
+ import useMediaQuery from '../';
14
+ import useViewportMatch from '../../use-viewport-match';
15
+
16
+ const MediaQueryComponent = ( { query } ) => {
17
+ const result = useMediaQuery( query );
18
+ return `useMediaQuery: ${ result }`;
19
+ };
20
+
21
+ const ViewportMatchComponent = ( { breakpoint, operator } ) => {
22
+ const result = useViewportMatch( breakpoint, operator );
23
+ return `useViewportMatch: ${ result }`;
24
+ };
25
+
26
+ describe( 'compose hooks in a server (no-window) environment', () => {
27
+ it( 'useMediaQuery renders to false without throwing when window is undefined', () => {
28
+ expect( typeof window ).toBe( 'undefined' );
29
+ expect(
30
+ renderToString( <MediaQueryComponent query="(min-width: 782px)" /> )
31
+ ).toBe( 'useMediaQuery: false' );
32
+ } );
33
+
34
+ it( 'useMediaQuery renders to false when no query is provided', () => {
35
+ expect( renderToString( <MediaQueryComponent /> ) ).toBe(
36
+ 'useMediaQuery: false'
37
+ );
38
+ } );
39
+
40
+ it( 'useViewportMatch renders to false without throwing when window is undefined', () => {
41
+ expect(
42
+ renderToString(
43
+ <ViewportMatchComponent breakpoint="small" operator=">=" />
44
+ )
45
+ ).toBe( 'useViewportMatch: false' );
46
+ } );
47
+ } );
@@ -4,12 +4,33 @@
4
4
  import { useRef, useCallback, useLayoutEffect } from '@wordpress/element';
5
5
  import type { MutableRefObject, Ref, RefCallback } from 'react';
6
6
 
7
- function assignRef< T >( ref: Ref< T >, value: T ) {
7
+ // Returns a cleanup function if the ref callback returned one (React 19 ref
8
+ // callback cleanup pattern), otherwise `undefined`. Object refs never have a
9
+ // cleanup and only set `.current`.
10
+ function assignRef< T >( ref: Ref< T >, value: T ): ( () => void ) | undefined {
8
11
  if ( typeof ref === 'function' ) {
9
- ref( value );
12
+ const returned = ref( value );
13
+ return typeof returned === 'function' ? returned : undefined;
10
14
  } else if ( ref && ref.hasOwnProperty( 'current' ) ) {
11
15
  ( ref as MutableRefObject< T > ).current = value;
12
16
  }
17
+ return undefined;
18
+ }
19
+
20
+ // Tear down a ref at the given index: prefer the stored cleanup; otherwise
21
+ // fall back to calling the ref with `null`.
22
+ function detachRef< T >(
23
+ ref: Ref< T >,
24
+ index: number,
25
+ cleanups: Array< ( () => void ) | undefined >
26
+ ): void {
27
+ const cleanup = cleanups[ index ];
28
+ if ( cleanup ) {
29
+ cleanups[ index ] = undefined;
30
+ cleanup();
31
+ } else {
32
+ assignRef( ref, null );
33
+ }
13
34
  }
14
35
 
15
36
  /**
@@ -28,22 +49,24 @@ function assignRef< T >( ref: Ref< T >, value: T ) {
28
49
  * old ref callback will be called with `null` and the new ref callback will be
29
50
  * called with the same value.
30
51
  *
31
- * To make ref callbacks easier to use, you can also pass the result of
32
- * `useRefEffect`, which makes cleanup easier by allowing you to return a
33
- * cleanup function instead of handling `null`.
52
+ * Inner ref callbacks may return a cleanup function (React 19's ref callback
53
+ * cleanup pattern). When a ref callback returns a function, that function is
54
+ * invoked at teardown (node change, dependency change, or unmount) **instead
55
+ * of** the callback being called with `null`. Callbacks that do not return a
56
+ * cleanup continue to receive `null` on teardown as before.
34
57
  *
35
58
  * It's also possible to _disable_ a ref (and its behaviour) by simply not
36
59
  * passing the ref.
37
60
  *
38
61
  * ```jsx
39
- * const ref = useRefEffect( ( node ) => {
62
+ * const ref = useCallback( ( node ) => {
40
63
  * node.addEventListener( ... );
41
64
  * return () => {
42
65
  * node.removeEventListener( ... );
43
66
  * };
44
67
  * }, [ ...dependencies ] );
45
68
  * const otherRef = useRef();
46
- * const mergedRefs useMergeRefs( [
69
+ * const mergedRefs = useMergeRefs( [
47
70
  * enabled && ref,
48
71
  * otherRef,
49
72
  * ] );
@@ -56,11 +79,15 @@ function assignRef< T >( ref: Ref< T >, value: T ) {
56
79
  export default function useMergeRefs< T >(
57
80
  refs: Ref< T >[]
58
81
  ): RefCallback< T > {
59
- const element = useRef( null );
82
+ const elementRef = useRef< T | null >( null );
60
83
  const isAttachedRef = useRef( false );
61
84
  const didElementChangeRef = useRef( false );
62
85
  const previousRefsRef = useRef< Ref< T >[] >( [] );
63
86
  const currentRefsRef = useRef( refs );
87
+ // Position-indexed cleanups returned by inner ref callbacks. A slot is
88
+ // `undefined` when the ref at that position did not return a cleanup (or
89
+ // is an object ref / disabled).
90
+ const cleanupsRef = useRef< Array< ( () => void ) | undefined > >( [] );
64
91
 
65
92
  // Update on render before the ref callback is called, so the ref callback
66
93
  // always has access to the current refs.
@@ -77,8 +104,11 @@ export default function useMergeRefs< T >(
77
104
  refs.forEach( ( ref, index ) => {
78
105
  const previousRef = previousRefsRef.current[ index ];
79
106
  if ( ref !== previousRef ) {
80
- assignRef( previousRef, null );
81
- assignRef( ref, element.current );
107
+ detachRef( previousRef, index, cleanupsRef.current );
108
+ cleanupsRef.current[ index ] = assignRef(
109
+ ref,
110
+ elementRef.current as T
111
+ );
82
112
  }
83
113
  } );
84
114
  }
@@ -97,20 +127,21 @@ export default function useMergeRefs< T >(
97
127
  return useCallback( ( value: T | null ) => {
98
128
  // Update the element so it can be used when calling ref callbacks on a
99
129
  // dependency change.
100
- assignRef( element, value );
130
+ elementRef.current = value;
101
131
 
102
132
  didElementChangeRef.current = true;
103
133
  isAttachedRef.current = value !== null;
104
134
 
105
135
  // When an element changes, the current ref callback should be called
106
136
  // with the new element and the previous one with `null`.
107
- const refsToAssign = value
108
- ? currentRefsRef.current
109
- : previousRefsRef.current;
110
-
111
- // Update the latest refs.
112
- for ( const ref of refsToAssign ) {
113
- assignRef( ref, value );
137
+ if ( value === null ) {
138
+ previousRefsRef.current.forEach( ( ref, index ) => {
139
+ detachRef( ref, index, cleanupsRef.current );
140
+ } );
141
+ } else {
142
+ currentRefsRef.current.forEach( ( ref, index ) => {
143
+ cleanupsRef.current[ index ] = assignRef( ref, value );
144
+ } );
114
145
  }
115
146
  }, [] );
116
147
  }
@@ -345,3 +345,291 @@ describe( 'useMergeRefs', () => {
345
345
  ] );
346
346
  } );
347
347
  } );
348
+
349
+ describe( 'useMergeRefs with cleanup-returning ref callbacks', () => {
350
+ // Setup
351
+ // =====
352
+ //
353
+ // Same structure as the suite above, but the ref callbacks return a
354
+ // cleanup function. React 19 (and `useMergeRefs`) invokes the cleanup at
355
+ // teardown instead of calling the callback with `null`. The cleanup pushes
356
+ // the string `'cleanup'` into the same history array, so each call site's
357
+ // full lifecycle is captured as `[ node, 'cleanup', newNode, 'cleanup' ]`.
358
+ // The strict invariant is that `null` never appears in the history of a
359
+ // cleanup-returning ref.
360
+
361
+ function renderCallback( args ) {
362
+ renderCallback.history.push( args );
363
+ }
364
+
365
+ renderCallback.history = [];
366
+
367
+ function MergedRefs( {
368
+ count,
369
+ tagName: TagName = 'ul',
370
+ disable1,
371
+ disable2,
372
+ } ) {
373
+ function refCallback1( value ) {
374
+ refCallback1.history.push( value );
375
+ return () => {
376
+ refCallback1.history.push( 'cleanup' );
377
+ };
378
+ }
379
+
380
+ refCallback1.history = [];
381
+
382
+ function refCallback2( value ) {
383
+ refCallback2.history.push( value );
384
+ return () => {
385
+ refCallback2.history.push( 'cleanup' );
386
+ };
387
+ }
388
+
389
+ refCallback2.history = [];
390
+
391
+ renderCallback( [ refCallback1.history, refCallback2.history ] );
392
+
393
+ const ref1 = useCallback( refCallback1, [] );
394
+ const ref2 = useCallback( refCallback2, [ count ] );
395
+ const mergedRefs = useMergeRefs( [
396
+ ! disable1 && ref1,
397
+ ! disable2 && ref2,
398
+ ] );
399
+
400
+ return <TagName ref={ mergedRefs } />;
401
+ }
402
+
403
+ afterEach( () => {
404
+ renderCallback.history = [];
405
+ } );
406
+
407
+ it( 'should invoke cleanup on unmount instead of calling ref with null', () => {
408
+ const { unmount } = render( <MergedRefs /> );
409
+
410
+ const originalElement = screen.getByRole( 'list' );
411
+
412
+ expect( renderCallback.history ).toEqual( [
413
+ [ [ originalElement ], [ originalElement ] ],
414
+ ] );
415
+
416
+ unmount();
417
+
418
+ // Cleanups run; refs are NOT called with `null`.
419
+ expect( renderCallback.history ).toEqual( [
420
+ [
421
+ [ originalElement, 'cleanup' ],
422
+ [ originalElement, 'cleanup' ],
423
+ ],
424
+ ] );
425
+ } );
426
+
427
+ it( 'should invoke cleanup on node change instead of calling ref with null', () => {
428
+ const { rerender, unmount } = render( <MergedRefs /> );
429
+
430
+ const originalElement = screen.getByRole( 'list' );
431
+
432
+ rerender( <MergedRefs tagName="button" /> );
433
+
434
+ const newElement = screen.getByRole( 'button' );
435
+
436
+ // Node change triggers the outer callback first with `null` (which
437
+ // runs the cleanups) and then with the new node (which sets up new
438
+ // cleanups).
439
+ expect( renderCallback.history ).toEqual( [
440
+ [
441
+ [ originalElement, 'cleanup', newElement ],
442
+ [ originalElement, 'cleanup', newElement ],
443
+ ],
444
+ [ [], [] ],
445
+ ] );
446
+
447
+ unmount();
448
+
449
+ expect( renderCallback.history ).toEqual( [
450
+ [
451
+ [ originalElement, 'cleanup', newElement, 'cleanup' ],
452
+ [ originalElement, 'cleanup', newElement, 'cleanup' ],
453
+ ],
454
+ [ [], [] ],
455
+ ] );
456
+ } );
457
+
458
+ it( 'should invoke cleanup on dependency change instead of calling ref with null', () => {
459
+ const { rerender, unmount } = render( <MergedRefs count={ 1 } /> );
460
+
461
+ const originalElement = screen.getByRole( 'list' );
462
+
463
+ expect( renderCallback.history ).toEqual( [
464
+ [ [ originalElement ], [ originalElement ] ],
465
+ ] );
466
+
467
+ rerender( <MergedRefs count={ 2 } /> );
468
+
469
+ // Only ref2's deps changed. Its previous cleanup runs and the new ref
470
+ // callback is called with the still-attached node. ref1 is untouched.
471
+ expect( renderCallback.history ).toEqual( [
472
+ [ [ originalElement ], [ originalElement, 'cleanup' ] ],
473
+ [ [], [ originalElement ] ],
474
+ ] );
475
+
476
+ unmount();
477
+
478
+ expect( renderCallback.history ).toEqual( [
479
+ [
480
+ [ originalElement, 'cleanup' ],
481
+ [ originalElement, 'cleanup' ],
482
+ ],
483
+ [ [], [ originalElement, 'cleanup' ] ],
484
+ ] );
485
+ } );
486
+
487
+ it( 'should invoke cleanup on simultaneous node and dependency change', () => {
488
+ const { rerender, unmount } = render( <MergedRefs count={ 1 } /> );
489
+
490
+ const originalElement = screen.getByRole( 'list' );
491
+
492
+ rerender( <MergedRefs count={ 2 } tagName="button" /> );
493
+
494
+ const newElement = screen.getByRole( 'button' );
495
+
496
+ // Outer callback's null/new-node pair runs both cleanups and sets
497
+ // up the new ones (ref1 reused, ref2 has a new identity).
498
+ expect( renderCallback.history ).toEqual( [
499
+ [
500
+ [ originalElement, 'cleanup', newElement ],
501
+ [ originalElement, 'cleanup' ],
502
+ ],
503
+ [ [], [ newElement ] ],
504
+ ] );
505
+
506
+ unmount();
507
+
508
+ expect( renderCallback.history ).toEqual( [
509
+ [
510
+ [ originalElement, 'cleanup', newElement, 'cleanup' ],
511
+ [ originalElement, 'cleanup' ],
512
+ ],
513
+ [ [], [ newElement, 'cleanup' ] ],
514
+ ] );
515
+ } );
516
+
517
+ it( 'should invoke cleanup when a ref becomes disabled', () => {
518
+ const { rerender, unmount } = render( <MergedRefs /> );
519
+
520
+ const originalElement = screen.getByRole( 'list' );
521
+
522
+ rerender( <MergedRefs disable1 /> );
523
+
524
+ // ref1 is disabled: its stored cleanup must fire. ref2 is unchanged.
525
+ expect( renderCallback.history ).toEqual( [
526
+ [ [ originalElement, 'cleanup' ], [ originalElement ] ],
527
+ [ [], [] ],
528
+ ] );
529
+
530
+ unmount();
531
+
532
+ expect( renderCallback.history ).toEqual( [
533
+ [
534
+ [ originalElement, 'cleanup' ],
535
+ [ originalElement, 'cleanup' ],
536
+ ],
537
+ [ [], [] ],
538
+ ] );
539
+ } );
540
+
541
+ it( 'should support mixing a cleanup-returning ref with a void-returning ref', () => {
542
+ function MergedRefsMixed() {
543
+ function refCleanup( value ) {
544
+ refCleanup.history.push( value );
545
+ return () => {
546
+ refCleanup.history.push( 'cleanup' );
547
+ };
548
+ }
549
+
550
+ refCleanup.history = [];
551
+
552
+ function refVoid( value ) {
553
+ refVoid.history.push( value );
554
+ }
555
+
556
+ refVoid.history = [];
557
+
558
+ renderCallback( [ refCleanup.history, refVoid.history ] );
559
+
560
+ const r1 = useCallback( refCleanup, [] );
561
+ const r2 = useCallback( refVoid, [] );
562
+ const merged = useMergeRefs( [ r1, r2 ] );
563
+
564
+ return <ul ref={ merged } />;
565
+ }
566
+
567
+ const { unmount } = render( <MergedRefsMixed /> );
568
+
569
+ const el = screen.getByRole( 'list' );
570
+
571
+ expect( renderCallback.history ).toEqual( [ [ [ el ], [ el ] ] ] );
572
+
573
+ unmount();
574
+
575
+ // Cleanup ref sees the cleanup; void ref still sees null. Independent.
576
+ expect( renderCallback.history ).toEqual( [
577
+ [
578
+ [ el, 'cleanup' ],
579
+ [ el, null ],
580
+ ],
581
+ ] );
582
+ } );
583
+
584
+ it( 'should support mixing an object ref with a cleanup-returning ref', () => {
585
+ const objectRef = { current: null };
586
+
587
+ function MergedRefsMixed( { tagName: TagName = 'ul' } ) {
588
+ function refCleanup( value ) {
589
+ refCleanup.history.push( value );
590
+ return () => {
591
+ refCleanup.history.push( 'cleanup' );
592
+ };
593
+ }
594
+
595
+ refCleanup.history = [];
596
+
597
+ renderCallback( [ refCleanup.history ] );
598
+
599
+ const r = useCallback( refCleanup, [] );
600
+ const merged = useMergeRefs( [ objectRef, r ] );
601
+
602
+ return <TagName ref={ merged } />;
603
+ }
604
+
605
+ const { rerender, unmount } = render( <MergedRefsMixed /> );
606
+
607
+ const originalElement = screen.getByRole( 'list' );
608
+
609
+ // Object ref's .current is set; cleanup ref is called with the node.
610
+ expect( objectRef.current ).toBe( originalElement );
611
+ expect( renderCallback.history ).toEqual( [ [ [ originalElement ] ] ] );
612
+
613
+ rerender( <MergedRefsMixed tagName="button" /> );
614
+
615
+ const newElement = screen.getByRole( 'button' );
616
+
617
+ // Node change: object ref retargets, callback ref's cleanup fires
618
+ // and the callback is re-invoked with the new node.
619
+ expect( objectRef.current ).toBe( newElement );
620
+ expect( renderCallback.history ).toEqual( [
621
+ [ [ originalElement, 'cleanup', newElement ] ],
622
+ [ [] ],
623
+ ] );
624
+
625
+ unmount();
626
+
627
+ // Object ref nulled via the fallback assignRef path; callback ref
628
+ // sees its cleanup, never null.
629
+ expect( objectRef.current ).toBe( null );
630
+ expect( renderCallback.history ).toEqual( [
631
+ [ [ originalElement, 'cleanup', newElement, 'cleanup' ] ],
632
+ [ [] ],
633
+ ] );
634
+ } );
635
+ } );
@@ -6,9 +6,9 @@ import useMediaQuery from '../use-media-query';
6
6
  /**
7
7
  * Hook returning whether the user has a preference for reduced motion.
8
8
  *
9
- * @return {boolean} Reduced motion preference value.
9
+ * @return Reduced motion preference value.
10
10
  */
11
- const useReducedMotion = () =>
11
+ const useReducedMotion = (): boolean =>
12
12
  useMediaQuery( '(prefers-reduced-motion: reduce)' );
13
13
 
14
14
  export default useReducedMotion;
@@ -12,6 +12,8 @@ import { useEffect } from '@wordpress/element';
12
12
  * Internal dependencies
13
13
  */
14
14
  import { throttle } from '../../utils/throttle';
15
+ import type { ThrottleOptions } from '../../utils/throttle';
16
+ import type { DebouncedFunc } from '../../utils/debounce';
15
17
 
16
18
  /**
17
19
  * Throttles a function similar to Lodash's `throttle`. A new throttled function will
@@ -21,14 +23,18 @@ import { throttle } from '../../utils/throttle';
21
23
  *
22
24
  * @see https://lodash.com/docs/4#throttle
23
25
  *
24
- * @template {(...args: any[]) => void} TFunc
26
+ * @template TFunc
25
27
  *
26
- * @param {TFunc} fn The function to throttle.
27
- * @param {number} [wait] The number of milliseconds to throttle invocations to.
28
- * @param {import('../../utils/throttle').ThrottleOptions} [options] The options object. See linked documentation for details.
29
- * @return {import('../../utils/debounce').DebouncedFunc<TFunc>} Throttled function.
28
+ * @param fn The function to throttle.
29
+ * @param wait The number of milliseconds to throttle invocations to.
30
+ * @param options The options object.
31
+ * @return Throttled function.
30
32
  */
31
- export default function useThrottle( fn, wait, options ) {
33
+ export default function useThrottle< TFunc extends ( ...args: any[] ) => void >(
34
+ fn: TFunc,
35
+ wait?: number,
36
+ options?: ThrottleOptions
37
+ ): DebouncedFunc< TFunc > {
32
38
  const throttled = useMemoOne(
33
39
  () => throttle( fn, wait ?? 0, options ),
34
40
  [ fn, wait, options ]
@@ -8,18 +8,23 @@ import { createContext, useContext } from '@wordpress/element';
8
8
  */
9
9
  import useMediaQuery from '../use-media-query';
10
10
 
11
- /**
12
- * @typedef {"xhuge" | "huge" | "wide" | "xlarge" | "large" | "medium" | "small" | "mobile"} WPBreakpoint
13
- */
11
+ type Breakpoint =
12
+ | 'xhuge'
13
+ | 'huge'
14
+ | 'wide'
15
+ | 'xlarge'
16
+ | 'large'
17
+ | 'medium'
18
+ | 'small'
19
+ | 'mobile';
20
+ type ViewportOperator = '>=' | '<';
14
21
 
15
22
  /**
16
23
  * Hash of breakpoint names with pixel width at which it becomes effective.
17
24
  *
18
25
  * @see _breakpoints.scss
19
- *
20
- * @type {Record<WPBreakpoint, number>}
21
26
  */
22
- const BREAKPOINTS = {
27
+ const BREAKPOINTS: Record< Breakpoint, number > = {
23
28
  xhuge: 1920,
24
29
  huge: 1440,
25
30
  wide: 1280,
@@ -30,57 +35,59 @@ const BREAKPOINTS = {
30
35
  mobile: 480,
31
36
  };
32
37
 
33
- /**
34
- * @typedef {">=" | "<"} WPViewportOperator
35
- */
36
-
37
38
  /**
38
39
  * Object mapping media query operators to the condition to be used.
39
- *
40
- * @type {Record<WPViewportOperator, string>}
41
40
  */
42
- const CONDITIONS = {
41
+ const CONDITIONS: Record< ViewportOperator, string > = {
43
42
  '>=': 'min-width',
44
43
  '<': 'max-width',
45
44
  };
46
45
 
47
46
  /**
48
- * Object mapping media query operators to a function that given a breakpointValue and a width evaluates if the operator matches the values.
49
- *
50
- * @type {Record<WPViewportOperator, (breakpointValue: number, width: number) => boolean>}
47
+ * Object mapping media query operators to a function that evaluates if the operator matches.
51
48
  */
52
- const OPERATOR_EVALUATORS = {
49
+ const OPERATOR_EVALUATORS: Record<
50
+ ViewportOperator,
51
+ ( breakpointValue: number, width: number ) => boolean
52
+ > = {
53
53
  '>=': ( breakpointValue, width ) => width >= breakpointValue,
54
54
  '<': ( breakpointValue, width ) => width < breakpointValue,
55
55
  };
56
56
 
57
- const ViewportMatchWidthContext = createContext(
58
- /** @type {null | number} */ ( null )
59
- );
57
+ const ViewportMatchWidthContext = createContext< number | null >( null );
60
58
  ViewportMatchWidthContext.displayName = 'ViewportMatchWidthContext';
61
59
 
62
60
  /**
63
61
  * Returns true if the viewport matches the given query, or false otherwise.
64
62
  *
65
- * @param {WPBreakpoint} breakpoint Breakpoint size name.
66
- * @param {WPViewportOperator} [operator=">="] Viewport operator.
67
- * @param {Window} [view=window] Window instance in which to perform viewport matching.
63
+ * @param breakpoint Breakpoint size name.
64
+ * @param operator Viewport operator.
65
+ * @param view Window instance in which to perform viewport matching.
68
66
  *
69
67
  * @example
70
68
  *
71
- * ```js
69
+ * ```ts
72
70
  * useViewportMatch( 'huge', '<' );
73
71
  * useViewportMatch( 'medium' );
74
72
  * ```
75
73
  *
76
- * @return {boolean} Whether viewport matches query.
74
+ * @return Whether viewport matches query.
77
75
  */
78
- const useViewportMatch = ( breakpoint, operator = '>=', view = window ) => {
76
+ const useViewportMatch = (
77
+ breakpoint: Breakpoint,
78
+ operator: ViewportOperator = '>=',
79
+ // Resolve the default lazily so SSR (where `window` is undeclared) does not
80
+ // throw a ReferenceError when this default expression is evaluated.
81
+ view: Window | undefined = typeof window !== 'undefined'
82
+ ? window
83
+ : undefined
84
+ ): boolean => {
79
85
  const simulatedWidth = useContext( ViewportMatchWidthContext );
80
86
  const mediaQuery =
81
87
  ! simulatedWidth &&
82
88
  `(${ CONDITIONS[ operator ] }: ${ BREAKPOINTS[ breakpoint ] }px)`;
83
89
  const mediaQueryResult = useMediaQuery( mediaQuery || undefined, view );
90
+
84
91
  if ( simulatedWidth ) {
85
92
  return OPERATOR_EVALUATORS[ operator ](
86
93
  BREAKPOINTS[ breakpoint ],
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import usePrevious from '../use-previous';
5
+
6
+ /**
7
+ * Hook that performs a shallow comparison between the previous value of an object
8
+ * and the new one, if there's a difference, it prints it to the console.
9
+ * This is useful in performance related work, to check why a component re-renders.
10
+ *
11
+ * @example
12
+ *
13
+ * ```tsx
14
+ * function MyComponent(props: Record<string, any>) {
15
+ * useWarnOnChange(props);
16
+ *
17
+ * return "Something";
18
+ * }
19
+ * ```
20
+ *
21
+ * @param object Object which changes to compare.
22
+ * @param prefix Just a prefix to show when console logging.
23
+ */
24
+ function useWarnOnChange(
25
+ object: Record< string, any > | any[],
26
+ prefix: string = 'Change detection'
27
+ ): void {
28
+ const previousValues = usePrevious( object );
29
+
30
+ Object.entries( previousValues ?? [] ).forEach( ( [ key, value ] ) => {
31
+ if ( value !== object[ key as keyof typeof object ] ) {
32
+ // eslint-disable-next-line no-console
33
+ console.warn(
34
+ `${ prefix }: ${ key } key changed:`,
35
+ value,
36
+ object[ key as keyof typeof object ]
37
+ );
38
+ }
39
+ } );
40
+ }
41
+
42
+ export default useWarnOnChange;
@@ -1,20 +0,0 @@
1
- # withNetworkConnectivity
2
-
3
- `withNetworkConnectivity` provides a true/false mobile connectivity status based on the `useNetworkConnectivity` hook.
4
-
5
- ## Usage
6
-
7
- ```jsx
8
- /**
9
- * WordPress dependencies
10
- */
11
- import { withNetworkConnectivity } from '@wordpress/compose';
12
-
13
- export class MyComponent extends Component {
14
- if ( this.props.isConnected !== true ) {
15
- console.log( 'You are currently offline.' )
16
- }
17
- }
18
-
19
- export default withNetworkConnectivity( MyComponent )
20
- ```