@wordpress/editor 14.44.0 → 14.44.1-next.v.202604201441.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 (103) hide show
  1. package/build/components/collab-sidebar/add-comment.cjs +6 -22
  2. package/build/components/collab-sidebar/add-comment.cjs.map +3 -3
  3. package/build/components/collab-sidebar/board-store.cjs +103 -0
  4. package/build/components/collab-sidebar/board-store.cjs.map +7 -0
  5. package/build/components/collab-sidebar/comment-form.cjs +2 -10
  6. package/build/components/collab-sidebar/comment-form.cjs.map +2 -2
  7. package/build/components/collab-sidebar/comments.cjs +29 -149
  8. package/build/components/collab-sidebar/comments.cjs.map +3 -3
  9. package/build/components/collab-sidebar/floating-container.cjs +62 -0
  10. package/build/components/collab-sidebar/floating-container.cjs.map +7 -0
  11. package/build/components/collab-sidebar/hooks.cjs +49 -41
  12. package/build/components/collab-sidebar/hooks.cjs.map +2 -2
  13. package/build/components/collab-sidebar/index.cjs +2 -13
  14. package/build/components/collab-sidebar/index.cjs.map +2 -2
  15. package/build/components/collab-sidebar/utils.cjs +72 -4
  16. package/build/components/collab-sidebar/utils.cjs.map +2 -2
  17. package/build/components/collaborators-presence/avatar/component.cjs.map +1 -1
  18. package/build/components/collaborators-presence/index.cjs +3 -3
  19. package/build/components/collaborators-presence/index.cjs.map +2 -2
  20. package/build/components/collaborators-presence/list.cjs +3 -3
  21. package/build/components/collaborators-presence/list.cjs.map +2 -2
  22. package/build/components/media-categories/index.cjs +1 -1
  23. package/build/components/media-categories/index.cjs.map +1 -1
  24. package/build/components/more-menu/index.cjs +1 -1
  25. package/build/components/more-menu/index.cjs.map +1 -1
  26. package/build/components/post-publish-panel/maybe-upload-media.cjs +1 -1
  27. package/build/components/post-publish-panel/maybe-upload-media.cjs.map +1 -1
  28. package/build/components/style-book/constants.cjs +1 -1
  29. package/build/components/style-book/constants.cjs.map +1 -1
  30. package/build/components/style-book/index.cjs +1 -1
  31. package/build/components/style-book/index.cjs.map +1 -1
  32. package/build-module/components/collab-sidebar/add-comment.mjs +8 -27
  33. package/build-module/components/collab-sidebar/add-comment.mjs.map +2 -2
  34. package/build-module/components/collab-sidebar/board-store.mjs +78 -0
  35. package/build-module/components/collab-sidebar/board-store.mjs.map +7 -0
  36. package/build-module/components/collab-sidebar/comment-form.mjs +4 -12
  37. package/build-module/components/collab-sidebar/comment-form.mjs.map +2 -2
  38. package/build-module/components/collab-sidebar/comments.mjs +30 -151
  39. package/build-module/components/collab-sidebar/comments.mjs.map +2 -2
  40. package/build-module/components/collab-sidebar/floating-container.mjs +27 -0
  41. package/build-module/components/collab-sidebar/floating-container.mjs.map +7 -0
  42. package/build-module/components/collab-sidebar/hooks.mjs +51 -44
  43. package/build-module/components/collab-sidebar/hooks.mjs.map +2 -2
  44. package/build-module/components/collab-sidebar/index.mjs +2 -13
  45. package/build-module/components/collab-sidebar/index.mjs.map +2 -2
  46. package/build-module/components/collab-sidebar/utils.mjs +71 -3
  47. package/build-module/components/collab-sidebar/utils.mjs.map +2 -2
  48. package/build-module/components/collaborators-presence/avatar/component.mjs.map +1 -1
  49. package/build-module/components/collaborators-presence/index.mjs +3 -3
  50. package/build-module/components/collaborators-presence/index.mjs.map +2 -2
  51. package/build-module/components/collaborators-presence/list.mjs +3 -3
  52. package/build-module/components/collaborators-presence/list.mjs.map +2 -2
  53. package/build-module/components/media-categories/index.mjs +1 -1
  54. package/build-module/components/media-categories/index.mjs.map +1 -1
  55. package/build-module/components/more-menu/index.mjs +1 -1
  56. package/build-module/components/more-menu/index.mjs.map +1 -1
  57. package/build-module/components/post-publish-panel/maybe-upload-media.mjs +1 -1
  58. package/build-module/components/post-publish-panel/maybe-upload-media.mjs.map +1 -1
  59. package/build-module/components/style-book/constants.mjs +1 -1
  60. package/build-module/components/style-book/constants.mjs.map +1 -1
  61. package/build-module/components/style-book/index.mjs +1 -1
  62. package/build-module/components/style-book/index.mjs.map +1 -1
  63. package/build-style/style-rtl.css +12 -30
  64. package/build-style/style.css +12 -30
  65. package/build-types/components/collab-sidebar/add-comment.d.ts +2 -6
  66. package/build-types/components/collab-sidebar/add-comment.d.ts.map +1 -1
  67. package/build-types/components/collab-sidebar/board-store.d.ts +8 -0
  68. package/build-types/components/collab-sidebar/board-store.d.ts.map +1 -0
  69. package/build-types/components/collab-sidebar/comment-form.d.ts +1 -3
  70. package/build-types/components/collab-sidebar/comment-form.d.ts.map +1 -1
  71. package/build-types/components/collab-sidebar/comments.d.ts +1 -3
  72. package/build-types/components/collab-sidebar/comments.d.ts.map +1 -1
  73. package/build-types/components/collab-sidebar/floating-container.d.ts +8 -0
  74. package/build-types/components/collab-sidebar/floating-container.d.ts.map +1 -0
  75. package/build-types/components/collab-sidebar/hooks.d.ts +13 -9
  76. package/build-types/components/collab-sidebar/hooks.d.ts.map +1 -1
  77. package/build-types/components/collab-sidebar/index.d.ts.map +1 -1
  78. package/build-types/components/collab-sidebar/utils.d.ts +27 -4
  79. package/build-types/components/collab-sidebar/utils.d.ts.map +1 -1
  80. package/build-types/components/style-book/constants.d.ts +1 -1
  81. package/build-types/components/style-book/constants.d.ts.map +1 -1
  82. package/package.json +45 -45
  83. package/src/components/collab-sidebar/add-comment.js +9 -31
  84. package/src/components/collab-sidebar/board-store.js +83 -0
  85. package/src/components/collab-sidebar/comment-form.js +5 -14
  86. package/src/components/collab-sidebar/comments.js +29 -202
  87. package/src/components/collab-sidebar/floating-container.js +29 -0
  88. package/src/components/collab-sidebar/hooks.js +60 -48
  89. package/src/components/collab-sidebar/index.js +3 -14
  90. package/src/components/collab-sidebar/test/utils.js +153 -0
  91. package/src/components/collab-sidebar/utils.js +112 -4
  92. package/src/components/collaborators-presence/avatar/component.tsx +1 -1
  93. package/src/components/collaborators-presence/styles/collaborators-list.scss +1 -1
  94. package/src/components/collaborators-presence/styles/collaborators-presence.scss +1 -1
  95. package/src/components/document-outline/style.scss +1 -1
  96. package/src/components/media-categories/index.js +1 -1
  97. package/src/components/more-menu/index.js +1 -1
  98. package/src/components/post-publish-panel/maybe-upload-media.js +1 -1
  99. package/src/components/post-publish-panel/test/__snapshots__/index.js.snap +2 -2
  100. package/src/components/post-revisions-preview/style.scss +1 -1
  101. package/src/components/style-book/constants.ts +1 -1
  102. package/src/components/style-book/index.js +1 -1
  103. package/src/components/template-actions-panel/style.scss +1 -1
