@wordpress/editor 14.48.0 → 14.48.1

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 (205) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/build/components/block-removal-warnings/index.cjs +0 -3
  3. package/build/components/block-removal-warnings/index.cjs.map +2 -2
  4. package/build/components/collab-sidebar/note-indicator-toolbar.cjs +49 -43
  5. package/build/components/collab-sidebar/note-indicator-toolbar.cjs.map +3 -3
  6. package/build/components/collaborators-overlay/use-block-highlighting.cjs +1 -8
  7. package/build/components/collaborators-overlay/use-block-highlighting.cjs.map +3 -3
  8. package/build/components/collaborators-overlay/use-render-cursors.cjs +1 -7
  9. package/build/components/collaborators-overlay/use-render-cursors.cjs.map +3 -3
  10. package/build/components/more-menu/view-more-menu-group.cjs +1 -2
  11. package/build/components/more-menu/view-more-menu-group.cjs.map +2 -2
  12. package/build/components/page-attributes/parent.cjs +1 -0
  13. package/build/components/page-attributes/parent.cjs.map +2 -2
  14. package/build/components/post-publish-button/index.cjs +114 -157
  15. package/build/components/post-publish-button/index.cjs.map +3 -3
  16. package/build/components/post-revisions-preview/block-diff.cjs +21 -9
  17. package/build/components/post-revisions-preview/block-diff.cjs.map +2 -2
  18. package/build/components/post-revisions-preview/preserve-client-ids.cjs +2 -2
  19. package/build/components/post-revisions-preview/preserve-client-ids.cjs.map +2 -2
  20. package/build/components/provider/index.cjs +2 -0
  21. package/build/components/provider/index.cjs.map +3 -3
  22. package/build/components/provider/use-network-reconnect.cjs +51 -0
  23. package/build/components/provider/use-network-reconnect.cjs.map +7 -0
  24. package/build/components/revision-fields-diff/index.cjs +2 -2
  25. package/build/components/revision-fields-diff/index.cjs.map +2 -2
  26. package/build/components/sidebar/index.cjs +1 -4
  27. package/build/components/sidebar/index.cjs.map +2 -2
  28. package/build/components/template-actions-panel/block-theme-content.cjs +7 -1
  29. package/build/components/template-actions-panel/block-theme-content.cjs.map +2 -2
  30. package/build/components/upload-progress-snackbar/index.cjs +161 -0
  31. package/build/components/upload-progress-snackbar/index.cjs.map +7 -0
  32. package/build/components/upload-progress-snackbar/tracker.cjs +90 -0
  33. package/build/components/upload-progress-snackbar/tracker.cjs.map +7 -0
  34. package/build/private-apis.cjs +2 -0
  35. package/build/private-apis.cjs.map +3 -3
  36. package/build/store/selectors.cjs +1 -2
  37. package/build/store/selectors.cjs.map +2 -2
  38. package/build/utils/media-upload/index.cjs +16 -0
  39. package/build/utils/media-upload/index.cjs.map +3 -3
  40. package/build-module/components/block-removal-warnings/index.mjs +0 -3
  41. package/build-module/components/block-removal-warnings/index.mjs.map +2 -2
  42. package/build-module/components/collab-sidebar/note-indicator-toolbar.mjs +53 -44
  43. package/build-module/components/collab-sidebar/note-indicator-toolbar.mjs.map +2 -2
  44. package/build-module/components/collaborators-overlay/use-block-highlighting.mjs +1 -8
  45. package/build-module/components/collaborators-overlay/use-block-highlighting.mjs.map +2 -2
  46. package/build-module/components/collaborators-overlay/use-render-cursors.mjs +1 -7
  47. package/build-module/components/collaborators-overlay/use-render-cursors.mjs.map +2 -2
  48. package/build-module/components/more-menu/view-more-menu-group.mjs +1 -2
  49. package/build-module/components/more-menu/view-more-menu-group.mjs.map +2 -2
  50. package/build-module/components/page-attributes/parent.mjs +1 -0
  51. package/build-module/components/page-attributes/parent.mjs.map +2 -2
  52. package/build-module/components/post-publish-button/index.mjs +116 -159
  53. package/build-module/components/post-publish-button/index.mjs.map +2 -2
  54. package/build-module/components/post-revisions-preview/block-diff.mjs +20 -8
  55. package/build-module/components/post-revisions-preview/block-diff.mjs.map +2 -2
  56. package/build-module/components/post-revisions-preview/preserve-client-ids.mjs +1 -1
  57. package/build-module/components/post-revisions-preview/preserve-client-ids.mjs.map +1 -1
  58. package/build-module/components/provider/index.mjs +2 -0
  59. package/build-module/components/provider/index.mjs.map +2 -2
  60. package/build-module/components/provider/use-network-reconnect.mjs +30 -0
  61. package/build-module/components/provider/use-network-reconnect.mjs.map +7 -0
  62. package/build-module/components/revision-fields-diff/index.mjs +2 -2
  63. package/build-module/components/revision-fields-diff/index.mjs.map +2 -2
  64. package/build-module/components/sidebar/index.mjs +2 -11
  65. package/build-module/components/sidebar/index.mjs.map +2 -2
  66. package/build-module/components/template-actions-panel/block-theme-content.mjs +7 -1
  67. package/build-module/components/template-actions-panel/block-theme-content.mjs.map +2 -2
  68. package/build-module/components/upload-progress-snackbar/index.mjs +135 -0
  69. package/build-module/components/upload-progress-snackbar/index.mjs.map +7 -0
  70. package/build-module/components/upload-progress-snackbar/tracker.mjs +61 -0
  71. package/build-module/components/upload-progress-snackbar/tracker.mjs.map +7 -0
  72. package/build-module/private-apis.mjs +2 -0
  73. package/build-module/private-apis.mjs.map +2 -2
  74. package/build-module/store/selectors.mjs +1 -2
  75. package/build-module/store/selectors.mjs.map +2 -2
  76. package/build-module/utils/media-upload/index.mjs +19 -0
  77. package/build-module/utils/media-upload/index.mjs.map +2 -2
  78. package/build-style/style-rtl.css +454 -81
  79. package/build-style/style.css +454 -81
  80. package/build-types/components/block-removal-warnings/index.d.ts.map +1 -1
  81. package/build-types/components/collab-sidebar/add-comment.d.ts +6 -0
  82. package/build-types/components/collab-sidebar/add-comment.d.ts.map +1 -0
  83. package/build-types/components/collab-sidebar/comment-author-info.d.ts +8 -0
  84. package/build-types/components/collab-sidebar/comment-author-info.d.ts.map +1 -0
  85. package/build-types/components/collab-sidebar/comment-form.d.ts +9 -0
  86. package/build-types/components/collab-sidebar/comment-form.d.ts.map +1 -0
  87. package/build-types/components/collab-sidebar/comment-indicator-toolbar.d.ts +6 -0
  88. package/build-types/components/collab-sidebar/comment-indicator-toolbar.d.ts.map +1 -0
  89. package/build-types/components/collab-sidebar/comment-menu-item.d.ts +6 -0
  90. package/build-types/components/collab-sidebar/comment-menu-item.d.ts.map +1 -0
  91. package/build-types/components/collab-sidebar/comments.d.ts +10 -0
  92. package/build-types/components/collab-sidebar/comments.d.ts.map +1 -0
  93. package/build-types/components/collab-sidebar/note-indicator-toolbar.d.ts.map +1 -1
  94. package/build-types/components/collaborators-overlay/use-block-highlighting.d.ts +0 -3
  95. package/build-types/components/collaborators-overlay/use-block-highlighting.d.ts.map +1 -1
  96. package/build-types/components/collaborators-overlay/use-render-cursors.d.ts.map +1 -1
  97. package/build-types/components/document-bar/index.d.ts +2 -2
  98. package/build-types/components/document-bar/index.d.ts.map +1 -1
  99. package/build-types/components/global-styles-provider/index.d.ts +16 -0
  100. package/build-types/components/global-styles-provider/index.d.ts.map +1 -0
  101. package/build-types/components/media/index.d.ts +3 -0
  102. package/build-types/components/media/index.d.ts.map +1 -0
  103. package/build-types/components/media/metadata-panel.d.ts +12 -0
  104. package/build-types/components/media/metadata-panel.d.ts.map +1 -0
  105. package/build-types/components/media/preview.d.ts +9 -0
  106. package/build-types/components/media/preview.d.ts.map +1 -0
  107. package/build-types/components/more-menu/view-more-menu-group.d.ts.map +1 -1
  108. package/build-types/components/page-attributes/parent.d.ts.map +1 -1
  109. package/build-types/components/post-publish-button/index.d.ts +9 -9
  110. package/build-types/components/post-publish-button/index.d.ts.map +1 -1
  111. package/build-types/components/post-revisions-preview/block-diff.d.ts +3 -0
  112. package/build-types/components/post-revisions-preview/block-diff.d.ts.map +1 -1
  113. package/build-types/components/post-text-editor/index.d.ts +1 -1
  114. package/build-types/components/post-text-editor/index.d.ts.map +1 -1
  115. package/build-types/components/post-text-editor/utils.d.ts +29 -0
  116. package/build-types/components/post-text-editor/utils.d.ts.map +1 -0
  117. package/build-types/components/provider/index.d.ts.map +1 -1
  118. package/build-types/components/provider/use-network-reconnect.d.ts +8 -0
  119. package/build-types/components/provider/use-network-reconnect.d.ts.map +1 -0
  120. package/build-types/components/revision-fields-diff/index.d.ts +3 -0
  121. package/build-types/components/revision-fields-diff/index.d.ts.map +1 -1
  122. package/build-types/components/sidebar/index.d.ts.map +1 -1
  123. package/build-types/components/template-actions-panel/block-theme-content.d.ts.map +1 -1
  124. package/build-types/components/upload-progress-snackbar/index.d.ts +19 -0
  125. package/build-types/components/upload-progress-snackbar/index.d.ts.map +1 -0
  126. package/build-types/components/upload-progress-snackbar/stories/index.story.d.ts +28 -0
  127. package/build-types/components/upload-progress-snackbar/stories/index.story.d.ts.map +1 -0
  128. package/build-types/components/upload-progress-snackbar/tracker.d.ts +41 -0
  129. package/build-types/components/upload-progress-snackbar/tracker.d.ts.map +1 -0
  130. package/build-types/private-apis.d.ts.map +1 -1
  131. package/build-types/store/selectors.d.ts.map +1 -1
  132. package/build-types/utils/get-template-part-icon.d.ts.map +1 -1
  133. package/build-types/utils/media-upload/index.d.ts.map +1 -1
  134. package/package.json +53 -50
  135. package/src/components/README.md +1 -1
  136. package/src/components/block-removal-warnings/index.js +0 -7
  137. package/src/components/collab-sidebar/note-indicator-toolbar.js +73 -60
  138. package/src/components/collaborators-overlay/use-block-highlighting.ts +0 -9
  139. package/src/components/collaborators-overlay/use-render-cursors.ts +0 -8
  140. package/src/components/collaborators-presence/avatar/test/index.tsx +8 -3
  141. package/src/components/more-menu/view-more-menu-group.js +1 -2
  142. package/src/components/page-attributes/parent.js +1 -0
  143. package/src/components/post-publish-button/index.js +143 -192
  144. package/src/components/post-publish-button/test/index.js +137 -114
  145. package/src/components/post-revisions-preview/block-diff.js +63 -19
  146. package/src/components/post-revisions-preview/preserve-client-ids.js +1 -1
  147. package/src/components/post-revisions-preview/test/block-diff.js +109 -6
  148. package/src/components/provider/index.js +4 -0
  149. package/src/components/provider/test/use-network-reconnect.js +137 -0
  150. package/src/components/provider/use-network-reconnect.js +44 -0
  151. package/src/components/revision-fields-diff/index.js +7 -2
  152. package/src/components/sidebar/index.js +2 -11
  153. package/src/components/template-actions-panel/block-theme-content.js +10 -1
  154. package/src/components/upload-progress-snackbar/README.md +26 -0
  155. package/src/components/upload-progress-snackbar/index.js +216 -0
  156. package/src/components/upload-progress-snackbar/stories/index.story.tsx +85 -0
  157. package/src/components/upload-progress-snackbar/style.scss +30 -0
  158. package/src/components/upload-progress-snackbar/test/index.js +199 -0
  159. package/src/components/upload-progress-snackbar/tracker.js +105 -0
  160. package/src/private-apis.js +2 -0
  161. package/src/store/selectors.js +1 -3
  162. package/src/style.scss +1 -0
  163. package/src/utils/media-upload/index.js +27 -0
  164. package/src/components/commands/index.native.js +0 -2
  165. package/src/components/deprecated.native.js +0 -47
  166. package/src/components/editor-help/add-blocks.native.js +0 -40
  167. package/src/components/editor-help/customize-blocks.native.js +0 -40
  168. package/src/components/editor-help/help-detail-navigation-screen.native.js +0 -67
  169. package/src/components/editor-help/help-get-support-button.native.js +0 -38
  170. package/src/components/editor-help/help-section-title.native.js +0 -29
  171. package/src/components/editor-help/help-topic-row.native.js +0 -33
  172. package/src/components/editor-help/icon-move-blocks.native.js +0 -10
  173. package/src/components/editor-help/index.native.js +0 -208
  174. package/src/components/editor-help/intro-to-blocks.native.js +0 -91
  175. package/src/components/editor-help/move-blocks.native.js +0 -55
  176. package/src/components/editor-help/remove-blocks.native.js +0 -35
  177. package/src/components/editor-help/style.android.scss +0 -6
  178. package/src/components/editor-help/style.ios.scss +0 -6
  179. package/src/components/editor-help/test/index.native.js +0 -81
  180. package/src/components/editor-help/view-sections.native.js +0 -79
  181. package/src/components/error-boundary/index.native.js +0 -192
  182. package/src/components/error-boundary/style.native.scss +0 -116
  183. package/src/components/index.native.js +0 -15
  184. package/src/components/offline-status/index.native.js +0 -99
  185. package/src/components/offline-status/style.native.scss +0 -28
  186. package/src/components/offline-status/test/index.native.js +0 -108
  187. package/src/components/post-title/index.native.js +0 -282
  188. package/src/components/post-title/style.native.scss +0 -13
  189. package/src/components/post-title/test/__snapshots__/index.native.js.snap +0 -25
  190. package/src/components/post-title/test/index.native.js +0 -78
  191. package/src/components/provider/index.native.js +0 -497
  192. package/src/components/provider/use-block-editor-settings.native.js +0 -48
  193. package/src/components/template-part-menu-items/index.native.js +0 -3
  194. package/src/hooks/index.native.js +0 -0
  195. package/src/index.native.js +0 -16
  196. package/src/private-apis.native.js +0 -33
  197. package/src/store/actions.native.js +0 -27
  198. package/src/store/reducer.native.js +0 -94
  199. package/src/store/selectors.native.js +0 -57
  200. package/src/store/test/actions.native.js +0 -16
  201. package/src/store/test/reducer.native.js +0 -36
  202. package/src/store/test/selectors.native.js +0 -28
  203. package/src/utils/index.native.js +0 -6
  204. package/src/utils/media-sideload/index.native.js +0 -1
  205. package/src/utils/media-upload/index.native.js +0 -1
