@wordpress/compose 7.45.1-next.v.202605131032.0 → 8.0.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/CHANGELOG.md +13 -0
- package/README.md +4 -0
- package/build/hooks/use-copy-on-click/index.cjs +1 -1
- package/build/hooks/use-copy-on-click/index.cjs.map +2 -2
- package/build/hooks/use-copy-to-clipboard/index.cjs +8 -10
- package/build/hooks/use-copy-to-clipboard/index.cjs.map +2 -2
- package/build/hooks/use-dialog/index.cjs +8 -13
- package/build/hooks/use-dialog/index.cjs.map +2 -2
- package/build/hooks/use-disabled/index.cjs +2 -2
- package/build/hooks/use-disabled/index.cjs.map +2 -2
- package/build/hooks/use-focus-return/index.cjs.map +1 -1
- package/build/hooks/use-media-query/index.cjs +42 -36
- package/build/hooks/use-media-query/index.cjs.map +2 -2
- package/build/hooks/use-merge-refs/index.cjs.map +2 -2
- package/build/hooks/use-resize-observer/use-resize-observer.cjs.map +2 -2
- package/build/index.cjs +3 -0
- package/build/index.cjs.map +2 -2
- package/build/lock-unlock.cjs +37 -0
- package/build/lock-unlock.cjs.map +7 -0
- package/build/private-apis.cjs +46 -0
- package/build/private-apis.cjs.map +7 -0
- package/build/utils/subscribe-delegated-listener/index.cjs +101 -0
- package/build/utils/subscribe-delegated-listener/index.cjs.map +7 -0
- package/build-module/hooks/use-copy-on-click/index.mjs +2 -2
- package/build-module/hooks/use-copy-on-click/index.mjs.map +2 -2
- package/build-module/hooks/use-copy-to-clipboard/index.mjs +6 -8
- package/build-module/hooks/use-copy-to-clipboard/index.mjs.map +2 -2
- package/build-module/hooks/use-dialog/index.mjs +8 -13
- package/build-module/hooks/use-dialog/index.mjs.map +2 -2
- package/build-module/hooks/use-disabled/index.mjs +2 -2
- package/build-module/hooks/use-disabled/index.mjs.map +2 -2
- package/build-module/hooks/use-focus-return/index.mjs.map +1 -1
- package/build-module/hooks/use-media-query/index.mjs +43 -37
- package/build-module/hooks/use-media-query/index.mjs.map +2 -2
- package/build-module/hooks/use-merge-refs/index.mjs.map +2 -2
- package/build-module/hooks/use-resize-observer/use-resize-observer.mjs.map +2 -2
- package/build-module/index.mjs +2 -0
- package/build-module/index.mjs.map +2 -2
- package/build-module/lock-unlock.mjs +11 -0
- package/build-module/lock-unlock.mjs.map +7 -0
- package/build-module/private-apis.mjs +11 -0
- package/build-module/private-apis.mjs.map +7 -0
- package/build-module/utils/subscribe-delegated-listener/index.mjs +80 -0
- package/build-module/utils/subscribe-delegated-listener/index.mjs.map +7 -0
- package/build-types/higher-order/with-safe-timeout/index.d.ts +1 -3
- package/build-types/higher-order/with-safe-timeout/index.d.ts.map +1 -1
- package/build-types/hooks/use-copy-to-clipboard/index.d.ts +2 -2
- package/build-types/hooks/use-copy-to-clipboard/index.d.ts.map +1 -1
- package/build-types/hooks/use-dialog/index.d.ts +8 -2
- package/build-types/hooks/use-dialog/index.d.ts.map +1 -1
- package/build-types/hooks/use-media-query/index.d.ts.map +1 -1
- package/build-types/hooks/use-merge-refs/index.d.ts.map +1 -1
- package/build-types/index.d.ts +1 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/lock-unlock.d.ts +2 -0
- package/build-types/lock-unlock.d.ts.map +1 -0
- package/build-types/private-apis.d.ts +5 -0
- package/build-types/private-apis.d.ts.map +1 -0
- package/build-types/utils/subscribe-delegated-listener/index.d.ts +27 -0
- package/build-types/utils/subscribe-delegated-listener/index.d.ts.map +1 -0
- package/package.json +11 -10
- package/src/hooks/use-copy-on-click/index.ts +2 -2
- package/src/hooks/use-copy-to-clipboard/index.ts +8 -8
- package/src/hooks/use-copy-to-clipboard/test/index.tsx +33 -5
- package/src/hooks/use-dialog/README.md +52 -18
- package/src/hooks/use-dialog/index.ts +30 -20
- package/src/hooks/use-dialog/test/index.tsx +162 -34
- package/src/hooks/use-disabled/index.ts +3 -3
- package/src/hooks/use-disabled/test/index.js +4 -4
- package/src/hooks/use-focus-outside/index.native.js +2 -2
- package/src/hooks/use-focus-return/index.js +2 -2
- package/src/hooks/use-media-query/index.ts +54 -52
- package/src/hooks/use-merge-refs/index.ts +2 -2
- package/src/hooks/use-resize-observer/use-resize-observer.ts +1 -1
- package/src/index.js +3 -0
- package/src/lock-unlock.ts +10 -0
- package/src/private-apis.ts +13 -0
- package/src/utils/subscribe-delegated-listener/index.ts +128 -0
- package/src/utils/subscribe-delegated-listener/test/index.js +170 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
KeyboardEvent,
|
|
6
|
+
KeyboardEventHandler,
|
|
7
|
+
RefCallback,
|
|
8
|
+
SyntheticEvent,
|
|
9
|
+
} from 'react';
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* WordPress dependencies
|
|
8
13
|
*/
|
|
9
14
|
import { useRef, useEffect, useCallback } from '@wordpress/element';
|
|
10
|
-
import { ESCAPE } from '@wordpress/keycodes';
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Internal dependencies
|
|
@@ -43,6 +47,10 @@ type DialogOptions = {
|
|
|
43
47
|
*/
|
|
44
48
|
constrainTabbing?: boolean;
|
|
45
49
|
onClose?: () => void;
|
|
50
|
+
/**
|
|
51
|
+
* Optional `onKeyDown` handler, merged with the built-in one.
|
|
52
|
+
*/
|
|
53
|
+
onKeyDown?: KeyboardEventHandler< HTMLElement >;
|
|
46
54
|
/**
|
|
47
55
|
* Use the `onClose` prop instead.
|
|
48
56
|
*
|
|
@@ -56,7 +64,9 @@ type DialogOptions = {
|
|
|
56
64
|
|
|
57
65
|
type useDialogReturn = [
|
|
58
66
|
RefCallback< HTMLElement >,
|
|
59
|
-
ReturnType< typeof useFocusOutside > &
|
|
67
|
+
ReturnType< typeof useFocusOutside > & {
|
|
68
|
+
onKeyDown: ( event: KeyboardEvent< HTMLElement > ) => void;
|
|
69
|
+
} & Pick< HTMLElement, 'tabIndex' >,
|
|
60
70
|
];
|
|
61
71
|
|
|
62
72
|
/**
|
|
@@ -86,23 +96,23 @@ function useDialog( options: DialogOptions ): useDialogReturn {
|
|
|
86
96
|
currentOptions.current.onClose();
|
|
87
97
|
}
|
|
88
98
|
} );
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
// Close on Escape via a React `onKeyDown` (rather than a native listener)
|
|
100
|
+
// so portaled descendants that handle Escape and call
|
|
101
|
+
// `event.stopPropagation()` correctly prevent the dialog from closing.
|
|
102
|
+
// See https://github.com/WordPress/gutenberg/issues/78432.
|
|
103
|
+
const onKeyDown = useCallback( ( event: KeyboardEvent< HTMLElement > ) => {
|
|
104
|
+
// Let the consumer-provided handler (if any) run first so it can
|
|
105
|
+
// call `preventDefault()` to opt out of close-on-Escape.
|
|
106
|
+
currentOptions.current?.onKeyDown?.( event );
|
|
107
|
+
if (
|
|
108
|
+
event.key === 'Escape' &&
|
|
109
|
+
! event.defaultPrevented &&
|
|
110
|
+
currentOptions.current?.onClose
|
|
111
|
+
) {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
event.stopPropagation();
|
|
114
|
+
currentOptions.current.onClose();
|
|
92
115
|
}
|
|
93
|
-
|
|
94
|
-
node.addEventListener( 'keydown', ( event: KeyboardEvent ) => {
|
|
95
|
-
// Close on escape.
|
|
96
|
-
if (
|
|
97
|
-
event.keyCode === ESCAPE &&
|
|
98
|
-
! event.defaultPrevented &&
|
|
99
|
-
currentOptions.current?.onClose
|
|
100
|
-
) {
|
|
101
|
-
event.preventDefault();
|
|
102
|
-
event.stopPropagation();
|
|
103
|
-
currentOptions.current.onClose();
|
|
104
|
-
}
|
|
105
|
-
} );
|
|
106
116
|
}, [] );
|
|
107
117
|
|
|
108
118
|
return [
|
|
@@ -110,10 +120,10 @@ function useDialog( options: DialogOptions ): useDialogReturn {
|
|
|
110
120
|
constrainTabbing ? constrainedTabbingRef : null,
|
|
111
121
|
options.focusOnMount !== false ? focusReturnRef : null,
|
|
112
122
|
options.focusOnMount !== false ? focusOnMountRef : null,
|
|
113
|
-
closeOnEscapeRef,
|
|
114
123
|
] ),
|
|
115
124
|
{
|
|
116
125
|
...focusOutsideProps,
|
|
126
|
+
onKeyDown,
|
|
117
127
|
tabIndex: -1,
|
|
118
128
|
},
|
|
119
129
|
];
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
|
-
import { render, screen
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import { createPortal } from 'react-dom';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Internal dependencies
|
|
@@ -22,44 +24,58 @@ function Dialog( { onClose }: { onClose?: () => void } ) {
|
|
|
22
24
|
);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
// `useDialog` currently detects the Escape key via the deprecated `keyCode`
|
|
26
|
-
// property (`event.keyCode === 27`). In jsdom, `userEvent.keyboard('[Escape]')`
|
|
27
|
-
// does not set `keyCode` to 27, so we use `fireEvent` to control the event
|
|
28
|
-
// shape. Once the hook is updated to use `event.key === 'Escape'`, these tests
|
|
29
|
-
// should be rewritten to use `userEvent` for more realistic event simulation.
|
|
30
|
-
function pressEscapeOn( element: HTMLElement ) {
|
|
31
|
-
fireEvent.keyDown( element, { keyCode: 27, key: 'Escape' } );
|
|
32
|
-
}
|
|
33
|
-
|
|
34
27
|
describe( 'useDialog', () => {
|
|
35
|
-
it( 'should call onClose when Escape is pressed', () => {
|
|
28
|
+
it( 'should call onClose when Escape is pressed', async () => {
|
|
29
|
+
const user = userEvent.setup();
|
|
36
30
|
const onClose = jest.fn();
|
|
37
31
|
|
|
38
32
|
render( <Dialog onClose={ onClose } /> );
|
|
39
33
|
|
|
40
|
-
|
|
34
|
+
screen.getByRole( 'dialog' ).focus();
|
|
35
|
+
await user.keyboard( '[Escape]' );
|
|
41
36
|
|
|
42
37
|
expect( onClose ).toHaveBeenCalledTimes( 1 );
|
|
43
38
|
} );
|
|
44
39
|
|
|
45
|
-
it( 'should not call onClose when Escape is pressed if defaultPrevented', () => {
|
|
40
|
+
it( 'should not call onClose when Escape is pressed if defaultPrevented', async () => {
|
|
41
|
+
const user = userEvent.setup();
|
|
46
42
|
const onClose = jest.fn();
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
function DialogWithChild() {
|
|
45
|
+
const [ ref, props ] = useDialog( {
|
|
46
|
+
onClose,
|
|
47
|
+
focusOnMount: false,
|
|
48
|
+
} );
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
ref={ ref }
|
|
52
|
+
{ ...props }
|
|
53
|
+
role="dialog"
|
|
54
|
+
aria-label="Test dialog"
|
|
55
|
+
>
|
|
56
|
+
<button
|
|
57
|
+
onKeyDown={ ( event ) => {
|
|
58
|
+
if ( event.key === 'Escape' ) {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
}
|
|
61
|
+
} }
|
|
62
|
+
>
|
|
63
|
+
Inside
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
render( <DialogWithChild /> );
|
|
49
70
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
keyCode: 27,
|
|
53
|
-
bubbles: true,
|
|
54
|
-
cancelable: true,
|
|
55
|
-
} );
|
|
56
|
-
event.preventDefault();
|
|
57
|
-
dialog.dispatchEvent( event );
|
|
71
|
+
screen.getByRole( 'button', { name: 'Inside' } ).focus();
|
|
72
|
+
await user.keyboard( '[Escape]' );
|
|
58
73
|
|
|
59
74
|
expect( onClose ).not.toHaveBeenCalled();
|
|
60
75
|
} );
|
|
61
76
|
|
|
62
|
-
it( 'should stop Escape event from propagating to parent elements', () => {
|
|
77
|
+
it( 'should stop Escape event from propagating to parent elements', async () => {
|
|
78
|
+
const user = userEvent.setup();
|
|
63
79
|
const onClose = jest.fn();
|
|
64
80
|
const parentKeyDownHandler = jest.fn();
|
|
65
81
|
|
|
@@ -70,13 +86,15 @@ describe( 'useDialog', () => {
|
|
|
70
86
|
</div>
|
|
71
87
|
);
|
|
72
88
|
|
|
73
|
-
|
|
89
|
+
screen.getByRole( 'button', { name: 'Inside' } ).focus();
|
|
90
|
+
await user.keyboard( '[Escape]' );
|
|
74
91
|
|
|
75
92
|
expect( onClose ).toHaveBeenCalledTimes( 1 );
|
|
76
93
|
expect( parentKeyDownHandler ).not.toHaveBeenCalled();
|
|
77
94
|
} );
|
|
78
95
|
|
|
79
|
-
it( 'should let Escape propagate when there is no onClose handler', () => {
|
|
96
|
+
it( 'should let Escape propagate when there is no onClose handler', async () => {
|
|
97
|
+
const user = userEvent.setup();
|
|
80
98
|
const parentKeyDownHandler = jest.fn();
|
|
81
99
|
|
|
82
100
|
render(
|
|
@@ -86,12 +104,62 @@ describe( 'useDialog', () => {
|
|
|
86
104
|
</div>
|
|
87
105
|
);
|
|
88
106
|
|
|
89
|
-
|
|
107
|
+
screen.getByRole( 'button', { name: 'Inside' } ).focus();
|
|
108
|
+
await user.keyboard( '[Escape]' );
|
|
90
109
|
|
|
91
110
|
expect( parentKeyDownHandler ).toHaveBeenCalledTimes( 1 );
|
|
92
111
|
} );
|
|
93
112
|
|
|
94
|
-
it( 'should close
|
|
113
|
+
it( 'should not close when a portaled descendant handles Escape and stops propagation', async () => {
|
|
114
|
+
const user = userEvent.setup();
|
|
115
|
+
const onClose = jest.fn();
|
|
116
|
+
const descendantHandled = jest.fn();
|
|
117
|
+
|
|
118
|
+
const portalTarget = document.createElement( 'div' );
|
|
119
|
+
document.body.appendChild( portalTarget );
|
|
120
|
+
|
|
121
|
+
function DialogWithPortaledChild() {
|
|
122
|
+
const [ ref, props ] = useDialog( {
|
|
123
|
+
onClose,
|
|
124
|
+
focusOnMount: false,
|
|
125
|
+
} );
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
ref={ ref }
|
|
129
|
+
{ ...props }
|
|
130
|
+
role="dialog"
|
|
131
|
+
aria-label="Test dialog"
|
|
132
|
+
>
|
|
133
|
+
{ createPortal(
|
|
134
|
+
<button
|
|
135
|
+
onKeyDown={ ( event ) => {
|
|
136
|
+
if ( event.key === 'Escape' ) {
|
|
137
|
+
descendantHandled();
|
|
138
|
+
event.stopPropagation();
|
|
139
|
+
}
|
|
140
|
+
} }
|
|
141
|
+
>
|
|
142
|
+
Portaled
|
|
143
|
+
</button>,
|
|
144
|
+
portalTarget
|
|
145
|
+
) }
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
render( <DialogWithPortaledChild /> );
|
|
151
|
+
|
|
152
|
+
screen.getByText( 'Portaled' ).focus();
|
|
153
|
+
await user.keyboard( '[Escape]' );
|
|
154
|
+
|
|
155
|
+
expect( descendantHandled ).toHaveBeenCalledTimes( 1 );
|
|
156
|
+
expect( onClose ).not.toHaveBeenCalled();
|
|
157
|
+
|
|
158
|
+
document.body.removeChild( portalTarget );
|
|
159
|
+
} );
|
|
160
|
+
|
|
161
|
+
it( 'should close only the innermost dialog when nested', async () => {
|
|
162
|
+
const user = userEvent.setup();
|
|
95
163
|
const outerOnClose = jest.fn();
|
|
96
164
|
const innerOnClose = jest.fn();
|
|
97
165
|
|
|
@@ -126,22 +194,82 @@ describe( 'useDialog', () => {
|
|
|
126
194
|
|
|
127
195
|
render( <NestedDialogs /> );
|
|
128
196
|
|
|
129
|
-
|
|
197
|
+
screen.getByRole( 'button', { name: 'Focusable' } ).focus();
|
|
198
|
+
await user.keyboard( '[Escape]' );
|
|
130
199
|
|
|
131
200
|
expect( innerOnClose ).toHaveBeenCalledTimes( 1 );
|
|
132
201
|
expect( outerOnClose ).not.toHaveBeenCalled();
|
|
133
202
|
} );
|
|
134
203
|
|
|
135
|
-
it( 'should
|
|
204
|
+
it( 'should call the consumer-provided onKeyDown alongside close-on-Escape', async () => {
|
|
205
|
+
const user = userEvent.setup();
|
|
136
206
|
const onClose = jest.fn();
|
|
207
|
+
const consumerOnKeyDown = jest.fn();
|
|
137
208
|
|
|
138
|
-
|
|
209
|
+
function DialogWithConsumerOnKeyDown() {
|
|
210
|
+
const [ ref, props ] = useDialog( {
|
|
211
|
+
onClose,
|
|
212
|
+
onKeyDown: consumerOnKeyDown,
|
|
213
|
+
focusOnMount: false,
|
|
214
|
+
} );
|
|
215
|
+
return (
|
|
216
|
+
<div
|
|
217
|
+
ref={ ref }
|
|
218
|
+
{ ...props }
|
|
219
|
+
role="dialog"
|
|
220
|
+
aria-label="Test dialog"
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
render( <DialogWithConsumerOnKeyDown /> );
|
|
226
|
+
|
|
227
|
+
screen.getByRole( 'dialog' ).focus();
|
|
228
|
+
await user.keyboard( '[Escape]' );
|
|
229
|
+
|
|
230
|
+
expect( consumerOnKeyDown ).toHaveBeenCalledTimes( 1 );
|
|
231
|
+
expect( consumerOnKeyDown.mock.calls[ 0 ][ 0 ].key ).toBe( 'Escape' );
|
|
232
|
+
expect( onClose ).toHaveBeenCalledTimes( 1 );
|
|
233
|
+
} );
|
|
234
|
+
|
|
235
|
+
it( 'should let the consumer-provided onKeyDown opt out of close-on-Escape via preventDefault', async () => {
|
|
236
|
+
const user = userEvent.setup();
|
|
237
|
+
const onClose = jest.fn();
|
|
139
238
|
|
|
140
|
-
|
|
239
|
+
function DialogOptingOut() {
|
|
240
|
+
const [ ref, props ] = useDialog( {
|
|
241
|
+
onClose,
|
|
242
|
+
onKeyDown: ( event ) => event.preventDefault(),
|
|
243
|
+
focusOnMount: false,
|
|
244
|
+
} );
|
|
245
|
+
return (
|
|
246
|
+
<div
|
|
247
|
+
ref={ ref }
|
|
248
|
+
{ ...props }
|
|
249
|
+
role="dialog"
|
|
250
|
+
aria-label="Test dialog"
|
|
251
|
+
/>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
render( <DialogOptingOut /> );
|
|
256
|
+
|
|
257
|
+
screen.getByRole( 'dialog' ).focus();
|
|
258
|
+
await user.keyboard( '[Escape]' );
|
|
259
|
+
|
|
260
|
+
expect( onClose ).not.toHaveBeenCalled();
|
|
261
|
+
} );
|
|
262
|
+
|
|
263
|
+
it( 'should not call onClose for non-Escape keys', async () => {
|
|
264
|
+
const user = userEvent.setup();
|
|
265
|
+
const onClose = jest.fn();
|
|
266
|
+
|
|
267
|
+
render( <Dialog onClose={ onClose } /> );
|
|
141
268
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
269
|
+
screen.getByRole( 'dialog' ).focus();
|
|
270
|
+
await user.keyboard( '[Enter]' );
|
|
271
|
+
await user.keyboard( 'a' );
|
|
272
|
+
await user.keyboard( '[Tab]' );
|
|
145
273
|
|
|
146
274
|
expect( onClose ).not.toHaveBeenCalled();
|
|
147
275
|
} );
|
|
@@ -45,14 +45,14 @@ export default function useDisabled( {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** A variable keeping track of the previous updates in order to restore them. */
|
|
48
|
-
const updates:
|
|
48
|
+
const updates: ( () => void )[] = [];
|
|
49
49
|
const disable = () => {
|
|
50
50
|
node.childNodes.forEach( ( child ) => {
|
|
51
51
|
if ( ! ( child instanceof defaultView.HTMLElement ) ) {
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
|
-
if ( ! child.
|
|
55
|
-
child.setAttribute( 'inert', '
|
|
54
|
+
if ( ! child.hasAttribute( 'inert' ) ) {
|
|
55
|
+
child.setAttribute( 'inert', '' );
|
|
56
56
|
updates.push( () => {
|
|
57
57
|
child.removeAttribute( 'inert' );
|
|
58
58
|
} );
|
|
@@ -37,9 +37,9 @@ describe( 'useDisabled', () => {
|
|
|
37
37
|
const link = screen.getByRole( 'link' );
|
|
38
38
|
const p = screen.getByRole( 'document' );
|
|
39
39
|
|
|
40
|
-
expect( input ).toHaveAttribute( 'inert'
|
|
41
|
-
expect( link ).toHaveAttribute( 'inert'
|
|
42
|
-
expect( p ).toHaveAttribute( 'inert'
|
|
40
|
+
expect( input ).toHaveAttribute( 'inert' );
|
|
41
|
+
expect( link ).toHaveAttribute( 'inert' );
|
|
42
|
+
expect( p ).toHaveAttribute( 'inert' );
|
|
43
43
|
} );
|
|
44
44
|
|
|
45
45
|
it( 'will disable an element rendered in an update to the component', async () => {
|
|
@@ -52,7 +52,7 @@ describe( 'useDisabled', () => {
|
|
|
52
52
|
|
|
53
53
|
const button = screen.getByText( 'Button' );
|
|
54
54
|
await waitFor( () => {
|
|
55
|
-
expect( button ).toHaveAttribute( 'inert'
|
|
55
|
+
expect( button ).toHaveAttribute( 'inert' );
|
|
56
56
|
} );
|
|
57
57
|
} );
|
|
58
58
|
} );
|
|
@@ -55,7 +55,7 @@ function isFocusNormalizedButton( eventTarget ) {
|
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
* @typedef {React.
|
|
58
|
+
* @typedef {React.RefObject<FocusOutsideReactElement | undefined>} FocusOutsideRef
|
|
59
59
|
*/
|
|
60
60
|
|
|
61
61
|
/**
|
|
@@ -88,7 +88,7 @@ export default function useFocusOutside( onFocusOutside ) {
|
|
|
88
88
|
const preventBlurCheck = useRef( false );
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* @type {React.
|
|
91
|
+
* @type {React.RefObject<number | undefined>}
|
|
92
92
|
*/
|
|
93
93
|
const blurCheckTimeoutId = useRef();
|
|
94
94
|
|
|
@@ -29,9 +29,9 @@ let origin = null;
|
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
31
|
function useFocusReturn( onFocusReturn ) {
|
|
32
|
-
/** @type {React.
|
|
32
|
+
/** @type {React.RefObject<HTMLElement| null>} */
|
|
33
33
|
const ref = useRef( null );
|
|
34
|
-
/** @type {React.
|
|
34
|
+
/** @type {React.RefObject<Element | null>} */
|
|
35
35
|
const focusedBeforeMount = useRef( null );
|
|
36
36
|
const onFocusReturnRef = useRef( onFocusReturn );
|
|
37
37
|
useEffect( () => {
|
|
@@ -1,45 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { useSyncExternalStore } from '@wordpress/element';
|
|
5
5
|
|
|
6
|
-
type
|
|
6
|
+
type MQLSubscriber = {
|
|
7
|
+
subscribe: ( onStoreChange: () => void ) => () => void;
|
|
8
|
+
getValue: () => boolean;
|
|
9
|
+
};
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
// One subscriber per (window, query). The underlying MediaQueryList lives
|
|
12
|
+
// inside the subscriber's closure; a single `change` listener fans out to
|
|
13
|
+
// every React consumer via an in-JS `Set` to avoid the per-consumer
|
|
14
|
+
// `addEventListener` cost (~85 ms during a large-post editor mount).
|
|
15
|
+
const perWindowCache = new WeakMap< Window, Map< string, MQLSubscriber > >();
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* @param [query] Media Query.
|
|
15
|
-
*/
|
|
16
|
-
function getMediaQueryList(
|
|
17
|
-
view: Window,
|
|
18
|
-
query?: string
|
|
19
|
-
): MediaQueryList | null {
|
|
20
|
-
if ( ! query ) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const matchMediaCache: MQLCache = perWindowCache.get( view ) ?? new Map();
|
|
17
|
+
const EMPTY_SUBSCRIBER: MQLSubscriber = {
|
|
18
|
+
subscribe: () => () => {},
|
|
19
|
+
getValue: () => false,
|
|
20
|
+
};
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
function getMQLSubscriber( view: Window, query?: string ): MQLSubscriber {
|
|
23
|
+
if ( ! query || typeof view?.matchMedia !== 'function' ) {
|
|
24
|
+
return EMPTY_SUBSCRIBER;
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
let
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
let queryCache = perWindowCache.get( view );
|
|
28
|
+
if ( ! queryCache ) {
|
|
29
|
+
queryCache = new Map();
|
|
30
|
+
perWindowCache.set( view, queryCache );
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return match;
|
|
33
|
+
const cached = queryCache.get( query );
|
|
34
|
+
if ( cached ) {
|
|
35
|
+
return cached;
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
|
|
38
|
+
const mediaQueryList = view.matchMedia( query );
|
|
39
|
+
const listeners = new Set< () => void >();
|
|
40
|
+
const notify = () => {
|
|
41
|
+
for ( const listener of listeners ) {
|
|
42
|
+
listener();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const subscriber: MQLSubscriber = {
|
|
47
|
+
subscribe( onStoreChange ) {
|
|
48
|
+
if ( listeners.size === 0 ) {
|
|
49
|
+
// Avoid a fatal error when browsers don't support `addEventListener` on MediaQueryList.
|
|
50
|
+
mediaQueryList.addEventListener?.( 'change', notify );
|
|
51
|
+
}
|
|
52
|
+
listeners.add( onStoreChange );
|
|
53
|
+
return () => {
|
|
54
|
+
listeners.delete( onStoreChange );
|
|
55
|
+
if ( listeners.size === 0 ) {
|
|
56
|
+
mediaQueryList.removeEventListener?.( 'change', notify );
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
getValue() {
|
|
61
|
+
return mediaQueryList.matches;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
queryCache.set( query, subscriber );
|
|
66
|
+
return subscriber;
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
/**
|
|
@@ -53,29 +77,7 @@ export default function useMediaQuery(
|
|
|
53
77
|
query?: string,
|
|
54
78
|
view: Window = window
|
|
55
79
|
): boolean {
|
|
56
|
-
const source =
|
|
57
|
-
const mediaQueryList = getMediaQueryList( view, query );
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
subscribe( onStoreChange: any ) {
|
|
61
|
-
if ( ! mediaQueryList ) {
|
|
62
|
-
return () => {};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Avoid a fatal error when browsers don't support `addEventListener` on MediaQueryList.
|
|
66
|
-
mediaQueryList.addEventListener?.( 'change', onStoreChange );
|
|
67
|
-
return () => {
|
|
68
|
-
mediaQueryList.removeEventListener?.(
|
|
69
|
-
'change',
|
|
70
|
-
onStoreChange
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
|
-
},
|
|
74
|
-
getValue() {
|
|
75
|
-
return mediaQueryList?.matches ?? false;
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}, [ view, query ] );
|
|
80
|
+
const source = getMQLSubscriber( view, query );
|
|
79
81
|
|
|
80
82
|
return useSyncExternalStore(
|
|
81
83
|
source.subscribe,
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
4
|
import { useRef, useCallback, useLayoutEffect } from '@wordpress/element';
|
|
5
|
-
import type {
|
|
5
|
+
import type { Ref, RefCallback } from 'react';
|
|
6
6
|
|
|
7
7
|
function assignRef< T >( ref: Ref< T >, value: T ) {
|
|
8
8
|
if ( typeof ref === 'function' ) {
|
|
9
9
|
ref( value );
|
|
10
10
|
} else if ( ref && ref.hasOwnProperty( 'current' ) ) {
|
|
11
|
-
|
|
11
|
+
ref.current = value;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -18,7 +18,7 @@ export function useResizeObserver< T extends HTMLElement >(
|
|
|
18
18
|
): ( element?: T | null ) => void {
|
|
19
19
|
const callbackEvent = useEvent( callback );
|
|
20
20
|
|
|
21
|
-
const observedElementRef = useRef< T
|
|
21
|
+
const observedElementRef = useRef< T >( null );
|
|
22
22
|
const resizeObserverRef = useRef< ResizeObserver >( undefined );
|
|
23
23
|
return useEvent( ( element?: T | null ) => {
|
|
24
24
|
if ( element === observedElementRef.current ) {
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,9 @@ export * from './utils/throttle';
|
|
|
7
7
|
// The `ObservableMap` data structure
|
|
8
8
|
export * from './utils/observable-map';
|
|
9
9
|
|
|
10
|
+
// Private APIs.
|
|
11
|
+
export { privateApis } from './private-apis';
|
|
12
|
+
|
|
10
13
|
// The `compose` and `pipe` helpers (inspired by `flowRight` and `flow` from Lodash).
|
|
11
14
|
export { default as compose } from './higher-order/compose';
|
|
12
15
|
export { default as pipe } from './higher-order/pipe';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
|
|
5
|
+
|
|
6
|
+
export const { lock, unlock } =
|
|
7
|
+
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
|
|
8
|
+
'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
|
|
9
|
+
'@wordpress/compose'
|
|
10
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { lock } from './lock-unlock';
|
|
5
|
+
import subscribeDelegatedListener from './utils/subscribe-delegated-listener';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Private @wordpress/compose APIs.
|
|
9
|
+
*/
|
|
10
|
+
export const privateApis = {};
|
|
11
|
+
lock( privateApis, {
|
|
12
|
+
subscribeDelegatedListener,
|
|
13
|
+
} );
|