@@ -0,0 +1,83 @@
1
+ export function createBoardStore() {
2
+ const listeners = new Set();
3
+ const blockRefs = new Map();
4
+ const floatingRefs = new Map();
5
+ const idByElement = new WeakMap();
6
+ const heights = {};
7
+ let snapshot = {};
8
+
9
+ function emit() {
10
+ snapshot = { ...heights };
11
+ for ( const listener of listeners ) {
12
+ listener();
13
+ }
14
+ }
15
+
16
+ const observer = new window.ResizeObserver( ( entries ) => {
17
+ let changed = false;
18
+ for ( const entry of entries ) {
19
+ const id = idByElement.get( entry.target );
20
+ const newHeight = entry.borderBoxSize[ 0 ].blockSize;
21
+ if ( heights[ id ] !== newHeight ) {
22
+ heights[ id ] = newHeight;
23
+ changed = true;
24
+ }
25
+ }
26
+ if ( changed ) {
27
+ emit();
28
+ }
29
+ } );
30
+
31
+ return {
32
+ subscribe( listener ) {
33
+ listeners.add( listener );
34
+ return () => {
35
+ listeners.delete( listener );
36
+ if ( listeners.size === 0 ) {
37
+ observer.disconnect();
38
+ }
39
+ };
40
+ },
41
+
42
+ getSnapshot() {
43
+ return snapshot;
44
+ },
45
+
46
+ registerThread( id, blockEl, floatingEl ) {
47
+ blockRefs.set( id, blockEl );
48
+
49
+ const prev = floatingRefs.get( id );
50
+ if ( prev && prev !== floatingEl ) {
51
+ observer.unobserve( prev );
52
+ idByElement.delete( prev );
53
+ }
54
+ if ( floatingEl ) {
55
+ floatingRefs.set( id, floatingEl );
56
+ idByElement.set( floatingEl, id );
57
+ observer.observe( floatingEl );
58
+ }
59
+
60
+ emit();
61
+ },
62
+
63
+ unregisterThread( id ) {
64
+ blockRefs.delete( id );
65
+ const prev = floatingRefs.get( id );
66
+ if ( prev ) {
67
+ observer.unobserve( prev );
68
+ idByElement.delete( prev );
69
+ floatingRefs.delete( id );
70
+ }
71
+ delete heights[ id ];
72
+ },
73
+
74
+ getBlockRects() {
75
+ // Batch all rect reads before any writes to avoid layout thrashing.
76
+ return Object.fromEntries(
77
+ Array.from( blockRefs ).flatMap( ( [ id, el ] ) =>
78
+ el ? [ [ id, el.getBoundingClientRect() ] ] : []
79
+ )
80
+ );
81
+ },
82
+ };
83
+ }
@@ -15,13 +15,13 @@ import {
15
15
  VisuallyHidden,
16
16
  } from '@wordpress/components';