@@ -0,0 +1,137 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { renderHook } from '@testing-library/react';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useRegistry } from '@wordpress/data';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import useNetworkReconnect from '../use-network-reconnect';
15
+
16
+ const mockPauseQueue = jest.fn();
17
+ const mockResumeQueue = jest.fn();
18
+
19
+ jest.mock( '@wordpress/data', () => ( {
20
+ useRegistry: jest.fn(),
21
+ } ) );
22
+
23
+ jest.mock( '@wordpress/upload-media', () => ( {
24
+ store: 'core/upload-media',
25
+ } ) );
26
+
27
+ jest.mock( '../../../lock-unlock', () => ( {
28
+ unlock: jest.fn( () => ( {
29
+ pauseQueue: mockPauseQueue,
30
+ resumeQueue: mockResumeQueue,
31
+ } ) ),
32
+ } ) );
33
+
34
+ describe( 'useNetworkReconnect', () => {
35
+ const originalAddEventListener = window.addEventListener;
36
+ const originalRemoveEventListener = window.removeEventListener;
37
+ let listeners;
38
+
39
+ beforeEach( () => {
40
+ mockPauseQueue.mockClear();
41
+ mockResumeQueue.mockClear();
42
+ listeners = {};
43
+ window.addEventListener = jest.fn( ( event, cb ) => {
44
+ listeners[ event ] = cb;
45
+ } );
46
+ window.removeEventListener = jest.fn( ( event ) => {
47
+ delete listeners[ event ];
48
+ } );
49
+ useRegistry.mockReturnValue( {
50
+ dispatch: jest.fn( () => ( {} ) ),
51
+ } );
52
+ } );
53
+
54
+ afterEach( () => {
55
+ window.addEventListener = originalAddEventListener;
56
+ window.removeEventListener = originalRemoveEventListener;
57
+ delete window.__clientSideMediaProcessing;
58
+ } );
59
+
60
+ it( 'does nothing when client-side media processing is disabled', () => {
61
+ window.__clientSideMediaProcessing = false;
62
+ renderHook( () => useNetworkReconnect() );
63
+
64
+ expect( window.addEventListener ).not.toHaveBeenCalled();
65
+ } );
66
+
67
+ it( 'does nothing when the flag is undefined', () => {
68
+ renderHook( () => useNetworkReconnect() );
69
+
70
+ expect( window.addEventListener ).not.toHaveBeenCalled();
71
+ } );
72
+
73
+ it( 'registers offline and online listeners when enabled', () => {
74
+ window.__clientSideMediaProcessing = true;
75
+ renderHook( () => useNetworkReconnect() );
76
+
77
+ expect( window.addEventListener ).toHaveBeenCalledWith(
78
+ 'offline',
79
+ expect.any( Function )
80
+ );
81
+ expect( window.addEventListener ).toHaveBeenCalledWith(
82
+ 'online',
83
+ expect.any( Function )
84
+ );
85
+ } );
86
+
87
+ it( 'pauses the queue when the offline event fires', () => {
88
+ window.__clientSideMediaProcessing = true;
89
+ renderHook( () => useNetworkReconnect() );
90
+
91
+ listeners.offline();
92
+
93
+ expect( mockPauseQueue ).toHaveBeenCalledTimes( 1 );
94
+ expect( mockResumeQueue ).not.toHaveBeenCalled();
95
+ } );
96
+
97
+ it( 'resumes the queue when the online event fires', () => {
98
+ window.__clientSideMediaProcessing = true;
99
+ renderHook( () => useNetworkReconnect() );
100
+
101
+ listeners.online();
102
+
103
+ expect( mockResumeQueue ).toHaveBeenCalledTimes( 1 );
104
+ expect( mockPauseQueue ).not.toHaveBeenCalled();
105
+ } );
106
+
107
+ it( 'handles multiple offline/online cycles', () => {
108
+ window.__clientSideMediaProcessing = true;
109
+ renderHook( () => useNetworkReconnect() );
110
+
111
+ listeners.offline();
112
+ listeners.online();
113
+ listeners.offline();
114
+ listeners.online();
115
+
116
+ expect( mockPauseQueue ).toHaveBeenCalledTimes( 2 );
117
+ expect( mockResumeQueue ).toHaveBeenCalledTimes( 2 );
118
+ } );
119
+
120
+ it( 'removes the listeners on unmount', () => {
121
+ window.__clientSideMediaProcessing = true;
122
+ const { unmount } = renderHook( () => useNetworkReconnect() );
123
+
124
+ unmount();
125
+
126
+ expect( window.removeEventListener ).toHaveBeenCalledWith(
127
+ 'offline',
128
+ expect.any( Function )
129
+ );
130
+ expect( window.removeEventListener ).toHaveBeenCalledWith(
131
+ 'online',
132
+ expect.any( Function )
133
+ );
134
+ expect( listeners.offline ).toBeUndefined();
135
+ expect( listeners.online ).toBeUndefined();
136
+ } );
137
+ } );
@@ -0,0 +1,44 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useEffect } from '@wordpress/element';
5
+ import { useRegistry } from '@wordpress/data';
6
+ import { store as uploadStore } from '@wordpress/upload-media';
7
+
8
+ /**
9
+ * Internal dependencies
10
+ */
11
+ import { unlock } from '../../lock-unlock';
12
+
13
+ /**
14
+ * A hook that pauses the media upload queue when the browser goes offline
15
+ * and resumes it when connectivity is restored.
16
+ *
17
+ * Only active when client-side media processing is enabled.
18
+ */
19
+ export default function useNetworkReconnect() {
20
+ const isEnabled = window.__clientSideMediaProcessing;
21
+ const registry = useRegistry();
22
+
23
+ useEffect( () => {
24
+ if ( ! isEnabled ) {
25
+ return;
26
+ }
27
+
28
+ const handleOffline = () => {
29
+ unlock( registry.dispatch( uploadStore ) ).pauseQueue();
30
+ };
31
+
32
+ const handleOnline = () => {
33
+ unlock( registry.dispatch( uploadStore ) ).resumeQueue();
34
+ };
35
+
36
+ window.addEventListener( 'offline', handleOffline );
37
+ window.addEventListener( 'online', handleOnline );
38
+
39
+ return () => {
40
+ window.removeEventListener( 'offline', handleOffline );
41
+ window.removeEventListener( 'online', handleOnline );
42
+ };
43
+ }, [ isEnabled, registry ] );
44
+ }
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { diffWords } from 'diff/lib/diff/word';
4
+ /*
5
+ * `diffWordsWithSpace` preserves the v4-style per-word output. v6+
6
+ * stopped treating whitespace as a token in `diffWords`, which coalesces
7
+ * adjacent word changes into a single removed/added pair.
8
+ */
9
+ import { diffWordsWithSpace } from 'diff';
5
10
 
