@wordpress/compose 5.0.3 → 5.0.7-next.33ec3857e2.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 (102) hide show
  1. package/build/higher-order/with-global-events/index.js +3 -1
  2. package/build/higher-order/with-global-events/index.js.map +1 -1
  3. package/build/higher-order/with-state/index.js +3 -1
  4. package/build/higher-order/with-state/index.js.map +1 -1
  5. package/build/hooks/use-async-list/index.js +4 -3
  6. package/build/hooks/use-async-list/index.js.map +1 -1
  7. package/build/hooks/use-constrained-tabbing/index.js +47 -44
  8. package/build/hooks/use-constrained-tabbing/index.js.map +1 -1
  9. package/build/hooks/use-copy-on-click/index.js +9 -7
  10. package/build/hooks/use-copy-on-click/index.js.map +1 -1
  11. package/build/hooks/use-copy-to-clipboard/index.js +4 -3
  12. package/build/hooks/use-copy-to-clipboard/index.js.map +1 -1
  13. package/build/hooks/use-dialog/index.js +2 -2
  14. package/build/hooks/use-dialog/index.js.map +1 -1
  15. package/build/hooks/use-disabled/index.js +114 -0
  16. package/build/hooks/use-disabled/index.js.map +1 -0
  17. package/build/hooks/use-dragging/index.js +12 -11
  18. package/build/hooks/use-dragging/index.js.map +1 -1
  19. package/build/hooks/use-drop-zone/index.js +10 -9
  20. package/build/hooks/use-drop-zone/index.js.map +1 -1
  21. package/build/hooks/use-fixed-window-list/index.js +175 -0
  22. package/build/hooks/use-fixed-window-list/index.js.map +1 -0
  23. package/build/hooks/use-focus-on-mount/index.js +2 -1
  24. package/build/hooks/use-focus-on-mount/index.js.map +1 -1
  25. package/build/hooks/use-instance-id/index.js +2 -1
  26. package/build/hooks/use-instance-id/index.js.map +1 -1
  27. package/build/hooks/use-keyboard-shortcut/index.js +17 -15
  28. package/build/hooks/use-keyboard-shortcut/index.js.map +1 -1
  29. package/build/hooks/use-preferred-color-scheme/index.android.js +4 -3
  30. package/build/hooks/use-preferred-color-scheme/index.android.js.map +1 -1
  31. package/build/hooks/use-previous/index.js +3 -14
  32. package/build/hooks/use-previous/index.js.map +1 -1
  33. package/build/hooks/use-resize-observer/index.native.js +4 -3
  34. package/build/hooks/use-resize-observer/index.native.js.map +1 -1
  35. package/build/hooks/use-viewport-match/index.js +2 -1
  36. package/build/hooks/use-viewport-match/index.js.map +1 -1
  37. package/build/hooks/use-warn-on-change/index.js +5 -2
  38. package/build/hooks/use-warn-on-change/index.js.map +1 -1
  39. package/build/index.js +66 -50
  40. package/build/index.js.map +1 -1
  41. package/build/index.native.js +46 -46
  42. package/build-module/higher-order/with-global-events/index.js +3 -1
  43. package/build-module/higher-order/with-global-events/index.js.map +1 -1
  44. package/build-module/higher-order/with-state/index.js +3 -1
  45. package/build-module/higher-order/with-state/index.js.map +1 -1
  46. package/build-module/hooks/use-async-list/index.js +4 -3
  47. package/build-module/hooks/use-async-list/index.js.map +1 -1
  48. package/build-module/hooks/use-constrained-tabbing/index.js +42 -41
  49. package/build-module/hooks/use-constrained-tabbing/index.js.map +1 -1
  50. package/build-module/hooks/use-copy-on-click/index.js +9 -7
  51. package/build-module/hooks/use-copy-on-click/index.js.map +1 -1
  52. package/build-module/hooks/use-copy-to-clipboard/index.js +4 -3
  53. package/build-module/hooks/use-copy-to-clipboard/index.js.map +1 -1
  54. package/build-module/hooks/use-dialog/index.js +2 -2
  55. package/build-module/hooks/use-dialog/index.js.map +1 -1
  56. package/build-module/hooks/use-disabled/index.js +104 -0
  57. package/build-module/hooks/use-disabled/index.js.map +1 -0
  58. package/build-module/hooks/use-dragging/index.js +12 -11
  59. package/build-module/hooks/use-dragging/index.js.map +1 -1
  60. package/build-module/hooks/use-drop-zone/index.js +10 -9
  61. package/build-module/hooks/use-drop-zone/index.js.map +1 -1
  62. package/build-module/hooks/use-fixed-window-list/index.js +164 -0
  63. package/build-module/hooks/use-fixed-window-list/index.js.map +1 -0
  64. package/build-module/hooks/use-focus-on-mount/index.js +2 -1
  65. package/build-module/hooks/use-focus-on-mount/index.js.map +1 -1
  66. package/build-module/hooks/use-instance-id/index.js +2 -1
  67. package/build-module/hooks/use-instance-id/index.js.map +1 -1
  68. package/build-module/hooks/use-keyboard-shortcut/index.js +17 -15
  69. package/build-module/hooks/use-keyboard-shortcut/index.js.map +1 -1
  70. package/build-module/hooks/use-preferred-color-scheme/index.android.js +4 -3
  71. package/build-module/hooks/use-preferred-color-scheme/index.android.js.map +1 -1
  72. package/build-module/hooks/use-previous/index.js +3 -14
  73. package/build-module/hooks/use-previous/index.js.map +1 -1
  74. package/build-module/hooks/use-resize-observer/index.native.js +4 -3
  75. package/build-module/hooks/use-resize-observer/index.native.js.map +1 -1
  76. package/build-module/hooks/use-viewport-match/index.js +2 -1
  77. package/build-module/hooks/use-viewport-match/index.js.map +1 -1
  78. package/build-module/hooks/use-warn-on-change/index.js +5 -2
  79. package/build-module/hooks/use-warn-on-change/index.js.map +1 -1
  80. package/build-module/index.js +2 -0
  81. package/build-module/index.js.map +1 -1
  82. package/build-types/higher-order/with-state/index.d.ts.map +1 -1
  83. package/build-types/hooks/use-constrained-tabbing/index.d.ts.map +1 -1
  84. package/build-types/hooks/use-copy-on-click/index.d.ts.map +1 -1
  85. package/build-types/hooks/use-disabled/index.d.ts +23 -0
  86. package/build-types/hooks/use-disabled/index.d.ts.map +1 -0
  87. package/build-types/hooks/use-fixed-window-list/index.d.ts +57 -0
  88. package/build-types/hooks/use-fixed-window-list/index.d.ts.map +1 -0
  89. package/build-types/hooks/use-previous/index.d.ts +2 -4
  90. package/build-types/hooks/use-previous/index.d.ts.map +1 -1
  91. package/build-types/index.d.ts +2 -0
  92. package/package.json +9 -9
  93. package/src/higher-order/with-state/index.js +1 -0
  94. package/src/hooks/use-constrained-tabbing/index.js +39 -33
  95. package/src/hooks/use-copy-on-click/index.js +1 -2
  96. package/src/hooks/use-disabled/index.js +113 -0
  97. package/src/hooks/use-disabled/test/index.js +90 -0
  98. package/src/hooks/use-fixed-window-list/index.js +174 -0
  99. package/src/hooks/use-previous/index.ts +24 -0
  100. package/src/index.js +2 -0
  101. package/tsconfig.tsbuildinfo +1 -1
  102. package/src/hooks/use-previous/index.js +0 -31
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @typedef {Object} WPFixedWindowList
3
+ *
4
+ * @property {number} visibleItems Items visible in the current viewport
5
+ * @property {number} start Start index of the window
6
+ * @property {number} end End index of the window
7
+ * @property {(index:number)=>boolean} itemInView Returns true if item is in the window
8
+ */
9
+ /**
10
+ * @typedef {Object} WPFixedWindowListOptions
11
+ *
12
+ * @property {number} [windowOverscan] Renders windowOverscan number of items before and after the calculated visible window.
13
+ * @property {boolean} [useWindowing] When false avoids calculating the window size
14
+ * @property {number} [initWindowSize] Initial window size to use on first render before we can calculate the window size.
15
+ */
16
+ /**
17
+ *
18
+ * @param {import('react').RefObject<HTMLElement>} elementRef Used to find the closest scroll container that contains element.
19
+ * @param { number } itemHeight Fixed item height in pixels
20
+ * @param { number } totalItems Total items in list
21
+ * @param { WPFixedWindowListOptions } [options] Options object
22
+ * @return {[ WPFixedWindowList, setFixedListWindow:(nextWindow:WPFixedWindowList)=>void]} Array with the fixed window list and setter
23
+ */
24
+ export default function useFixedWindowList(elementRef: import('react').RefObject<HTMLElement>, itemHeight: number, totalItems: number, options?: WPFixedWindowListOptions | undefined): [WPFixedWindowList, (nextWindow: WPFixedWindowList) => void];
25
+ export type WPFixedWindowList = {
26
+ /**
27
+ * Items visible in the current viewport
28
+ */
29
+ visibleItems: number;
30
+ /**
31
+ * Start index of the window
32
+ */
33
+ start: number;
34
+ /**
35
+ * End index of the window
36
+ */
37
+ end: number;
38
+ /**
39
+ * Returns true if item is in the window
40
+ */
41
+ itemInView: (index: number) => boolean;
42
+ };
43
+ export type WPFixedWindowListOptions = {
44
+ /**
45
+ * Renders windowOverscan number of items before and after the calculated visible window.
46
+ */
47
+ windowOverscan?: number | undefined;
48
+ /**
49
+ * When false avoids calculating the window size
50
+ */
51
+ useWindowing?: boolean | undefined;
52
+ /**
53
+ * Initial window size to use on first render before we can calculate the window size.
54
+ */
55
+ initWindowSize?: number | undefined;
56
+ };
57
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-fixed-window-list/index.js"],"names":[],"mappings":"AAcA;;;;;;;GAOG;AAEH;;;;;;GAMG;AAEH;;;;;;;GAOG;AACH,uDANW,OAAO,OAAO,EAAE,SAAS,CAAC,WAAW,CAAC,cACrC,MAAM,cACN,MAAM,oFAE8C,iBAAiB,KAAG,IAAI,EAwIvF;;;;;kBA5Ja,MAAM;;;;WACN,MAAM;;;;SACN,MAAM;;;;wBACC,MAAM,KAAG,OAAO"}
@@ -2,11 +2,9 @@
2
2
  * Use something's value from the previous render.