17
17
  import { __ } from '@wordpress/i18n';
18
- import { useInstanceId, useDebounce } from '@wordpress/compose';
18
+ import { useInstanceId } from '@wordpress/compose';
19
19
  import { isKeyboardEvent } from '@wordpress/keycodes';
20
20
 
21
21
  /**
22
22
  * Internal dependencies
23
23
  */
24
- import { sanitizeCommentString, noop } from './utils';
24
+ import { sanitizeCommentString } from './utils';
25
25
 
26
26
  function CommentForm( {
27
27
  onSubmit,
@@ -29,19 +29,11 @@ function CommentForm( {
29
29
  thread,
30
30
  submitButtonText,
31
31
  labelText,
32
- reflowComments = noop,
33
32
  } ) {
34
33
  const [ inputComment, setInputComment ] = useState(
35
34
  thread?.content?.raw ?? ''
36
35
  );
37
36
 
38
- // Regularly trigger a reflow as the user types since the textarea may grow or shrink.
39
- const debouncedCommentUpdated = useDebounce( reflowComments, 100 );
40
-
41
- const updateComment = ( value ) => {
42
- setInputComment( value );
43
- };
44
-
45
37
  const inputId = useInstanceId( CommentForm, 'comment-input' );
46
38
  const isDisabled =
47
39
  inputComment === thread?.content?.raw ||
@@ -64,10 +56,9 @@ function CommentForm( {
64
56
  <TextareaAutosize
65
57
  id={ inputId }
66
58
  value={ inputComment ?? '' }
67
- onChange={ ( comment ) => {
68
- updateComment( comment.target.value );
69
- debouncedCommentUpdated();
70
- } }
59
+ onChange={ ( comment ) =>
60
+ setInputComment( comment.target.value )
61
+ }
71
62
  rows={ 1 }
72
63
  maxRows={ 20 }
73
64
  onKeyDown={ ( event ) => {
@@ -10,7 +10,6 @@ import {
10
10
  useState,
11
11
  RawHTML,
12
12
  useEffect,
13
- useCallback,
14
13
  useMemo,
15
14
  useRef,
16
15
  } from '@wordpress/element';
@@ -41,7 +40,8 @@ import { unlock } from '../../lock-unlock';
41
40
  import CommentAuthorInfo from './comment-author-info';
42
41
  import CommentForm from './comment-form';
43
42
  import { focusCommentThread, getCommentExcerpt } from './utils';
44
- import { useFloatingThread } from './hooks';
43
+ import { useFloatingBoard, useFloatingThread } from './hooks';
44
+ import { FloatingContainer } from './floating-container';
45
45
  import { AddComment } from './add-comment';
46
46
  import { store as editorStore } from '../../store';
47
47
 
@@ -54,17 +54,9 @@ export function Comments( {
54
54
  onAddReply,
55
55
  onCommentDelete,
56
56
  commentSidebarRef,
57
- reflowComments,
58
57
  isFloating = false,
59
- commentLastUpdated,
60
58
  } ) {
61
- const [ heights, setHeights ] = useState( {} );
62
- const [ boardOffsets, setBoardOffsets ] = useState( {} );
63
- const [ blockRefs, setBlockRefs ] = useState( {} );
64
-
65
- const { setCanvasMinHeight, selectNote } = unlock(
66
- useDispatch( editorStore )
67
- );
59
+ const { selectNote } = unlock( useDispatch( editorStore ) );
68
60
  const { selectBlock, toggleBlockSpotlight } = unlock(
69
61
  useDispatch( blockEditorStore )
70
62
  );
@@ -182,151 +174,13 @@ export function Comments( {
182
174
  }
183
175
  }, [ noteFocused, selectedNote, selectNote, commentSidebarRef ] );
184
176
 
185
- // Recalculate floating comment thread offsets whenever the heights change.
186
- useEffect( () => {
187
- /**
188
- * Calculate the y offsets for all comment threads. Account for potentially
189
- * overlapping threads and adjust their positions accordingly.
190
- */
191
- const calculateAllOffsets = () => {
192
- const offsets = {};
193
-
194
- if ( ! isFloating ) {
195
- return { offsets, minHeight: 0 };
196
- }
197
-
198
- // Find the index of the selected thread.
199
- const selectedThreadIndex = threads.findIndex(
200
- ( t ) => t.id === selectedNote
201
- );
202
-
203
- const breakIndex =
204
- selectedThreadIndex === -1 ? 0 : selectedThreadIndex;
205
-
206
- // If there is a selected thread, push threads above up and threads below down.
207
- const selectedThreadData = threads[ breakIndex ];
208
-
209
- if (
210
- ! selectedThreadData ||
211
- ! blockRefs[ selectedThreadData.id ]
212
- ) {
213
- return { offsets, minHeight: 0 };
214
- }
215
-
216
- let blockElement = blockRefs[ selectedThreadData.id ];
217
- let blockRect = blockElement?.getBoundingClientRect();
218
- const selectedThreadTop = blockRect?.top || 0;
219
- const selectedThreadHeight = heights[ selectedThreadData.id ] || 0;
220
-
221
- offsets[ selectedThreadData.id ] = -16;
222
-
223
- let previousThreadData = {
224
- threadTop: selectedThreadTop - 16,
225
- threadHeight: selectedThreadHeight,
226
- };
227
-
228
- // Process threads after the selected thread, offsetting any overlapping
229
- // threads downward.
230
- for ( let i = breakIndex + 1; i < threads.length; i++ ) {
231
- const thread = threads[ i ];
232
- if ( ! blockRefs[ thread.id ] ) {
233
- continue;
234
- }
235
-
236
- blockElement = blockRefs[ thread.id ];
237
- blockRect = blockElement?.getBoundingClientRect();
238
- const threadTop = blockRect?.top || 0;
239
- const threadHeight = heights[ thread.id ] || 0;
240
-
241
- let additionalOffset = -16;
242
-
243
- // Check if the thread overlaps with the previous one.
244
- const previousBottom =
245
- previousThreadData.threadTop +
246
- previousThreadData.threadHeight;
247
- if ( threadTop < previousBottom + 16 ) {
248
- // Shift down by the difference plus a margin to avoid overlap.
249
- additionalOffset = previousBottom - threadTop + 20;
250
- }
251
-
252
- offsets[ thread.id ] = additionalOffset;
253
-
254
- // Update for next iteration.
255
- previousThreadData = {
256
- threadTop: threadTop + additionalOffset,
257
- threadHeight,
258
- };
259
- }
260
-
261
- // Process threads before the selected thread, offsetting any overlapping
262
- // threads upward.
263
- let nextThreadData = {
264
- threadTop: selectedThreadTop - 16,
265
- };
266
-
267
- for ( let i = selectedThreadIndex - 1; i >= 0; i-- ) {
268
- const thread = threads[ i ];
269
- if ( ! blockRefs[ thread.id ] ) {
270
- continue;
271
- }
272
-
273
- blockElement = blockRefs[ thread.id ];
274
- blockRect = blockElement?.getBoundingClientRect();
275
- const threadTop = blockRect?.top || 0;
276
- const threadHeight = heights[ thread.id ] || 0;
277
-
278
- let additionalOffset = -16;
279
-
280
- // Calculate the bottom position of this thread with default offset.
281
- const threadBottom = threadTop + threadHeight;
282
-
283
- // Check if this thread's bottom would overlap with the next thread's top.
284
- if ( threadBottom > nextThreadData.threadTop ) {
285
- // Shift up by the difference plus a margin to avoid overlap.
286
- additionalOffset =
287
- nextThreadData.threadTop -
288
- threadTop -
289
- threadHeight -
290
- 20;
291
- }
292
-
293
- offsets[ thread.id ] = additionalOffset;
294
-
295
- // Update for next iteration (going upward).
296
- nextThreadData = {
297
- threadTop: threadTop + additionalOffset,
298
- };
299
- }
300
-
301
- let editorMinHeight = 0;
302
- // Take the calculated top of the final note plus its height as the editor min height.
303
- const lastThread = threads[ threads.length - 1 ];
304
- if ( blockRefs[ lastThread.id ] ) {
305
- const lastBlockElement = blockRefs[ lastThread.id ];
306
- const lastBlockRect = lastBlockElement?.getBoundingClientRect();
307
- const lastThreadTop = lastBlockRect?.top || 0;
308
- const lastThreadHeight = heights[ lastThread.id ] || 0;
309
- const lastThreadOffset = offsets[ lastThread.id ] || 0;
310
- editorMinHeight =
311
- lastThreadTop + lastThreadHeight + lastThreadOffset + 32;
312
- }
313
-
314
- return { offsets, minHeight: editorMinHeight };
315
- };
316
- const { offsets: newOffsets, minHeight } = calculateAllOffsets();
317
- if ( Object.keys( newOffsets ).length > 0 ) {
318
- setBoardOffsets( newOffsets );
177
+ const { boardOffsets, registerThread, unregisterThread } = useFloatingBoard(
178
+ {
179
+ threads,
180
+ selectedNoteId: selectedNote,
181
+ isFloating,
319
182
  }
320
- // Ensure the editor has enough height to scroll to all notes.
321
- setCanvasMinHeight( minHeight );
322
- }, [
323
- heights,
324
- blockRefs,
325
- isFloating,
326
- threads,
327
- selectedNote,
328
- setCanvasMinHeight,
329
- ] );
183
+ );
330
184
 
331
185
  const handleThreadNavigation = ( event, thread, isSelected ) => {
332
186
  if ( event.defaultPrevented ) {
@@ -393,10 +247,6 @@ export function Comments( {
393
247
  }
394
248
  };
395
249
 
396
- const setBlockRef = useCallback( ( id, blockRef ) => {
397
- setBlockRefs( ( prev ) => ( { ...prev, [ id ]: blockRef } ) );
398
- }, [] );
399
-
400
250
  const hasThreads = Array.isArray( threads ) && threads.length > 0;
401
251
  // A special case for `template-locked` mode - https://github.com/WordPress/gutenberg/pull/72646.
402
252
  if ( ! hasThreads && ! isFloating ) {
@@ -425,12 +275,16 @@ export function Comments( {
425
275
  onEditComment={ onEditComment }
426
276
  isSelected={ selectedNote === thread.id }
427
277
  commentSidebarRef={ commentSidebarRef }
428
- reflowComments={ reflowComments }
429
- isFloating={ isFloating }
430
- calculatedOffset={ boardOffsets[ thread.id ] ?? 0 }
431
- setHeights={ setHeights }
432
- setBlockRef={ setBlockRef }
433
- commentLastUpdated={ commentLastUpdated }
278
+ floating={
279
+ isFloating
280
+ ? {
281
+ calculatedOffset:
282
+ boardOffsets[ thread.id ] ?? 0,
283
+ registerThread,
284
+ unregisterThread,
285
+ }
286
+ : undefined
287
+ }
434
288
  onKeyDown={ ( event ) =>
435
289
  handleThreadNavigation(
436
290
  event,
@@ -451,22 +305,14 @@ function Thread( {
451
305
  onCommentDelete,
452
306
  isSelected,
453
307
  commentSidebarRef,
454
- reflowComments,
455
- isFloating,
456
- calculatedOffset,
457
- setHeights,
458
- setBlockRef,
459
- commentLastUpdated,
308
+ floating,
460
309
  onKeyDown,
461
310
  } ) {
311
+ const isFloating = !! floating;
462
312
  const { toggleBlockHighlight, selectBlock, toggleBlockSpotlight } = unlock(
463
313
  useDispatch( blockEditorStore )
464
314
  );
465
315
  const { selectNote } = unlock( useDispatch( editorStore ) );
466
- const selectedNote = useSelect(
467
- ( select ) => unlock( select( editorStore ) ).getSelectedNote(),
468
- []
469
- );
470
316
  const relatedBlockElement = useBlockElement( thread.blockClientId );
471
317
  const debouncedToggleBlockHighlight = useDebounce(
472
318
  toggleBlockHighlight,
@@ -474,11 +320,9 @@ function Thread( {
474
320
  );
475
321
  const { y, refs } = useFloatingThread( {
476
322
  thread,
477
- calculatedOffset,
478
- setHeights,
479
- setBlockRef,
480
- selectedThread: selectedNote,
481
- commentLastUpdated,
323
+ calculatedOffset: floating?.calculatedOffset ?? 0,
324
+ registerThread: floating?.registerThread,
325
+ unregisterThread: floating?.unregisterThread,
482
326
  } );
483
327
  const isKeyboardTabbingRef = useRef( false );
484
328
 
@@ -570,19 +414,16 @@ function Thread( {
570
414
  <AddComment
571
415
  onSubmit={ onAddReply }
572
416
  commentSidebarRef={ commentSidebarRef }
573
- reflowComments={ reflowComments }
574
- isFloating={ isFloating }
575
- y={ y }
576
- refs={ refs }
417
+ floating={ { y, refs } }
577
418
  />
578
419
  );
579
420
  }
580
421
 
581
422
  return (
582
- <VStack
423
+ <FloatingContainer
424
+ floating={ isFloating ? { y, refs } : undefined }
583
425
  className={ clsx( 'editor-collab-sidebar-panel__thread', {
584
426
  'is-selected': isSelected,
585
- 'is-floating': isFloating,
586
427
  } ) }
587
428
  id={ `comment-thread-${ thread.id }` }
588
429
  spacing="3"
@@ -607,8 +448,6 @@ function Thread( {
607
448
  role="treeitem"
608
449
  aria-label={ ariaLabel }
609
450
  aria-expanded={ isSelected }
610
- ref={ isFloating ? refs.setFloating : undefined }
611
- style={ isFloating ? { top: y } : undefined }
612
451
  >
613
452
  <Button
614
453
  className="editor-collab-sidebar-panel__skip-to-comment"
@@ -647,7 +486,6 @@ function Thread( {
647
486
  }
648
487
  } }
649
488
  onDelete={ onCommentDelete }
650
- reflowComments={ reflowComments }
651
489
  />
652
490
  { isSelected &&
653
491
  allReplies.map( ( reply ) => (
@@ -658,7 +496,6 @@ function Thread( {
658
496
  isExpanded={ isSelected }
659
497
  onEdit={ onEditComment }
660
498
  onDelete={ onCommentDelete }
661
- reflowComments={ reflowComments }
662
499
  />
663
500
  ) ) }
664
501
  { ! isSelected && restReplies.length > 0 && (
@@ -694,7 +531,6 @@ function Thread( {
694
531
  isExpanded={ isSelected }
695
532
  onEdit={ onEditComment }
696
533
  onDelete={ onCommentDelete }
697
- reflowComments={ reflowComments }
698
534
  />
699
535
  ) }
700
536
  { isSelected && (
@@ -741,7 +577,6 @@ function Thread( {
741
577
  thread.id,
742
578
  thread.author_name
743
579
  ) }
744
- reflowComments={ reflowComments }
745
580
  />
746
581
  </VStack>
747
582
  </VStack>
@@ -759,18 +594,11 @@ function Thread( {
759
594
  { __( 'Back to block' ) }
760
595
  </Button>
761
596
  ) }
762
- </VStack>
597
+ </FloatingContainer>
763
598
  );
764
599
  }
765
600
 
766
- const CommentBoard = ( {
767
- thread,
768
- parent,
769
- isExpanded,
770
- onEdit,
771
- onDelete,
772
- reflowComments,
773
- } ) => {
601
+ const CommentBoard = ( { thread, parent, isExpanded, onEdit, onDelete } ) => {
774
602
  const [ actionState, setActionState ] = useState( false );
775
603
  const [ showConfirmDialog, setShowConfirmDialog ] = useState( false );
776
604
  const actionButtonRef = useRef( null );
@@ -930,7 +758,6 @@ const CommentBoard = ( {
930
758
  thread.id,
931
759
  thread.author_name
932
760
  ) }
933
- reflowComments={ reflowComments }
934
761
  />
935
762
  ) : (
936
763
  <RawHTML
@@ -0,0 +1,29 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import clsx from 'clsx';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { __experimentalVStack as VStack } from '@wordpress/components';
10
+
11
+ export function FloatingContainer( {
12
+ floating,
13
+ className,
14
+ style,
15
+ children,
16
+ ...props
17
+ } ) {
18
+ const isFloating = !! floating;
19
+ return (
20
+ <VStack
21
+ className={ clsx( className, { 'is-floating': isFloating } ) }
22
+ ref={ isFloating ? floating.refs.setFloating : undefined }
23
+ style={ isFloating ? { top: floating.y, ...style } : style }
24
+ { ...props }
25
+ >
26
+ { children }
27
+ </VStack>
28
+ );
29
+ }
@@ -12,10 +12,10 @@ import {
12
12
  */
13
13
  import { __ } from '@wordpress/i18n';
14
14
  import {
15
+ useState,
15
16
  useEffect,
16
17
  useMemo,
17
- useCallback,
18
- useReducer,
18
+ useSyncExternalStore,
19
19
  } from '@wordpress/element';
20
20
  import { useEntityRecords, store as coreStore } from '@wordpress/core-data';
21
21
  import { useDispatch, useRegistry, useSelect } from '@wordpress/data';
@@ -33,16 +33,12 @@ import { store as interfaceStore } from '@wordpress/interface';
33
33
  import { store as editorStore } from '../../store';
34
34
  import { FLOATING_NOTES_SIDEBAR } from './constants';
35
35
  import { unlock } from '../../lock-unlock';
36
- import { noop } from './utils';
36
+ import { createBoardStore } from './board-store';
37
+ import { calculateAllOffsets } from './utils';
37
38
 
38
39
  const { useBlockElement, cleanEmptyObject } = unlock( blockEditorPrivateApis );
39
40
 
40
41
  export function useBlockComments( postId ) {
41
- const [ commentLastUpdated, reflowComments ] = useReducer(
42
- () => Date.now(),
43
- 0
44
- );
45
-
46
42
  const queryArgs = {
47
43
  post: postId,
48
44
  type: 'note',
@@ -165,12 +161,10 @@ export function useBlockComments( postId ) {
165
161
  return {
166
162
  resultComments,
167
163
  unresolvedSortedThreads,
168
- reflowComments,
169
- commentLastUpdated,
170
164
  };
171
165
  }
172
166
 
173
- export function useBlockCommentsActions( reflowComments = noop ) {
167
+ export function useBlockCommentsActions() {
174
168
  const { createNotice } = useDispatch( noticesStore );
175
169
  const { saveEntityRecord, deleteEntityRecord } = useDispatch( coreStore );
176
170
  const { getCurrentPostId } = useSelect( editorStore );
@@ -224,10 +218,8 @@ export function useBlockCommentsActions( reflowComments = noop ) {
224
218
  isDismissible: true,
225
219
  }
226
220
  );
227
- setTimeout( reflowComments, 300 );
228
221
  return savedRecord;
229
222
  } catch ( error ) {
230
- reflowComments();
231
223
  onError( error );
232
224
  }
233
225
  };
@@ -292,9 +284,7 @@ export function useBlockCommentsActions( reflowComments = noop ) {
292
284
  isDismissible: true,
293
285
  }
294
286
  );
295
- reflowComments();
296
287
  } catch ( error ) {
297
- reflowComments();
298
288
  onError( error );
299
289
  }
300
290
  };
@@ -326,9 +316,7 @@ export function useBlockCommentsActions( reflowComments = noop ) {
326
316
  type: 'snackbar',
327
317
  isDismissible: true,
328
318
  } );
329
- reflowComments();
330
319
  } catch ( error ) {
331
- reflowComments();
332
320
  onError( error );
333
321
  }
334
322
  };
@@ -366,26 +354,55 @@ export function useEnableFloatingSidebar( enabled = false ) {
366
354
  }, [ enabled, registry ] );
367
355
  }
368
356
 
357
+ export function useFloatingBoard( { threads, selectedNoteId, isFloating } ) {
358
+ const [ boardOffsets, setBoardOffsets ] = useState( {} );
359
+ const [ store ] = useState( createBoardStore );
360
+ const { setCanvasMinHeight } = unlock( useDispatch( editorStore ) );
361
+
362
+ const heights = useSyncExternalStore( store.subscribe, store.getSnapshot );
363
+
364
+ // Recalc is deferred to a rAF; the cleanup cancels the pending frame
365
+ // when deps change, so back-to-back updates collapse into one paint.
366
+ useEffect( () => {
367
+ if ( ! isFloating ) {
368
+ return;
369
+ }
370
+
371
+ const rafId = window.requestAnimationFrame( () => {
372
+ const { offsets, minHeight } = calculateAllOffsets( {
373
+ threads,
374
+ selectedNoteId,
375
+ blockRects: store.getBlockRects(),
376
+ heights,
377
+ } );
378
+ setBoardOffsets( offsets );
379
+ setCanvasMinHeight( minHeight );
380
+ } );
381
+
382
+ return () => window.cancelAnimationFrame( rafId );
383
+ }, [
384
+ heights,
385
+ isFloating,
386
+ selectedNoteId,
387
+ setCanvasMinHeight,
388
+ store,
389
+ threads,
390
+ ] );
391
+
392
+ return {
393
+ boardOffsets,
394
+ registerThread: store.registerThread,
395
+ unregisterThread: store.unregisterThread,
396
+ };
397
+ }
398
+
369
399
  export function useFloatingThread( {
370
400
  thread,
371
401
  calculatedOffset,
372
- setHeights,
373
- selectedThread,
374
- setBlockRef,
375
- commentLastUpdated,
402
+ registerThread,
403
+ unregisterThread,
376
404
  } ) {
377
405
  const blockElement = useBlockElement( thread.blockClientId );
378
- const updateHeight = useCallback(
379
- ( id, newHeight ) => {
380
- setHeights( ( prev ) => {
381
- if ( prev[ id ] !== newHeight ) {
382
- return { ...prev, [ id ]: newHeight };
383
- }
384
- return prev;
385
- } );
386
- },
387
- [ setHeights ]
388
- );
389
406
 
390
407
  // Use floating-ui to track the block element's position with the calculated offset.
391
408
  const { y, refs } = useFloating( {
@@ -398,32 +415,27 @@ export function useFloatingThread( {
398
415
  whileElementsMounted: autoUpdate,
399
416
  } );
400
417
 
401
- // Store the block reference for each thread.
418
+ // Set the floating-ui reference element.
402
419
  useEffect( () => {
403
420
  if ( blockElement ) {
404
421
  refs.setReference( blockElement );
405
422
  }
406
- }, [ blockElement, refs, commentLastUpdated ] );
407
-
408
- // Track thread heights.
409
- useEffect( () => {
410
- if ( refs.floating?.current ) {
411
- setBlockRef( thread.id, blockElement );
412
- }
413
- }, [ blockElement, thread.id, refs.floating, setBlockRef ] );
423
+ }, [ blockElement, refs ] );
414
424
 
415
- // When the selected thread changes, update heights, triggering offset recalculation.
425
+ // Register block + floating elements with the board.
426
+ // The board's ResizeObserver tracks height changes automatically.
416
427
  useEffect( () => {
417
- if ( refs.floating?.current ) {
418
- const newHeight = refs.floating.current.scrollHeight;
419
- updateHeight( thread.id, newHeight );
428
+ const floatingEl = refs.floating?.current;
429
+ if ( floatingEl && registerThread ) {
430
+ registerThread( thread.id, blockElement, floatingEl );
420
431
  }
432
+ return () => unregisterThread?.( thread.id );
421
433
  }, [
434
+ blockElement,
422
435
  thread.id,
423
- updateHeight,
424
436
  refs.floating,
425
- selectedThread,
426
- commentLastUpdated,
437
+ registerThread,
438
+ unregisterThread,
427
439
  ] );
428
440
 
429
441
  return {