@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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds a callback to a shared `addEventListener`. Only one underlying
|
|
3
|
+
* native listener is attached per (root, event type, phase); subscribers
|
|
4
|
+
* join an in-JS registry that dispatches events along the DOM ancestry
|
|
5
|
+
* of `event.target`.
|
|
6
|
+
*
|
|
7
|
+
* The model mirrors React's synthetic event system: a single root
|
|
8
|
+
* listener handles every event of a given type, and callbacks bound to
|
|
9
|
+
* an `Element` only fire when that element is on the target's path.
|
|
10
|
+
* Callbacks bound to a `Document` always fire (document is the root of
|
|
11
|
+
* every event in that document); callbacks bound to a `Window` always
|
|
12
|
+
* fire as a flat fan-out, since `window` isn't on the DOM tree.
|
|
13
|
+
*
|
|
14
|
+
* @param target `Element`, `Document`, or `Window` to bind the
|
|
15
|
+
* callback to. For `Element`, the callback only fires
|
|
16
|
+
* when the event happens on the element or a
|
|
17
|
+
* descendant.
|
|
18
|
+
* @param eventType DOM event name.
|
|
19
|
+
* @param callback Listener to be invoked with the event.
|
|
20
|
+
* @param capture Use the capture phase. Required when ancestor
|
|
21
|
+
* listeners gate on `event.defaultPrevented`, since a
|
|
22
|
+
* bubble-phase root listener fires after them. Defaults
|
|
23
|
+
* to `false`.
|
|
24
|
+
* @return Unsubscribe function.
|
|
25
|
+
*/
|
|
26
|
+
// root -> eventTypeKey -> subscribedTarget -> Set<callback>
|
|
27
|
+
//
|
|
28
|
+
// Inner registry is a `WeakMap`: element subscribers are held weakly so
|
|
29
|
+
// an iframe removal lets the iframe's Elements (and through them, its
|
|
30
|
+
// `ownerDocument`) be garbage-collected. The native listener is
|
|
31
|
+
// attached to the document itself, so it goes when the document goes.
|
|
32
|
+
const registries = new WeakMap<
|
|
33
|
+
EventTarget,
|
|
34
|
+
Map< string, WeakMap< EventTarget, Set< EventListener > > >
|
|
35
|
+
>();
|
|
36
|
+
|
|
37
|
+
export default function subscribeDelegatedListener(
|
|
38
|
+
target: EventTarget,
|
|
39
|
+
eventType: string,
|
|
40
|
+
callback: EventListener,
|
|
41
|
+
capture: boolean = false
|
|
42
|
+
): () => void {
|
|
43
|
+
// Where the native listener is attached:
|
|
44
|
+
// Element → its `ownerDocument`
|
|
45
|
+
// Document → itself (own `ownerDocument` is `null`)
|
|
46
|
+
// Window → itself (no `ownerDocument` property)
|
|
47
|
+
// `undefined` (Window) → use a fan-out branch on dispatch since
|
|
48
|
+
// events bubble *to* window but never *from* it via `parentNode`.
|
|
49
|
+
const ownerDoc = ( target as Node ).ownerDocument;
|
|
50
|
+
const root = ownerDoc ?? target;
|
|
51
|
+
const isWindow = ownerDoc === undefined;
|
|
52
|
+
|
|
53
|
+
let perRoot = registries.get( root );
|
|
54
|
+
if ( ! perRoot ) {
|
|
55
|
+
perRoot = new Map();
|
|
56
|
+
registries.set( root, perRoot );
|
|
57
|
+
}
|
|
58
|
+
const key = capture ? `${ eventType }:capture` : eventType;
|
|
59
|
+
let perEvent = perRoot.get( key );
|
|
60
|
+
if ( ! perEvent ) {
|
|
61
|
+
perEvent = new WeakMap< EventTarget, Set< EventListener > >();
|
|
62
|
+
perRoot.set( key, perEvent );
|
|
63
|
+
const subscribers = perEvent;
|
|
64
|
+
root.addEventListener(
|
|
65
|
+
eventType,
|
|
66
|
+
( event ) => {
|
|
67
|
+
if ( isWindow ) {
|
|
68
|
+
// Window has no DOM ancestry — all subscribers share
|
|
69
|
+
// the window key; fetch its set and fan out.
|
|
70
|
+
const set = subscribers.get( root );
|
|
71
|
+
if ( set ) {
|
|
72
|
+
for ( const cb of set ) {
|
|
73
|
+
cb( event );
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Walk the target → root ancestry, dispatching callbacks
|
|
79
|
+
// for any node in the path. Bubble order matches the walk
|
|
80
|
+
// direction, so dispatch inline (no path array). Capture
|
|
81
|
+
// has to materialise the path to iterate in reverse.
|
|
82
|
+
if ( capture ) {
|
|
83
|
+
const path: Array< Node | Document > = [];
|
|
84
|
+
let current: Node | null = event.target as Node | null;
|
|
85
|
+
while ( current ) {
|
|
86
|
+
path.push( current );
|
|
87
|
+
if ( current === root ) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
current = current.parentNode;
|
|
91
|
+
}
|
|
92
|
+
for ( let i = path.length - 1; i >= 0; i-- ) {
|
|
93
|
+
const set = subscribers.get( path[ i ] );
|
|
94
|
+
if ( set ) {
|
|
95
|
+
for ( const cb of set ) {
|
|
96
|
+
cb( event );
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
let current: Node | null = event.target as Node | null;
|
|
102
|
+
while ( current ) {
|
|
103
|
+
const set = subscribers.get( current );
|
|
104
|
+
if ( set ) {
|
|
105
|
+
for ( const cb of set ) {
|
|
106
|
+
cb( event );
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if ( current === root ) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
current = current.parentNode;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
capture
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
let set = perEvent.get( target );
|
|
120
|
+
if ( ! set ) {
|
|
121
|
+
set = new Set();
|
|
122
|
+
perEvent.set( target, set );
|
|
123
|
+
}
|
|
124
|
+
set.add( callback );
|
|
125
|
+
return () => {
|
|
126
|
+
set.delete( callback );
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies
|
|
3
|
+
*/
|
|
4
|
+
import subscribeDelegatedListener from '..';
|
|
5
|
+
|
|
6
|
+
describe( 'subscribeDelegatedListener', () => {
|
|
7
|
+
let root;
|
|
8
|
+
let target;
|
|
9
|
+
|
|
10
|
+
beforeEach( () => {
|
|
11
|
+
// Build a nested DOM:
|
|
12
|
+
// document.body > #outer > #inner > #leaf
|
|
13
|
+
root = document.createElement( 'div' );
|
|
14
|
+
root.id = 'outer';
|
|
15
|
+
const inner = document.createElement( 'div' );
|
|
16
|
+
inner.id = 'inner';
|
|
17
|
+
target = document.createElement( 'span' );
|
|
18
|
+
target.id = 'leaf';
|
|
19
|
+
inner.appendChild( target );
|
|
20
|
+
root.appendChild( inner );
|
|
21
|
+
document.body.appendChild( root );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
afterEach( () => {
|
|
25
|
+
document.body.removeChild( root );
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
function fire( element, type = 'click', init = { bubbles: true } ) {
|
|
29
|
+
element.dispatchEvent( new Event( type, init ) );
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test( 'invokes element subscriber when event fires on that element', () => {
|
|
33
|
+
const cb = jest.fn();
|
|
34
|
+
subscribeDelegatedListener( target, 'click', cb );
|
|
35
|
+
fire( target );
|
|
36
|
+
expect( cb ).toHaveBeenCalledTimes( 1 );
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
test( 'invokes element subscriber when event fires on a descendant', () => {
|
|
40
|
+
const cb = jest.fn();
|
|
41
|
+
const child = document.createElement( 'b' );
|
|
42
|
+
target.appendChild( child );
|
|
43
|
+
subscribeDelegatedListener( target, 'click', cb );
|
|
44
|
+
fire( child );
|
|
45
|
+
expect( cb ).toHaveBeenCalledTimes( 1 );
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
test( 'does not invoke element subscriber when event fires outside its subtree', () => {
|
|
49
|
+
const cb = jest.fn();
|
|
50
|
+
const sibling = document.createElement( 'div' );
|
|
51
|
+
document.body.appendChild( sibling );
|
|
52
|
+
subscribeDelegatedListener( target, 'click', cb );
|
|
53
|
+
fire( sibling );
|
|
54
|
+
expect( cb ).not.toHaveBeenCalled();
|
|
55
|
+
document.body.removeChild( sibling );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
test( 'bubble phase: nested subscribers fire inner-to-outer', () => {
|
|
59
|
+
const order = [];
|
|
60
|
+
subscribeDelegatedListener( root, 'click', () =>
|
|
61
|
+
order.push( 'outer' )
|
|
62
|
+
);
|
|
63
|
+
subscribeDelegatedListener( target, 'click', () =>
|
|
64
|
+
order.push( 'leaf' )
|
|
65
|
+
);
|
|
66
|
+
fire( target );
|
|
67
|
+
expect( order ).toEqual( [ 'leaf', 'outer' ] );
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
test( 'capture phase: nested subscribers fire outer-to-inner', () => {
|
|
71
|
+
const order = [];
|
|
72
|
+
subscribeDelegatedListener(
|
|
73
|
+
root,
|
|
74
|
+
'click',
|
|
75
|
+
() => order.push( 'outer' ),
|
|
76
|
+
true
|
|
77
|
+
);
|
|
78
|
+
subscribeDelegatedListener(
|
|
79
|
+
target,
|
|
80
|
+
'click',
|
|
81
|
+
() => order.push( 'leaf' ),
|
|
82
|
+
true
|
|
83
|
+
);
|
|
84
|
+
fire( target );
|
|
85
|
+
expect( order ).toEqual( [ 'outer', 'leaf' ] );
|
|
86
|
+
} );
|
|
87
|
+
|
|
88
|
+
test( 'capture and bubble registries are independent', () => {
|
|
89
|
+
const captureCb = jest.fn();
|
|
90
|
+
const bubbleCb = jest.fn();
|
|
91
|
+
subscribeDelegatedListener( target, 'click', captureCb, true );
|
|
92
|
+
subscribeDelegatedListener( target, 'click', bubbleCb, false );
|
|
93
|
+
fire( target );
|
|
94
|
+
expect( captureCb ).toHaveBeenCalledTimes( 1 );
|
|
95
|
+
expect( bubbleCb ).toHaveBeenCalledTimes( 1 );
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
test( 'document subscriber always fires for events in the document', () => {
|
|
99
|
+
const cb = jest.fn();
|
|
100
|
+
subscribeDelegatedListener( document, 'click', cb );
|
|
101
|
+
fire( target );
|
|
102
|
+
expect( cb ).toHaveBeenCalledTimes( 1 );
|
|
103
|
+
} );
|
|
104
|
+
|
|
105
|
+
test( 'window subscriber fans out independently of element subscribers', () => {
|
|
106
|
+
const winCb = jest.fn();
|
|
107
|
+
const elCb = jest.fn();
|
|
108
|
+
subscribeDelegatedListener( window, 'resize', winCb );
|
|
109
|
+
subscribeDelegatedListener( target, 'resize', elCb );
|
|
110
|
+
window.dispatchEvent( new Event( 'resize' ) );
|
|
111
|
+
expect( winCb ).toHaveBeenCalledTimes( 1 );
|
|
112
|
+
// Element subscriber doesn't see a resize on window.
|
|
113
|
+
expect( elCb ).not.toHaveBeenCalled();
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
test( 'multiple callbacks for the same element all fire', () => {
|
|
117
|
+
const a = jest.fn();
|
|
118
|
+
const b = jest.fn();
|
|
119
|
+
subscribeDelegatedListener( target, 'click', a );
|
|
120
|
+
subscribeDelegatedListener( target, 'click', b );
|
|
121
|
+
fire( target );
|
|
122
|
+
expect( a ).toHaveBeenCalledTimes( 1 );
|
|
123
|
+
expect( b ).toHaveBeenCalledTimes( 1 );
|
|
124
|
+
} );
|
|
125
|
+
|
|
126
|
+
test( 'unsubscribe stops further dispatch', () => {
|
|
127
|
+
const cb = jest.fn();
|
|
128
|
+
const unsub = subscribeDelegatedListener( target, 'click', cb );
|
|
129
|
+
fire( target );
|
|
130
|
+
expect( cb ).toHaveBeenCalledTimes( 1 );
|
|
131
|
+
unsub();
|
|
132
|
+
fire( target );
|
|
133
|
+
expect( cb ).toHaveBeenCalledTimes( 1 );
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
test( 'attaches one native listener per (root, eventType, phase) regardless of subscriber count', () => {
|
|
137
|
+
const spy = jest.spyOn( document, 'addEventListener' );
|
|
138
|
+
// First subscribe attaches the native listener; subsequent ones
|
|
139
|
+
// for the same key share it.
|
|
140
|
+
subscribeDelegatedListener( target, 'mousemove', jest.fn() );
|
|
141
|
+
subscribeDelegatedListener( root, 'mousemove', jest.fn() );
|
|
142
|
+
subscribeDelegatedListener( document, 'mousemove', jest.fn() );
|
|
143
|
+
const nativeMousemoves = spy.mock.calls.filter(
|
|
144
|
+
( [ type, , opts ] ) => type === 'mousemove' && ! opts // bubble-phase only
|
|
145
|
+
).length;
|
|
146
|
+
expect( nativeMousemoves ).toBe( 1 );
|
|
147
|
+
spy.mockRestore();
|
|
148
|
+
} );
|
|
149
|
+
|
|
150
|
+
test( 'cross-realm Document/Window: accepts targets from a different realm', () => {
|
|
151
|
+
// Simulate an iframe document — a Document from a different realm
|
|
152
|
+
// would not be `instanceof Document` of the parent realm. Replicate
|
|
153
|
+
// that by passing a duck-typed Document-like object.
|
|
154
|
+
const iframe = document.createElement( 'iframe' );
|
|
155
|
+
document.body.appendChild( iframe );
|
|
156
|
+
const iframeDoc = iframe.contentDocument;
|
|
157
|
+
const iframeTarget = iframeDoc.createElement( 'span' );
|
|
158
|
+
iframeDoc.body.appendChild( iframeTarget );
|
|
159
|
+
|
|
160
|
+
const cb = jest.fn();
|
|
161
|
+
expect( () =>
|
|
162
|
+
subscribeDelegatedListener( iframeTarget, 'click', cb )
|
|
163
|
+
).not.toThrow();
|
|
164
|
+
iframeTarget.dispatchEvent(
|
|
165
|
+
new iframe.contentWindow.Event( 'click', { bubbles: true } )
|
|
166
|
+
);
|
|
167
|
+
expect( cb ).toHaveBeenCalledTimes( 1 );
|
|
168
|
+
document.body.removeChild( iframe );
|
|
169
|
+
} );
|
|
170
|
+
} );
|