3
3
  * Based on https://usehooks.com/usePrevious/.
4
4
  *
5
- * @template T
5
+ * @param value The value to track.
6
6
  *
7
- * @param {T} value The value to track.
8
- *
9
- * @return {T | undefined} The value from the previous render.
7
+ * @return The value from the previous render.
10
8
  */
11
9
  export default function usePrevious<T>(value: T): T | undefined;
12
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-previous/index.js"],"names":[],"mappings":"AAKA;;;;;;;;;GASG;AACH,gEAeC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-previous/index.ts"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,UAAU,WAAW,CAAE,CAAC,EAAI,KAAK,EAAE,CAAC,GAAI,CAAC,GAAG,SAAS,CAUlE"}
@@ -10,6 +10,7 @@ export { default as useConstrainedTabbing } from "./hooks/use-constrained-tabbin
10
10
  export { default as useCopyOnClick } from "./hooks/use-copy-on-click";
11
11
  export { default as useCopyToClipboard } from "./hooks/use-copy-to-clipboard";
12
12
  export { default as __experimentalUseDialog } from "./hooks/use-dialog";
13
+ export { default as __experimentalUseDisabled } from "./hooks/use-disabled";
13
14
  export { default as __experimentalUseDragging } from "./hooks/use-dragging";
14
15
  export { default as useFocusOnMount } from "./hooks/use-focus-on-mount";
