@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.
- package/build/higher-order/with-global-events/index.js +3 -1
- package/build/higher-order/with-global-events/index.js.map +1 -1
- package/build/higher-order/with-state/index.js +3 -1
- package/build/higher-order/with-state/index.js.map +1 -1
- package/build/hooks/use-async-list/index.js +4 -3
- package/build/hooks/use-async-list/index.js.map +1 -1
- package/build/hooks/use-constrained-tabbing/index.js +47 -44
- package/build/hooks/use-constrained-tabbing/index.js.map +1 -1
- package/build/hooks/use-copy-on-click/index.js +9 -7
- package/build/hooks/use-copy-on-click/index.js.map +1 -1
- package/build/hooks/use-copy-to-clipboard/index.js +4 -3
- package/build/hooks/use-copy-to-clipboard/index.js.map +1 -1
- package/build/hooks/use-dialog/index.js +2 -2
- package/build/hooks/use-dialog/index.js.map +1 -1
- package/build/hooks/use-disabled/index.js +114 -0
- package/build/hooks/use-disabled/index.js.map +1 -0
- package/build/hooks/use-dragging/index.js +12 -11
- package/build/hooks/use-dragging/index.js.map +1 -1
- package/build/hooks/use-drop-zone/index.js +10 -9
- package/build/hooks/use-drop-zone/index.js.map +1 -1
- package/build/hooks/use-fixed-window-list/index.js +175 -0
- package/build/hooks/use-fixed-window-list/index.js.map +1 -0
- package/build/hooks/use-focus-on-mount/index.js +2 -1
- package/build/hooks/use-focus-on-mount/index.js.map +1 -1
- package/build/hooks/use-instance-id/index.js +2 -1
- package/build/hooks/use-instance-id/index.js.map +1 -1
- package/build/hooks/use-keyboard-shortcut/index.js +17 -15
- package/build/hooks/use-keyboard-shortcut/index.js.map +1 -1
- package/build/hooks/use-preferred-color-scheme/index.android.js +4 -3
- package/build/hooks/use-preferred-color-scheme/index.android.js.map +1 -1
- package/build/hooks/use-previous/index.js +3 -14
- package/build/hooks/use-previous/index.js.map +1 -1
- package/build/hooks/use-resize-observer/index.native.js +4 -3
- package/build/hooks/use-resize-observer/index.native.js.map +1 -1
- package/build/hooks/use-viewport-match/index.js +2 -1
- package/build/hooks/use-viewport-match/index.js.map +1 -1
- package/build/hooks/use-warn-on-change/index.js +5 -2
- package/build/hooks/use-warn-on-change/index.js.map +1 -1
- package/build/index.js +66 -50
- package/build/index.js.map +1 -1
- package/build/index.native.js +46 -46
- package/build-module/higher-order/with-global-events/index.js +3 -1
- package/build-module/higher-order/with-global-events/index.js.map +1 -1
- package/build-module/higher-order/with-state/index.js +3 -1
- package/build-module/higher-order/with-state/index.js.map +1 -1
- package/build-module/hooks/use-async-list/index.js +4 -3
- package/build-module/hooks/use-async-list/index.js.map +1 -1
- package/build-module/hooks/use-constrained-tabbing/index.js +42 -41
- package/build-module/hooks/use-constrained-tabbing/index.js.map +1 -1
- package/build-module/hooks/use-copy-on-click/index.js +9 -7
- package/build-module/hooks/use-copy-on-click/index.js.map +1 -1
- package/build-module/hooks/use-copy-to-clipboard/index.js +4 -3
- package/build-module/hooks/use-copy-to-clipboard/index.js.map +1 -1
- package/build-module/hooks/use-dialog/index.js +2 -2
- package/build-module/hooks/use-dialog/index.js.map +1 -1
- package/build-module/hooks/use-disabled/index.js +104 -0
- package/build-module/hooks/use-disabled/index.js.map +1 -0
- package/build-module/hooks/use-dragging/index.js +12 -11
- package/build-module/hooks/use-dragging/index.js.map +1 -1
- package/build-module/hooks/use-drop-zone/index.js +10 -9
- package/build-module/hooks/use-drop-zone/index.js.map +1 -1
- package/build-module/hooks/use-fixed-window-list/index.js +164 -0
- package/build-module/hooks/use-fixed-window-list/index.js.map +1 -0
- package/build-module/hooks/use-focus-on-mount/index.js +2 -1
- package/build-module/hooks/use-focus-on-mount/index.js.map +1 -1
- package/build-module/hooks/use-instance-id/index.js +2 -1
- package/build-module/hooks/use-instance-id/index.js.map +1 -1
- package/build-module/hooks/use-keyboard-shortcut/index.js +17 -15
- package/build-module/hooks/use-keyboard-shortcut/index.js.map +1 -1
- package/build-module/hooks/use-preferred-color-scheme/index.android.js +4 -3
- package/build-module/hooks/use-preferred-color-scheme/index.android.js.map +1 -1
- package/build-module/hooks/use-previous/index.js +3 -14
- package/build-module/hooks/use-previous/index.js.map +1 -1
- package/build-module/hooks/use-resize-observer/index.native.js +4 -3
- package/build-module/hooks/use-resize-observer/index.native.js.map +1 -1
- package/build-module/hooks/use-viewport-match/index.js +2 -1
- package/build-module/hooks/use-viewport-match/index.js.map +1 -1
- package/build-module/hooks/use-warn-on-change/index.js +5 -2
- package/build-module/hooks/use-warn-on-change/index.js.map +1 -1
- package/build-module/index.js +2 -0
- package/build-module/index.js.map +1 -1
- package/build-types/higher-order/with-state/index.d.ts.map +1 -1
- package/build-types/hooks/use-constrained-tabbing/index.d.ts.map +1 -1
- package/build-types/hooks/use-copy-on-click/index.d.ts.map +1 -1
- package/build-types/hooks/use-disabled/index.d.ts +23 -0
- package/build-types/hooks/use-disabled/index.d.ts.map +1 -0
- package/build-types/hooks/use-fixed-window-list/index.d.ts +57 -0
- package/build-types/hooks/use-fixed-window-list/index.d.ts.map +1 -0
- package/build-types/hooks/use-previous/index.d.ts +2 -4
- package/build-types/hooks/use-previous/index.d.ts.map +1 -1
- package/build-types/index.d.ts +2 -0
- package/package.json +9 -9
- package/src/higher-order/with-state/index.js +1 -0
- package/src/hooks/use-constrained-tabbing/index.js +39 -33
- package/src/hooks/use-copy-on-click/index.js +1 -2
- package/src/hooks/use-disabled/index.js +113 -0
- package/src/hooks/use-disabled/test/index.js +90 -0
- package/src/hooks/use-fixed-window-list/index.js +174 -0
- package/src/hooks/use-previous/index.ts +24 -0
- package/src/index.js +2 -0
- package/tsconfig.tsbuildinfo +1 -1
- 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
|
-
* @
|
|
5
|
+
* @param value The value to track.
|
|
6
6
|
*
|
|
7
|
-
* @
|
|
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.
|
|
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"}
|
package/build-types/index.d.ts
CHANGED
|
@@ -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
|
+
"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.
|
|
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.
|
|
36
|
-
"@wordpress/dom": "^3.2.
|
|
37
|
-
"@wordpress/element": "^4.0.
|
|
38
|
-
"@wordpress/is-shallow-equal": "^4.2.0",
|
|
39
|
-
"@wordpress/keycodes": "^3.2.
|
|
40
|
-
"@wordpress/priority-queue": "^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": "
|
|
50
|
+
"gitHead": "51c7917ea7fac72953702f24d6daac87d99e7617"
|
|
51
51
|
}
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { TAB } from '@wordpress/keycodes';
|
|
5
5
|
import { focus } from '@wordpress/dom';
|
|
6
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 (
|
|
41
|
+
if ( keyCode !== TAB ) {
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
const
|
|
44
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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: '
|
|
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';
|