6
11
  /**
7
12
  * WordPress dependencies
@@ -71,7 +76,7 @@ export default function RevisionFieldsDiffPanel() {
71
76
  continue;
72
77
  }
73
78
 
74
- result[ key ] = diffWords( prevStr, revStr );
79
+ result[ key ] = diffWordsWithSpace( prevStr, revStr );
75
80
  }
76
81
 
77
82
  if ( Object.keys( result ).length === 0 ) {
@@ -6,13 +6,7 @@ import {
6
6
  store as blockEditorStore,
7
7
  } from '@wordpress/block-editor';
8
8
  import { useSelect, useDispatch } from '@wordpress/data';
9
- import {
10
- Platform,
11
- useCallback,
12
- useContext,
13
- useEffect,
14
- useRef,
15
- } from '@wordpress/element';
9
+ import { useCallback, useContext, useEffect, useRef } from '@wordpress/element';
16
10
  import { isRTL, __, _x } from '@wordpress/i18n';
17
11
  import { drawerLeft, drawerRight } from '@wordpress/icons';
18
12
  import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
@@ -47,10 +41,7 @@ import {
47
41
 
48
42
  const { Tabs } = unlock( componentsPrivateApis );
49
43
 
50
- const SIDEBAR_ACTIVE_BY_DEFAULT = Platform.select( {
51
- web: true,
52
- native: false,
53
- } );
44
+ const SIDEBAR_ACTIVE_BY_DEFAULT = true;
54
45
 
55
46
  const SidebarContent = ( {
56
47
  tabName,
@@ -17,6 +17,7 @@ import {
17
17
  import { useState } from '@wordpress/element';
18
18
  import { __, sprintf } from '@wordpress/i18n';
19
19
  import { decodeEntities } from '@wordpress/html-entities';
20
+ import { ENTER, SPACE } from '@wordpress/keycodes';
20
21
  import { store as noticesStore } from '@wordpress/notices';
21
22
  import { store as preferencesStore } from '@wordpress/preferences';
22
23
  // eslint-disable-next-line @wordpress/use-recommended-components -- `Tooltip` is not yet on the recommended `@wordpress/ui` allow-list; landing as a migration step ahead of the wider rollout.
@@ -127,7 +128,15 @@ export default function TemplateActionsPanelContent() {
127
128
  tabIndex={ 0 }
128
129
  aria-label={ tooltipText }
129
130
  onClick={ () => setIsSwapModalOpen( true ) }
130
- onKeyPress={ () => setIsSwapModalOpen( true ) }
131
+ onKeyDown={ ( event ) => {
132
+ if (
133
+ event.keyCode === ENTER ||
134
+ event.keyCode === SPACE
135
+ ) {
136
+ event.preventDefault();
137
+ setIsSwapModalOpen( true );
138
+ }
139
+ } }
131
140
  >
132
141
  { previewContent }
133
142
  </div>
@@ -0,0 +1,26 @@
1
+ # UploadProgressSnackbar
2
+
3
+ `UploadProgressSnackbar` manages a snackbar notice that shows media upload progress. It creates and updates a notice via the notices store, so the snackbar positions and stacks with every other snackbar in the editor.
4
+
5
+ The component reads from two sources so it works for both upload paths:
6
+
7
+ - **`@wordpress/upload-media`** — the client-side media processing (CSM) path. Only counts original user-uploaded files, ignoring generated subsizes and thumbnails (items with a `parentId`).
8
+ - **Editor-local tracker** — populated by the editor's `mediaUpload` wrapper for the traditional (non-CSM) upload path (e.g. Safari, or when a filter disables CSM).
9
+
10
+ While uploads are in progress the snackbar shows a spinner to the left of the text. When the last upload finishes, the spinner is replaced with a green checkmark for a brief moment before the snackbar dismisses.
11
+
12
+ The component renders nothing itself — it is a controller that manages a notice.
13
+
14
+ ## Usage
15
+
16
+ ```jsx
17
+ import { UploadProgressSnackbar } from '@wordpress/editor';
18
+
19
+ // Mount anywhere in the editor — it doesn't render DOM, just manages a notice.
20
+ <UploadProgressSnackbar />
21
+ ```
22
+
23
+ ## Accessibility
24
+
25
+ - The component calls `wp.a11y.speak()` once when uploads start and once when they complete, avoiding per-tick chatter.
26
+ - The snackbar is created with `speak: false` to prevent the notices store from re-announcing on every text update.
@@ -0,0 +1,216 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useSelect, useDispatch } from '@wordpress/data';
5
+ import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
6
+ import { __, sprintf } from '@wordpress/i18n';
7
+ import { speak } from '@wordpress/a11y';
8
+ import { store as uploadStore } from '@wordpress/upload-media';
9
+ import { store as noticesStore } from '@wordpress/notices';
10
+ import { Icon as WCIcon, Spinner } from '@wordpress/components';
11
+ import { check } from '@wordpress/icons';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import { useTracker } from './tracker';
17
+
18
+ const NOTICE_ID = 'upload-progress';
19
+
20
+ // How long the completion checkmark is shown before the snackbar dismisses.
21
+ const COMPLETION_DISPLAY_MS = 3000;
22
+
23
+ // Longest filename shown before it is middle-truncated. Long names (e.g. the
24
+ // UUID-style names some sources produce) would otherwise stretch the snackbar.
25
+ const MAX_FILENAME_LENGTH = 40;
26
+
27
+ /**
28
+ * Middle-truncates a filename that exceeds `MAX_FILENAME_LENGTH`, keeping the
29
+ * start and the end (so the file extension stays visible).
30
+ *
31
+ * @param {string} filename The filename to truncate.
32
+ * @return {string} The original or middle-truncated filename.
33
+ */
34
+ function truncateFilename( filename ) {
35
+ if ( filename.length <= MAX_FILENAME_LENGTH ) {
36
+ return filename;
37
+ }
38
+ const ellipsis = '…';
39
+ const visible = MAX_FILENAME_LENGTH - ellipsis.length;
40
+ const front = Math.ceil( visible / 2 );
41
+ const back = Math.floor( visible / 2 );
42
+ return (
43
+ filename.slice( 0, front ) +
44
+ ellipsis +
45
+ filename.slice( filename.length - back )
46
+ );
47
+ }
48
+
49
+ // Exported so the Storybook story can render the exact icon markup the notice
50
+ // uses, keeping the visual review faithful to what ships.
51
+ export const UPLOAD_SPINNER = (
52
+ <span
53
+ className="editor-upload-progress-snackbar__spinner"
54
+ aria-hidden="true"
55
+ >
56
+ <Spinner />
57
+ </span>
58
+ );
59
+
60
+ export const UPLOAD_DONE = (
61
+ <span className="editor-upload-progress-snackbar__check" aria-hidden="true">
62
+ <WCIcon icon={ check } />
63
+ </span>
64
+ );
65
+
66
+ /**
67
+ * Manages a snackbar notice that shows media upload progress while uploads are
68
+ * in progress. It creates/updates a notice via the notices store so that it
69
+ * positions and stacks with every other snackbar in the editor.
70
+ *
71
+ * Reads from two sources to cover both upload paths:
72
+ * - `@wordpress/upload-media` store (client-side media processing path).
73
+ * - An editor-local tracker populated by the traditional `mediaUpload`
74
+ * wrapper (non-CSM path — e.g. Safari, or when a filter disables CSM).
75
+ *
76
+ * Only counts original user-uploaded files (items without a `parentId`),
77
+ * ignoring generated subsizes/thumbnails.
78
+ *
79
+ * @return {null} This component renders nothing — it only manages a notice.
80
+ */
81
+ export default function UploadProgressSnackbar() {
82
+ const items = useSelect(
83
+ ( select ) => select( uploadStore ).getItems(),
84
+ []
85
+ );
86
+ const tracker = useTracker();
87
+
88
+ // CSM path: originals in the upload-media queue (subsizes excluded). Memoized
89
+ // so its reference is stable across renders where `items` is unchanged, since
90
+ // it's a dependency of the effect below.
91
+ const csmOriginals = useMemo(
92
+ () => items.filter( ( item ) => ! item.parentId ),
93
+ [ items ]
94
+ );
95
+ const csmRemaining = csmOriginals.length;
96
+
97
+ // Non-CSM path: files tracked by the editor's mediaUpload wrapper.
98
+ const trackedRemaining = tracker ? tracker.total - tracker.completed : 0;
99
+
100
+ const remaining = csmRemaining + trackedRemaining;
101
+
102
+ // Track peak total across sources during a session. The CSM queue removes
103
+ // items on completion, and the tracker tops out at its recorded total, so
104
+ // `total` has to be tracked as the high-water mark.
105
+ const [ peak, setPeak ] = useState( 0 );
106
+ const sessionTotal = csmRemaining + ( tracker ? tracker.total : 0 );
107
+ if ( sessionTotal > peak ) {
108
+ setPeak( sessionTotal );
109
+ }
110
+
111
+ const { createNotice, removeNotice } = useDispatch( noticesStore );
112
+
113
+ // Track whether the user has dismissed the notice. If so, don't re-create
114
+ // it until the current batch finishes and a new one starts.
115
+ const dismissedRef = useRef( false );
116
+ const wasUploadingRef = useRef( false );
117
+
118
+ // Timeout that removes the completion-state (checkmark) notice after a
119
+ // brief display. Held so a new upload can cancel it.
120
+ const completionTimeoutRef = useRef( null );
121
+ useEffect( () => {
122
+ return () => {
123
+ if ( completionTimeoutRef.current ) {
124
+ clearTimeout( completionTimeoutRef.current );
125
+ }
126
+ };
127
+ }, [] );
128
+
129
+ useEffect( () => {
130
+ const isUploading = remaining > 0;
131
+
132
+ if ( isUploading && ! wasUploadingRef.current ) {
133
+ dismissedRef.current = false;
134
+ speak( __( 'Media upload started' ), 'polite' );
135
+ // A new batch started during the completion display: cancel the
136
+ // pending dismissal so the snackbar transitions straight back
137
+ // into the uploading state, and reset the peak so the new batch
138
+ // counts from `1 of N` rather than resuming the previous total.
139
+ if ( completionTimeoutRef.current ) {
140
+ clearTimeout( completionTimeoutRef.current );
141
+ completionTimeoutRef.current = null;
142
+ setPeak( 0 );
143
+ }
144
+ } else if ( ! isUploading && wasUploadingRef.current ) {
145
+ speak( __( 'Media upload complete' ), 'polite' );
146
+
147
+ if ( ! dismissedRef.current ) {
148
+ createNotice( 'info', __( 'Upload complete' ), {
149
+ id: NOTICE_ID,
150
+ type: 'snackbar',
151
+ isDismissible: false,
152
+ explicitDismiss: false,
153
+ speak: false,
154
+ icon: UPLOAD_DONE,
155
+ onDismiss: () => {
156
+ dismissedRef.current = true;
157
+ },
158
+ } );
159
+
160
+ completionTimeoutRef.current = setTimeout( () => {
161
+ removeNotice( NOTICE_ID );
162
+ completionTimeoutRef.current = null;
163
+ setPeak( 0 );
164
+ }, COMPLETION_DISPLAY_MS );
165
+ } else {
166
+ setPeak( 0 );
167
+ }
168
+ }
169
+
170
+ wasUploadingRef.current = isUploading;
171
+
172
+ if ( ! isUploading || dismissedRef.current ) {
173
+ return;
174
+ }
175
+
176
+ const total = peak;
177
+ const current = total - remaining + 1;
178
+
179
+ // Prefer the CSM queue's first original filename, then fall back to
180
+ // the tracker's first pending filename.
181
+ const filename = truncateFilename(
182
+ csmOriginals[ 0 ]?.sourceFile?.name ||
183
+ tracker?.pending[ 0 ] ||
184
+ __( 'Uploading' )
185
+ );
186
+
187
+ const content =
188
+ total === 1
189
+ ? sprintf(
190
+ /* translators: %s: filename. */
191
+ __( 'Uploading — %s' ),
192
+ filename
193
+ )
194
+ : sprintf(
195
+ /* translators: 1: current upload number, 2: total uploads, 3: filename. */
196
+ __( 'Uploading %1$d of %2$d — %3$s' ),
197
+ current,
198
+ total,
199
+ filename
200
+ );
201
+
202
+ createNotice( 'info', content, {
203
+ id: NOTICE_ID,
204
+ type: 'snackbar',
205
+ isDismissible: false,
206
+ explicitDismiss: true,
207
+ speak: false,
208
+ icon: UPLOAD_SPINNER,
209
+ onDismiss: () => {
210
+ dismissedRef.current = true;
211
+ },
212
+ } );
213
+ }, [ remaining, peak, csmOriginals, tracker, createNotice, removeNotice ] );
214
+
215
+ return null;
216
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import type { Meta, StoryObj } from '@storybook/react-vite';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { Snackbar } from '@wordpress/components';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import { UPLOAD_DONE, UPLOAD_SPINNER } from '../index';
15
+
16
+ /**
17
+ * The `UploadProgressSnackbar` component itself renders no UI: it manages a
18
+ * snackbar notice via the notices store while media uploads are in progress.
19
+ *
20
+ * These stories render the `Snackbar` with the same icon markup and text the
21
+ * notice produces, so the in-progress and completed states can be reviewed
22
+ * visually without running an actual upload.
23
+ */
24
+ const meta: Meta< typeof Snackbar > = {
25
+ title: 'Editor/UploadProgressSnackbar',
26
+ component: Snackbar,
27
+ parameters: {
28
+ docs: { canvas: { sourceState: 'shown' } },
29
+ },
30
+ argTypes: {
31
+ icon: { control: false },
32
+ children: { control: false },
33
+ },
34
+ };
35
+ export default meta;
36
+
37
+ type Story = StoryObj< typeof Snackbar >;
38
+
39
+ export const Uploading: Story = {
40
+ args: {
41
+ icon: UPLOAD_SPINNER,
42
+ children: 'Uploading — sunset-over-the-bay.jpg',
43
+ explicitDismiss: true,
44
+ },
45
+ };
46
+
47
+ export const UploadingMultiple: Story = {
48
+ args: {
49
+ icon: UPLOAD_SPINNER,
50
+ children: 'Uploading 1 of 3 — sunset-over-the-bay.jpg',
51
+ explicitDismiss: true,
52
+ },
53
+ };
54
+
55
+ export const Complete: Story = {
56
+ args: {
57
+ icon: UPLOAD_DONE,
58
+ children: 'Upload complete',
59
+ },
60
+ };
61
+
62
+ /**
63
+ * All states shown together for quick comparison of the icon alignment and
64
+ * sizing between the spinner and the completion checkmark.
65
+ */
66
+ export const AllStates: Story = {
67
+ render: () => (
68
+ <div
69
+ style={ {
70
+ display: 'flex',
71
+ flexDirection: 'column',
72
+ alignItems: 'flex-start',
73
+ gap: '12px',
74
+ } }
75
+ >
76
+ <Snackbar icon={ UPLOAD_SPINNER } explicitDismiss>
77
+ Uploading — sunset-over-the-bay.jpg
78
+ </Snackbar>
79
+ <Snackbar icon={ UPLOAD_SPINNER } explicitDismiss>
80
+ Uploading 1 of 3 — sunset-over-the-bay.jpg
81
+ </Snackbar>
82
+ <Snackbar icon={ UPLOAD_DONE }>Upload complete</Snackbar>
83
+ </div>
84
+ ),
85
+ };
@@ -0,0 +1,30 @@
1
+ @use "@wordpress/base-styles/variables" as *;
2
+
3
+ .editor-upload-progress-snackbar__spinner,
4
+ .editor-upload-progress-snackbar__check {
5
+ display: inline-flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ // Size the slot to $icon-size to match the icon dimensions the Snackbar's
9
+ // absolute positioning is tuned for, so the spinner and checkmark sit
10
+ // vertically centered against the notice text.
11
+ width: $icon-size;
12
+ height: $icon-size;
13
+ }
14
+
15
+ .editor-upload-progress-snackbar__spinner .components-spinner {
16
+ // The Spinner ships with an asymmetric default margin (top, not bottom),
17
+ // which offsets it within the centered icon slot. Reset it so the flex
18
+ // centering above keeps the spinner vertically centered against the text.
19
+ margin: 0;
20
+ }
21
+
22
+ .editor-upload-progress-snackbar__check svg {
23
+ // Render at the full icon size: the check glyph only occupies the center of
24
+ // its viewBox, so a smaller size makes the mark look faint next to the text.
25
+ width: $icon-size;
26
+ height: $icon-size;
27
+ // The icon ships without a fill, which renders black and disappears against
28
+ // the dark snackbar. Inherit the snackbar's white text color instead.
29
+ fill: currentColor;
30
+ }