15
16
  export { default as __experimentalUseFocusOutside } from "./hooks/use-focus-outside";
@@ -30,4 +31,5 @@ export { default as useMergeRefs } from "./hooks/use-merge-refs";
30
31
  export { default as useRefEffect } from "./hooks/use-ref-effect";
31
32
  export { default as __experimentalUseDropZone } from "./hooks/use-drop-zone";
32
33
  export { default as useFocusableIframe } from "./hooks/use-focusable-iframe";
34
+ export { default as __experimentalUseFixedWindowList } from "./hooks/use-fixed-window-list";
33
35
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/compose",
3
- "version": "5.0.3",
3
+ "version": "5.0.7-next.33ec3857e2.0",
4
4
  "description": "WordPress higher-order components (HOCs).",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -29,15 +29,15 @@
29
29
  "types": "build-types",
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@babel/runtime": "^7.13.10",
32
+ "@babel/runtime": "^7.16.0",
33
33
  "@types/lodash": "^4.14.172",
34
34
  "@types/mousetrap": "^1.6.8",
35
- "@wordpress/deprecated": "^3.2.2",
36
- "@wordpress/dom": "^3.2.4",
37
- "@wordpress/element": "^4.0.2",
38
- "@wordpress/is-shallow-equal": "^4.2.0",
39
- "@wordpress/keycodes": "^3.2.3",
40
- "@wordpress/priority-queue": "^2.2.2",
35
+ "@wordpress/deprecated": "^3.2.4-next.33ec3857e2.0",
36
+ "@wordpress/dom": "^3.2.8-next.33ec3857e2.0",
37
+ "@wordpress/element": "^4.0.6-next.33ec3857e2.0",
38
+ "@wordpress/is-shallow-equal": "^4.2.2-next.33ec3857e2.0",
39
+ "@wordpress/keycodes": "^3.2.5-next.33ec3857e2.0",
40
+ "@wordpress/priority-queue": "^2.2.4-next.33ec3857e2.0",
41
41
  "clipboard": "^2.0.8",
