@wordpress/compose 7.46.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +4 -0
  3. package/build/hooks/use-copy-on-click/index.cjs +1 -1
  4. package/build/hooks/use-copy-on-click/index.cjs.map +2 -2
  5. package/build/hooks/use-copy-to-clipboard/index.cjs +8 -10
  6. package/build/hooks/use-copy-to-clipboard/index.cjs.map +2 -2
  7. package/build/hooks/use-dialog/index.cjs +8 -13
  8. package/build/hooks/use-dialog/index.cjs.map +2 -2
  9. package/build/hooks/use-disabled/index.cjs +2 -2
  10. package/build/hooks/use-disabled/index.cjs.map +2 -2
  11. package/build/hooks/use-focus-return/index.cjs.map +1 -1
  12. package/build/hooks/use-media-query/index.cjs +42 -36
  13. package/build/hooks/use-media-query/index.cjs.map +2 -2
  14. package/build/hooks/use-merge-refs/index.cjs.map +2 -2
  15. package/build/hooks/use-resize-observer/use-resize-observer.cjs.map +2 -2
  16. package/build/index.cjs +3 -0
  17. package/build/index.cjs.map +2 -2
  18. package/build/lock-unlock.cjs +37 -0
  19. package/build/lock-unlock.cjs.map +7 -0
  20. package/build/private-apis.cjs +46 -0
  21. package/build/private-apis.cjs.map +7 -0
  22. package/build/utils/subscribe-delegated-listener/index.cjs +101 -0
  23. package/build/utils/subscribe-delegated-listener/index.cjs.map +7 -0
  24. package/build-module/hooks/use-copy-on-click/index.mjs +2 -2
  25. package/build-module/hooks/use-copy-on-click/index.mjs.map +2 -2
  26. package/build-module/hooks/use-copy-to-clipboard/index.mjs +6 -8
  27. package/build-module/hooks/use-copy-to-clipboard/index.mjs.map +2 -2
  28. package/build-module/hooks/use-dialog/index.mjs +8 -13
  29. package/build-module/hooks/use-dialog/index.mjs.map +2 -2
  30. package/build-module/hooks/use-disabled/index.mjs +2 -2
  31. package/build-module/hooks/use-disabled/index.mjs.map +2 -2
  32. package/build-module/hooks/use-focus-return/index.mjs.map +1 -1
  33. package/build-module/hooks/use-media-query/index.mjs +43 -37
  34. package/build-module/hooks/use-media-query/index.mjs.map +2 -2
  35. package/build-module/hooks/use-merge-refs/index.mjs.map +2 -2
  36. package/build-module/hooks/use-resize-observer/use-resize-observer.mjs.map +2 -2
  37. package/build-module/index.mjs +2 -0
  38. package/build-module/index.mjs.map +2 -2
  39. package/build-module/lock-unlock.mjs +11 -0
  40. package/build-module/lock-unlock.mjs.map +7 -0
  41. package/build-module/private-apis.mjs +11 -0
  42. package/build-module/private-apis.mjs.map +7 -0
  43. package/build-module/utils/subscribe-delegated-listener/index.mjs +80 -0
  44. package/build-module/utils/subscribe-delegated-listener/index.mjs.map +7 -0
  45. package/build-types/higher-order/with-safe-timeout/index.d.ts +1 -3
  46. package/build-types/higher-order/with-safe-timeout/index.d.ts.map +1 -1
  47. package/build-types/hooks/use-copy-to-clipboard/index.d.ts +2 -2
  48. package/build-types/hooks/use-copy-to-clipboard/index.d.ts.map +1 -1
  49. package/build-types/hooks/use-dialog/index.d.ts +8 -2
  50. package/build-types/hooks/use-dialog/index.d.ts.map +1 -1
  51. package/build-types/hooks/use-media-query/index.d.ts.map +1 -1
  52. package/build-types/hooks/use-merge-refs/index.d.ts.map +1 -1
  53. package/build-types/index.d.ts +1 -0
  54. package/build-types/index.d.ts.map +1 -1
  55. package/build-types/lock-unlock.d.ts +2 -0
  56. package/build-types/lock-unlock.d.ts.map +1 -0
  57. package/build-types/private-apis.d.ts +5 -0
  58. package/build-types/private-apis.d.ts.map +1 -0
  59. package/build-types/utils/subscribe-delegated-listener/index.d.ts +27 -0
  60. package/build-types/utils/subscribe-delegated-listener/index.d.ts.map +1 -0
  61. package/package.json +11 -10
  62. package/src/hooks/use-copy-on-click/index.ts +2 -2
  63. package/src/hooks/use-copy-to-clipboard/index.ts +8 -8
  64. package/src/hooks/use-copy-to-clipboard/test/index.tsx +33 -5
  65. package/src/hooks/use-dialog/README.md +52 -18
  66. package/src/hooks/use-dialog/index.ts +30 -20
  67. package/src/hooks/use-dialog/test/index.tsx +162 -34
  68. package/src/hooks/use-disabled/index.ts +3 -3
  69. package/src/hooks/use-disabled/test/index.js +4 -4
  70. package/src/hooks/use-focus-outside/index.native.js +2 -2
  71. package/src/hooks/use-focus-return/index.js +2 -2
  72. package/src/hooks/use-media-query/index.ts +54 -52
  73. package/src/hooks/use-merge-refs/index.ts +2 -2
  74. package/src/hooks/use-resize-observer/use-resize-observer.ts +1 -1
  75. package/src/index.js +3 -0
  76. package/src/lock-unlock.ts +10 -0
  77. package/src/private-apis.ts +13 -0
  78. package/src/utils/subscribe-delegated-listener/index.ts +128 -0
  79. 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
+ } );