42
42
  "lodash": "^4.17.21",
43
43
  "mousetrap": "^1.6.5",
@@ -47,5 +47,5 @@
47
47
  "publishConfig": {
48
48
  "access": "public"
49
49
  },
50
- "gitHead": "8f7f052bc04e3f4eb50f479ced14be1489b9fa79"
50
+ "gitHead": "51c7917ea7fac72953702f24d6daac87d99e7617"
51
51
  }
@@ -21,6 +21,7 @@ import createHigherOrderComponent from '../../utils/create-higher-order-componen
21
21
  */
22
22
  export default function withState( initialState = {} ) {
23
23
  deprecated( 'wp.compose.withState', {
24
+ since: '5.8',
24
25
  alternative: 'wp.element.useState',
25
26
  } );
26
27
 
@@ -3,7 +3,11 @@
3
3
  */
4
4
  import { TAB } from '@wordpress/keycodes';
5
5
  import { focus } from '@wordpress/dom';
6
- import { useCallback } from '@wordpress/element';
6
+
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import useRefEffect from '../use-ref-effect';
7
11
 
8
12
  /**
9
13
  * In Dialogs/modals, the tabbing must be constrained to the content of
@@ -27,46 +31,48 @@ import { useCallback } from '@wordpress/element';
27
31
  * ```
28
32
  */
29
33
  function useConstrainedTabbing() {
30
- const ref = useCallback( ( /** @type {Element} */ node ) => {
31
- if ( ! node ) {
32
- return;
33
- }
34
- node.addEventListener( 'keydown', ( /** @type {Event} */ event ) => {
35
- if ( ! ( event instanceof window.KeyboardEvent ) ) {
36
- return;
37
- }
34
+ return useRefEffect( ( /** @type {HTMLElement} */ node ) => {
35
+ /** @type {number|undefined} */
36
+ let timeoutId;
37
+
38
+ function onKeyDown( /** @type {KeyboardEvent} */ event ) {
39
+ const { keyCode, shiftKey, target } = event;
38
40
 
39
- if ( event.keyCode !== TAB ) {
41
+ if ( keyCode !== TAB ) {
40
42
  return;
41
43
  }
42
44
 
43
- const tabbables = focus.tabbable.find( node );
44
- if ( ! tabbables.length ) {
45
+ const action = shiftKey ? 'findPrevious' : 'findNext';
46
+ const nextElement =
47
+ focus.tabbable[ action ](
48
+ /** @type {HTMLElement} */ ( target )
49
+ ) || null;
50
+
51
+ // If the element that is about to receive focus is outside the
52
+ // area, move focus to a div and insert it at the start or end of
53
+ // the area, depending on the direction. Without preventing default
54
+ // behaviour, the browser will then move focus to the next element.
55
+ if ( node.contains( nextElement ) ) {
45
56
  return;
46
57
  }
47
- const firstTabbable = tabbables[ 0 ];
48
- const lastTabbable = tabbables[ tabbables.length - 1 ];
49
58
 
50
- if ( event.shiftKey && event.target === firstTabbable ) {
51
- event.preventDefault();
52
- /** @type {HTMLElement} */ ( lastTabbable ).focus();
53
- } else if ( ! event.shiftKey && event.target === lastTabbable ) {
54
- event.preventDefault();
55
- /** @type {HTMLElement} */ ( firstTabbable ).focus();
56
- /*
57
- * When pressing Tab and none of the tabbables has focus, the keydown
58
- * event happens on the wrapper div: move focus on the first tabbable.
59
- */
60
- } else if (
61
- ! tabbables.includes( /** @type {Element} */ ( event.target ) )
62
- ) {
63
- event.preventDefault();
64
- /** @type {HTMLElement} */ ( firstTabbable ).focus();
65
- }
66
- } );
67
- }, [] );
59
+ const domAction = shiftKey ? 'append' : 'prepend';
60
+ const { ownerDocument } = node;
61
+ const trap = ownerDocument.createElement( 'div' );
68
62
 
69
- return ref;
63
+ trap.tabIndex = -1;
64
+ node[ domAction ]( trap );
65
+ trap.focus();
66
+ // Remove after the browser moves focus to the next element.
67
+ timeoutId = setTimeout( () => node.removeChild( trap ) );
68
+ }
69
+
70
+ node.addEventListener( 'keydown', onKeyDown );
71
+ return () => {
72
+ node.removeEventListener( 'keydown', onKeyDown );
73
+ clearTimeout( timeoutId );
74
+ };
75
+ }, [] );
70
76
  }
71
77
 
72
78
  export default useConstrainedTabbing;
@@ -26,8 +26,7 @@ import deprecated from '@wordpress/deprecated';
26
26
  export default function useCopyOnClick( ref, text, timeout = 4000 ) {
27
27
  /* eslint-enable jsdoc/no-undefined-types */
28
28
  deprecated( 'wp.compose.useCopyOnClick', {
29
- since: '10.3',
30
- plugin: 'Gutenberg',
29
+ since: '5.8',
31
30
  alternative: 'wp.compose.useCopyToClipboard',
32
31
  } );
33
32
 
@@ -0,0 +1,113 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { includes, debounce } from 'lodash';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useCallback, useLayoutEffect, useRef } from '@wordpress/element';
10
+ import { focus } from '@wordpress/dom';
11
+
12
+ /**
13
+ * Names of control nodes which qualify for disabled behavior.
14
+ *
15
+ * See WHATWG HTML Standard: 4.10.18.5: "Enabling and disabling form controls: the disabled attribute".
16
+ *
17
+ * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#enabling-and-disabling-form-controls:-the-disabled-attribute
18
+ *
19
+ * @type {string[]}
20
+ */
21
+ const DISABLED_ELIGIBLE_NODE_NAMES = [
22
+ 'BUTTON',
23
+ 'FIELDSET',
24
+ 'INPUT',
25
+ 'OPTGROUP',
26
+ 'OPTION',
27
+ 'SELECT',
28
+ 'TEXTAREA',
29
+ ];
30
+
31
+ /**
32
+ * In some circumstances, such as block previews, all focusable DOM elements
33
+ * (input fields, links, buttons, etc.) need to be disabled. This hook adds the
34
+ * behavior to disable nested DOM elements to the returned ref.
35
+ *
36
+ * @return {import('react').RefObject<HTMLElement>} Element Ref.
37
+ *
38
+ * @example
39
+ * ```js
40
+ * import { __experimentalUseDisabled as useDisabled } from '@wordpress/compose';
41
+ * const DisabledExample = () => {
42
+ * const disabledRef = useDisabled();
43
+ * return (
44
+ * <div ref={ disabledRef }>
45
+ * <a href="#">This link will have tabindex set to -1</a>
46
+ * <input placeholder="This input will have the disabled attribute added to it." type="text" />
47
+ * </div>
48
+ * );
49
+ * };
50
+ * ```
51
+ */
52
+ export default function useDisabled() {
53
+ /** @type {import('react').RefObject<HTMLElement>} */
54
+ const node = useRef( null );
55
+
56
+ const disable = () => {
57
+ if ( ! node.current ) {
58
+ return;
59
+ }
60
+
61
+ focus.focusable.find( node.current ).forEach( ( focusable ) => {
62
+ if (
63
+ includes( DISABLED_ELIGIBLE_NODE_NAMES, focusable.nodeName )
64
+ ) {
65
+ focusable.setAttribute( 'disabled', '' );
66
+ }
67
+
68
+ if ( focusable.nodeName === 'A' ) {
69
+ focusable.setAttribute( 'tabindex', '-1' );
70
+ }
71
+
72
+ const tabIndex = focusable.getAttribute( 'tabindex' );
73
+ if ( tabIndex !== null && tabIndex !== '-1' ) {
74
+ focusable.removeAttribute( 'tabindex' );
75
+ }
76
+
77
+ if ( focusable.hasAttribute( 'contenteditable' ) ) {
78
+ focusable.setAttribute( 'contenteditable', 'false' );
79
+ }
80
+ } );
81
+ };
82
+
83
+ // Debounce re-disable since disabling process itself will incur
84
+ // additional mutations which should be ignored.
85
+ const debouncedDisable = useCallback(
86
+ debounce( disable, undefined, { leading: true } ),
87
+ []
88
+ );
89
+
90
+ useLayoutEffect( () => {
91
+ disable();
92
+
93
+ /** @type {MutationObserver | undefined} */
94
+ let observer;
95
+ if ( node.current ) {
96
+ observer = new window.MutationObserver( debouncedDisable );
97
+ observer.observe( node.current, {
98
+ childList: true,
99
+ attributes: true,
100
+ subtree: true,
101
+ } );
102
+ }
103
+
104
+ return () => {
105
+ if ( observer ) {
106
+ observer.disconnect();
107
+ }
108
+ debouncedDisable.cancel();
109
+ };
110
+ }, [] );
111
+
112
+ return node;
113
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen, waitFor } from '@testing-library/react';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { forwardRef } from '@wordpress/element';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import useDisabled from '../';
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
+ describe( 'useDisabled', () => {
47
+ const Form = forwardRef( ( { showButton }, ref ) => {
48
+ return (
49
+ <form ref={ ref }>
50
+ <input />
51
+ <a href="https://wordpress.org/">A link</a>
52
+ <p contentEditable={ true } tabIndex="0"></p>
53
+ { showButton && <button>Button</button> }
54
+ </form>
55
+ );
56
+ } );
57
+
58
+ function DisabledComponent( props ) {
59
+ const disabledRef = useDisabled();
60
+ return <Form ref={ disabledRef } { ...props } />;
61
+ }
62
+
63
+ it( 'will disable all fields', () => {
64
+ const { container } = render( <DisabledComponent /> );
65
+
66
+ const input = screen.getByRole( 'textbox' );
67
+ const link = screen.getByRole( 'link' );
68
+ const p = container.querySelector( 'p' );
69
+
70
+ expect( input.hasAttribute( 'disabled' ) ).toBe( true );
71
+ expect( link.getAttribute( 'tabindex' ) ).toBe( '-1' );
72
+ expect( p.getAttribute( 'contenteditable' ) ).toBe( 'false' );
73
+ expect( p.hasAttribute( 'tabindex' ) ).toBe( false );
74
+ expect( p.hasAttribute( 'disabled' ) ).toBe( false );
75
+ } );
76
+
77
+ it( 'will disable an element rendered in an update to the component', async () => {
78
+ const { rerender } = render(
79
+ <DisabledComponent showButton={ false } />
80
+ );
81
+
82
+ expect( screen.queryByText( 'Button' ) ).not.toBeInTheDocument();
83
+ rerender( <DisabledComponent showButton={ true } /> );
84
+
85
+ const button = screen.getByText( 'Button' );
86
+ await waitFor( () => {
87
+ expect( button.hasAttribute( 'disabled' ) ).toBe( true );
88
+ } );
89
+ } );
90
+ } );
@@ -0,0 +1,174 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { debounce } from 'lodash';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useState, useLayoutEffect } from '@wordpress/element';
10
+ import { getScrollContainer } from '@wordpress/dom';
11
+ import { PAGEUP, PAGEDOWN, HOME, END } from '@wordpress/keycodes';
12
+
13
+ const DEFAULT_INIT_WINDOW_SIZE = 30;
14
+
15
+ /**
16
+ * @typedef {Object} WPFixedWindowList
17
+ *
18
+ * @property {number} visibleItems Items visible in the current viewport
19
+ * @property {number} start Start index of the window
20
+ * @property {number} end End index of the window
21
+ * @property {(index:number)=>boolean} itemInView Returns true if item is in the window
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} WPFixedWindowListOptions
26
+ *
27
+ * @property {number} [windowOverscan] Renders windowOverscan number of items before and after the calculated visible window.
28
+ * @property {boolean} [useWindowing] When false avoids calculating the window size
29
+ * @property {number} [initWindowSize] Initial window size to use on first render before we can calculate the window size.
30
+ */
31
+
32
+ /**
33
+ *
34
+ * @param {import('react').RefObject<HTMLElement>} elementRef Used to find the closest scroll container that contains element.
35
+ * @param { number } itemHeight Fixed item height in pixels
36
+ * @param { number } totalItems Total items in list
37
+ * @param { WPFixedWindowListOptions } [options] Options object
38
+ * @return {[ WPFixedWindowList, setFixedListWindow:(nextWindow:WPFixedWindowList)=>void]} Array with the fixed window list and setter
39
+ */
40
+ export default function useFixedWindowList(
41
+ elementRef,
42
+ itemHeight,
43
+ totalItems,
44
+ options
45
+ ) {
46
+ const initWindowSize = options?.initWindowSize ?? DEFAULT_INIT_WINDOW_SIZE;
47
+ const useWindowing = options?.useWindowing ?? true;
48
+
49
+ const [ fixedListWindow, setFixedListWindow ] = useState( {
50
+ visibleItems: initWindowSize,
51
+ start: 0,
52
+ end: initWindowSize,
53
+ itemInView: ( /** @type {number} */ index ) => {
54
+ return index >= 0 && index <= initWindowSize;
55
+ },
56
+ } );
57
+
58
+ useLayoutEffect( () => {
59
+ if ( ! useWindowing ) {
60
+ return;
61
+ }
62
+ const scrollContainer = getScrollContainer( elementRef.current );
63
+ const measureWindow = (
64
+ /** @type {boolean | undefined} */ initRender
65
+ ) => {
66
+ if ( ! scrollContainer ) {
67
+ return;
68
+ }
69
+ const visibleItems = Math.ceil(
70
+ scrollContainer.clientHeight / itemHeight
71
+ );
72
+ // Aim to keep opening list view fast, afterward we can optimize for scrolling
73
+ const windowOverscan = initRender
74
+ ? visibleItems
75
+ : options?.windowOverscan ?? visibleItems;
76
+ const firstViewableIndex = Math.floor(
77
+ scrollContainer.scrollTop / itemHeight
78
+ );
79
+ const start = Math.max( 0, firstViewableIndex - windowOverscan );
80
+ const end = Math.min(
81
+ totalItems - 1,
82
+ firstViewableIndex + visibleItems + windowOverscan
83
+ );
84
+ setFixedListWindow( ( lastWindow ) => {
85
+ const nextWindow = {
86
+ visibleItems,
87
+ start,
88
+ end,
89
+ itemInView: ( /** @type {number} */ index ) => {
90
+ return start <= index && index <= end;
91
+ },
92
+ };
93
+ if (
94
+ lastWindow.start !== nextWindow.start ||
95
+ lastWindow.end !== nextWindow.end ||
96
+ lastWindow.visibleItems !== nextWindow.visibleItems
97
+ ) {
98
+ return nextWindow;
99
+ }
100
+ return lastWindow;
101
+ } );
102
+ };
103
+
104
+ measureWindow( true );
105
+ const debounceMeasureList = debounce( () => {
106
+ measureWindow();
107
+ }, 16 );
108
+ scrollContainer?.addEventListener( 'scroll', debounceMeasureList );
109
+ scrollContainer?.ownerDocument?.defaultView?.addEventListener(
110
+ 'resize',
111
+ debounceMeasureList
112
+ );
113
+ scrollContainer?.ownerDocument?.defaultView?.addEventListener(
114
+ 'resize',
115
+ debounceMeasureList
116
+ );
117
+
118
+ return () => {
119
+ scrollContainer?.removeEventListener(
120
+ 'scroll',
121
+ debounceMeasureList
122
+ );
123
+ scrollContainer?.ownerDocument?.defaultView?.removeEventListener(
124
+ 'resize',
125
+ debounceMeasureList
126
+ );
127
+ };
128
+ }, [ itemHeight, elementRef, totalItems ] );
129
+
130
+ useLayoutEffect( () => {
131
+ if ( ! useWindowing ) {
132
+ return;
133
+ }
134
+ const scrollContainer = getScrollContainer( elementRef.current );
135
+ const handleKeyDown = ( /** @type {KeyboardEvent} */ event ) => {
136
+ switch ( event.keyCode ) {
137
+ case HOME: {
138
+ return scrollContainer?.scrollTo( { top: 0 } );
139
+ }
140
+ case END: {
141
+ return scrollContainer?.scrollTo( {
142
+ top: totalItems * itemHeight,
143
+ } );
144
+ }
145
+ case PAGEUP: {
146
+ return scrollContainer?.scrollTo( {
147
+ top:
148
+ scrollContainer.scrollTop -
149
+ fixedListWindow.visibleItems * itemHeight,
150
+ } );
151
+ }
152
+ case PAGEDOWN: {
153
+ return scrollContainer?.scrollTo( {
154
+ top:
155
+ scrollContainer.scrollTop +
156
+ fixedListWindow.visibleItems * itemHeight,
157
+ } );
158
+ }
159
+ }
160
+ };
161
+ scrollContainer?.ownerDocument?.defaultView?.addEventListener(
162
+ 'keydown',
163
+ handleKeyDown
164
+ );
165
+ return () => {
166
+ scrollContainer?.ownerDocument?.defaultView?.removeEventListener(
167
+ 'keydown',
168
+ handleKeyDown
169
+ );
170
+ };
171
+ }, [ totalItems, itemHeight, elementRef, fixedListWindow.visibleItems ] );
172
+
173
+ return [ fixedListWindow, setFixedListWindow ];
174
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useEffect, useRef } from '@wordpress/element';
5
+
6
+ /**
7
+ * Use something's value from the previous render.
8
+ * Based on https://usehooks.com/usePrevious/.
9
+ *
10
+ * @param value The value to track.
11
+ *
12
+ * @return The value from the previous render.
13
+ */
14
+ export default function usePrevious< T >( value: T ): T | undefined {
15
+ const ref = useRef< T >();
16
+
17
+ // Store current value in ref.
18
+ useEffect( () => {
19
+ ref.current = value;
20
+ }, [ value ] ); // Re-run when value changes.
21
+
22
+ // Return previous value (happens before update in useEffect above).
23
+ return ref.current;
24
+ }
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ export { default as useConstrainedTabbing } from './hooks/use-constrained-tabbin
17
17
  export { default as useCopyOnClick } from './hooks/use-copy-on-click';
18
18
  export { default as useCopyToClipboard } from './hooks/use-copy-to-clipboard';
19
19
  export { default as __experimentalUseDialog } from './hooks/use-dialog';
20
+ export { default as __experimentalUseDisabled } from './hooks/use-disabled';
20
21
  export { default as __experimentalUseDragging } from './hooks/use-dragging';
21
22
  export { default as useFocusOnMount } from './hooks/use-focus-on-mount';
22
23
  export { default as __experimentalUseFocusOutside } from './hooks/use-focus-outside';
@@ -37,3 +38,4 @@ export { default as useMergeRefs } from './hooks/use-merge-refs';
37
38
  export { default as useRefEffect } from './hooks/use-ref-effect';
38
39
  export { default as __experimentalUseDropZone } from './hooks/use-drop-zone';
39
40
  export { default as useFocusableIframe } from './hooks/use-focusable-iframe';
41
+ export { default as __experimentalUseFixedWindowList } from './hooks/use-fixed-window-list';