Skip to content

Notes: keep a floating thread in place when its block is hidden - #81549

Open
adamsilverstein wants to merge 12 commits into
trunkfrom
fix/73565-hidden-block-note-anchor
Open

adamsilverstein wants to merge 12 commits into
trunkfrom
fix/73565-hidden-block-note-anchor

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Aug 13, 2026 •

Copy link
Copy Markdown
Member

Test in WordPress Playground

Rebuilt on top of #83485 with Claude, which also rewrote this description:

Fixes #73565

When a block carrying a note is hidden at the current viewport, block.js renders nothing for it as soon as it is deselected. The thread then has no element to anchor to. Since #83485 the thread no longer drops to the top of the notes panel - it stays invisible until the block is selected. That keeps it out of the way, but the note disappears from the floating view for as long as its block is hidden.

This gives those threads a stand-in anchor: the gap the hidden block leaves between its rendered neighbours. That spot is not arbitrary - it is exactly where the block sits when it is selected, since a selected hidden block is still rendered, just collapsed to zero height so the block toolbar keeps working. Anchoring there means the thread does not shift when you click it.

The thread also says why it is not pointing at anything. "Block is hidden." is the same phrase the block inspector already uses, and the thread's accessible name becomes "Note on hidden block: ..." so screen reader users get the same context.

A note thread for a hidden block, sitting in the gap the block left between two paragraphs, with a Block is hidden notice

How it fits the board store

Following the suggestion in #73565 (comment), the fallback lives next to getNoteAnchorRect, in the board store's single measurement pass, so it adds no extra DOM reads or timers:

  • Each thread registers its block's client ID along with its elements, and useFloatingBoard hands the store the document's block order (getClientIdsWithDescendants).
  • In measure(), a thread with no block element gets its anchor from getHiddenBlockAnchorRect, which walks to the nearest rendered block on each side and returns the midpoint of the gap. A hidden last block falls back to the bottom of the block before it, a hidden first block to the top of the block after it.
  • Neighbours that fail checkVisibility() are skipped, for the same reason Notes: Refactor floating board measurement #83485 climbs out of a closed Details block: collapsed content still reports its expanded box.
  • The store finds the canvas root by climbing from a registered block. When every noted block is hidden there is none, so it looks the root up from the threads' document instead (the editor-canvas iframe, or the same document when the canvas is not iframed).

On the [Status] Blocked label

The issue is blocked on #73838 (hidden blocks should still be visible in the editor), which is still in discussion. This fix does not depend on how that lands: it only kicks in when a noted block has no rendered element. If hidden blocks later render as ghosts, or anything else visible, useBlockElement returns a real element and this path never runs. Suggest removing the blocked label.

The other two scenarios in the original issue - block transform and Group wrapping - were fixed by #77433, so this covers what was left.

How has this been tested?

Manually, in a wide viewport so the floating notes panel is active:

  1. Add three paragraphs: a long one, then "Noted paragraph", then "Trailing paragraph".
  2. Select the second paragraph and add a note. Confirm the thread lines up with the block.
  3. With that block still selected, Options -> Hide, check "Hide on Desktop", Apply, then press Escape.
  4. Click the first paragraph to deselect. On trunk the thread disappears. With this change it holds the gap where the block was, and reads "Block is hidden."
  5. Click the thread. It expands in place and keeps the notice.
  6. With the block still selected, Options -> Show, uncheck "Hide on Desktop", Apply. Deselect, and the thread realigns with the block.

Automated:

  • Three Playwright tests in a new "Hidden blocks" group in test/e2e/specs/editor/various/block-notes-floating.spec.js: the middle-block case, the select/show round trip, and a hidden last block that is the only noted block. All three fail with trunk's board store and pass with this change.
  • Full block-notes-floating.spec.js (37 passed) and block-notes.spec.js (58 passed) runs locally.
  • Unit tests for getHiddenBlockAnchorRect (including skipping collapsed content), a board-store test for a thread whose block isn't rendered, and the tie-breaking cases in calculateNotePositions.

Types of changes

Bug fix.

  • Add getHiddenBlockAnchorRect to derive a boundary anchor for a block with no rendered element.
  • Measure that anchor in the board store, which now tracks each thread's block client ID and the document's block order.
  • Find the canvas root from the threads' document when no noted block is rendered.
  • Show a "Block is hidden." notice on the thread and adjust its accessible name.

Open questions

  • Copy: "Block is hidden." matches the inspector, but it could hint at the affordance instead, eg. "Block is hidden. Select it in the List View to show it."
  • The notice does not distinguish per-viewport hiding from hidden-everywhere. One generic string felt like enough for now.

AI Use

Code and description both came out of a session with 🤖 Claude Code, including the rebuild on #83485. I will review and test.

A block hidden at the current viewport renders nothing once it is
deselected, so its note threads lose the element they anchor to. Add a
pure helper that resolves the gap such a block leaves between its
rendered neighbours, centred where the block itself sits while selected
(back in the DOM, collapsed to zero height), so callers have a rect to
position those threads with.
Threads whose block has no rendered element came back from the board
with no rect, so the position loop skipped them and they fell to their
static position at the top of the notes panel. Substitute the boundary
rect for those threads inside the existing rAF read batch, and stop
getFirstBlockElement returning a hidden block's null entry, which cost
the board its scroll container.
A thread anchored to a hidden block sits next to content it does not
belong to and highlights nothing on hover, with no explanation. Show
the same notice the block inspector uses - while the block is out of
the DOM and while it is selected but still invisible - and give the
thread an accessible name that carries the state to screen readers.
Measure against a noted block that is not the first one: a thread that
lost its anchor parks at the top of the panel, which coincidentally
matches a first block's position.
@github-actions github-actions Bot added the [Package] Editor /packages/editor label Aug 13, 2026
@adamsilverstein adamsilverstein added [Type] Bug An existing feature does not function as intended [Feature] Notes Phase 3 of the Gutenberg roadmap around block commenting labels Aug 13, 2026
@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

Size Change: +477 B (+0.01%)

Total Size: 7.92 MB

📦 View Changed
Filename Size Change
build/scripts/editor/index.min.js 581 kB +455 B (+0.08%)
build/styles/editor/style-rtl.css 31.8 kB +6 B (+0.02%)
build/styles/editor/style-rtl.min.css 27.2 kB +4 B (+0.01%)
build/styles/editor/style.css 31.8 kB +7 B (+0.02%)
build/styles/editor/style.min.css 27.2 kB +5 B (+0.02%)

compressed-size-action

@github-actions

Copy link
Copy Markdown

Flaky tests detected in 569b024.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/33116216951
📝 Reported tests:

refuses the drop and uploads nothing in /test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js, passed after 1 failed attempt.
Error: expect(received).toHaveLength(expected)

Expected length: 0
Received length: 2
Received array:  [{"_links": {"about": [{"href": "http://localhost:8889/wp-json/wp/v2/types/attachment"}], "author": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/users/1"}], "collection": [{"href": "http://localhost:8889/wp-json/wp/v2/media"}], "replies": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/comments?post=110"}], "self": [{"href": "http://localhost:8889/wp-json/wp/v2/media/110", "targetHints": {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE"]}}]}, "alt_text": "", "author": 1, "caption": {"rendered": ""}, "class_list": ["post-110", "attachment", "type-attachment", "status-inherit", "hentry", "entry"], "comment_status": "open", "date": "2026-08-27T21:12:59", "date_gmt": "2026-08-27T21:12:59", "description": {"rendered": "<p class=\"attachment\"><a href='http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK-1.png'><img loading=\"lazy\" decoding=\"async\" width=\"10\" height=\"10\" src=\"http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK-1.png\" class=\"attachment-medium size-medium\" alt=\"\" style=\"width:100%;height:100%;max-width:10px;\" /></a></p>
"}, "featured_media": 0, "filename": "10x10_e2e_test_image_z9T8jK-1.png", "filesize": 80, "guid": {"rendered": "http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK-1.png"}, "id": 110, "link": "http://localhost:8889/10x10_e2e_test_image_z9t8jk-2/", "media_details": {"file": "2026/08/10x10_e2e_test_image_z9T8jK-1.png", "filesize": 80, "height": 10, "image_meta": {"alt": "", "aperture": "0", "camera": "", "caption": "", "copyright": "", "created_timestamp": "0", "credit": "", "focal_length": "0", "iso": "0", "keywords": [], "orientation": "0", "shutter_speed": "0", "title": ""}, "sizes": {}, "width": 10}, "media_type": "image", "meta": [], "mime_type": "image/png", "modified": "2026-08-27T21:12:59", "modified_gmt": "2026-08-27T21:12:59", "ping_status": "closed", "post": null, "slug": "10x10_e2e_test_image_z9t8jk-2", "source_url": "http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK-1.png", "status": "inherit", "template": "", "title": {"rendered": "10x10_e2e_test_image_z9T8jK"}, "type": "attachment"}, {"_links": {"about": [{"href": "http://localhost:8889/wp-json/wp/v2/types/attachment"}], "author": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/users/1"}], "collection": [{"href": "http://localhost:8889/wp-json/wp/v2/media"}], "replies": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/comments?post=106"}], "self": [{"href": "http://localhost:8889/wp-json/wp/v2/media/106", "targetHints": {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE"]}}]}, "alt_text": "", "author": 1, "caption": {"rendered": ""}, "class_list": ["post-106", "attachment", "type-attachment", "status-inherit", "hentry", "entry"], "comment_status": "open", "date": "2026-08-27T21:12:39", "date_gmt": "2026-08-27T21:12:39", "description": {"rendered": "<p class=\"attachment\"><a href='http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK.png'><img loading=\"lazy\" decoding=\"async\" width=\"10\" height=\"10\" src=\"http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK.png\" class=\"attachment-medium size-medium\" alt=\"\" style=\"width:100%;height:100%;max-width:10px;\" /></a></p>
"}, "featured_media": 0, "filename": "10x10_e2e_test_image_z9T8jK.png", "filesize": 80, "guid": {"rendered": "http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK.png"}, "id": 106, "link": "http://localhost:8889/10x10_e2e_test_image_z9t8jk/", "media_details": {"file": "2026/08/10x10_e2e_test_image_z9T8jK.png", "filesize": 80, "height": 10, "image_meta": {"alt": "", "aperture": "0", "camera": "", "caption": "", "copyright": "", "created_timestamp": "0", "credit": "", "focal_length": "0", "iso": "0", "keywords": [], "orientation": "0", "shutter_speed": "0", "title": ""}, "sizes": {}, "width": 10}, "media_type": "image", "meta": [], "mime_type": "image/png", "modified": "2026-08-27T21:12:39", "modified_gmt": "2026-08-27T21:12:39", "ping_status": "closed", "post": null, "slug": "10x10_e2e_test_image_z9t8jk", "source_url": "http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK.png", "status": "inherit", "template": "", "title": {"rendered": "10x10_e2e_test_image_z9T8jK"}, "type": "attachment"}]
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js:71:45

adamsilverstein and others added 3 commits October 6, 2026 16:19
Take trunk's board store and floating board hook from #83485; the
hidden-block anchor is rebuilt on top of them in the following commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFNXAzK5ogseY5tCtU2x53
The floating board now measures in the store's ResizeObserver pass (#83485),
so the stand-in anchor for a block with no rendered element moves there
from the old rAF batch. Threads register their block's client ID and the
store keeps the document's block order, so it can place the thread between
the neighbours that are rendered. With every noted block hidden, the store
finds the canvas root from the threads' document instead.

Neighbours inside collapsed content are skipped, since they still report
their expanded box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFNXAzK5ogseY5tCtU2x53
The floating alignment tests moved to block-notes-floating.spec.js on
trunk, so the hidden-block cases follow them and reuse its helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFNXAzK5ogseY5tCtU2x53
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

🎉 Props

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>
Co-authored-by: Vrishabhsk <vrishabhsk@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>
Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>
Co-authored-by: t-hamano <wildworks@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Updated as activity occurs, without notifying anyone named here. Add the props-bot label to refresh.

📦 Bundle size

Size Change: +466 B (+0.01%)

Total Size: 8.51 MB

📦 View Changed
Filename Size Change
build/scripts/editor/index.min.js 637 kB +445 B (+0.07%)
build/styles/editor/style-rtl.css 32.6 kB +8 B (+0.02%)
build/styles/editor/style-rtl.min.css 27.7 kB +3 B (+0.01%)
build/styles/editor/style.css 32.7 kB +6 B (+0.02%)
build/styles/editor/style.min.css 27.7 kB +4 B (+0.01%)

d000aa9 Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric f244704 trunk % Change
timeToFirstByte 55.25 ms +10.77% -6.52% 53.5 ms +13.93% -2.9% 3.27%
largestContentfulPaint 96 ms +8.33% -6.25% 92 ms +4.35% -6.52% 4.35%
lcpMinusTtfb 38.8 ms +10.82% -12.24% 34.2 ms +26.46% -5.26% 13.45%
wpBeforeTemplate 28.57 ms +18.73% -2.42% 28.31 ms +15.05% -1.06% 0.92%
wpTemplate 21.79 ms +7.71% -6.56% 20.69 ms +9.28% -1.88% 5.32%
wpTotal 51.67 ms +10.92% -7.1% 49.96 ms +14.23% -2.94% 3.42%
wpMemoryUsage 7.61 MB +0% -0% 7.58 MB +0% -0% 0.46%
wpDbQueries 17 +0% -0% 17 +0% -0% 0%

front-end-classic-theme

Metric f244704 trunk % Change
timeToFirstByte 45.15 ms +7.75% -0.78% 41.5 ms +8.8% -2.41% 8.8%
largestContentfulPaint 100 ms +6% -0% 92 ms +0% -4.35% 8.7%
lcpMinusTtfb 54.95 ms +1.64% -2.82% 47.5 ms +5.79% -2% 15.68%
wpBeforeTemplate 26.05 ms +5.68% -1.54% 26.4 ms +6.93% -1.63% -1.33%
wpTemplate 16.13 ms +2.85% -2.11% 12.21 ms +3.28% -2.38% 32.1%
wpTotal 42.09 ms +8.08% -0.88% 38.74 ms +9.03% -2.43% 8.65%
wpMemoryUsage 6.25 MB +0% -0% 6.10 MB +0% -0% 2.4%
wpDbQueries 14 +0% -0% 10 +0% -0% 40%

media-processing

Metric f244704 trunk % Change
mediaProcessingJpeg 396.47 ms +2.52% -0.37% 400.44 ms +0.97% -1.18% -0.99%
mediaProcessingAvif 6048.03 ms +0.4% -0.03% 6076.55 ms +0.18% -0.16% -0.47%
mediaProcessingJpegToAvif 4183.75 ms +0.22% -0.18% 4196.84 ms +0.17% -0.19% -0.31%

media-upload

Metric f244704 trunk % Change
jpegUploadProcessing 1430.79 ms +36.33% -0.72% 1434.48 ms +0.72% -0.6% -0.26%
pngUploadProcessing 192.06 ms +9.94% -8.81% 223 ms +7.81% -11.87% -13.87%
largeJpegUploadProcessing 1415.28 ms +0.4% -0.8% 1410.91 ms +0.79% -0.47% 0.31%
multipleImageUploadProcessing 1564.74 ms +1.79% -0.74% 1603.24 ms +9.03% -2.84% -2.4%

post-editor

Metric f244704 trunk % Change
serverResponse 320.99 ms +3.31% -8.62% 325.47 ms +4.98% -10.33% -1.38%
firstPaint 184.73 ms +8.21% -6.02% 170.36 ms +12.3% -7.53% 8.44%
domContentLoaded 872.77 ms +1.77% -0.59% 871.34 ms +2.57% -2.75% 0.16%
loaded 873.85 ms +1.76% -0.6% 872.38 ms +2.6% -2.73% 0.17%
firstContentfulPaint 378.17 ms +2.57% -1.51% 378.94 ms +0.68% -4.45% -0.2%
firstBlock 2543.24 ms +1.09% -0.39% 2548.64 ms +0.88% -2.07% -0.21%
type 16.34 ms +2.75% -7.83% 15.77 ms +5.39% -2.16% 3.61%
typeWithoutInspector 15.72 ms +2.99% -3.82% 16.33 ms +3.92% -4.9% -3.74%
typeWithTopToolbar 18.64 ms +2.63% -1.07% 19.98 ms +4.5% -3% -6.71%
typeContainer 7.35 ms +10.34% -2.99% 7.46 ms +11.13% -4.42% -1.47%
focus 89.15 ms +7.28% -7.36% 92.6 ms +10.76% -4.14% -3.73%
firstFocus 170.3 ms +0% -0% 171.75 ms +0% -0% -0.84%
selectAll 462.79 ms +1.29% -3.24% 461.66 ms +4.61% -6.03% 0.24%
listViewOpen 60.98 ms +3.92% -6.13% 58.68 ms +5.47% -3.37% 3.92%
inserterOpen 33.8 ms +7.9% -8.17% 33.98 ms +6.97% -9.54% -0.53%
inserterHover 9.57 ms +17.03% -9.51% 9.83 ms +7.73% -7.63% -2.64%
inserterSearch 7.12 ms +5.76% -5.2% 7.26 ms +6.61% -5.65% -1.93%
loadPatterns 507.98 ms +1.28% -1.66% 488.02 ms +8.37% -2.1% 4.09%
wpTotal 311.32 ms +2.99% -8.9% 315.81 ms +5.22% -10.69% -1.42%
wpMemoryUsage 13.33 MB +0% -0% 13.30 MB +0% -0% 0.25%
wpDbQueries 54 +0% -1.85% 54 +0% -0% 0%

site-editor

Metric f244704 trunk % Change
serverResponse 513.63 ms +4.02% -6.4% 499.66 ms +3.76% -3.55% 2.8%
firstPaint 259.04 ms +26.46% -8.21% 258.15 ms +24.49% -10.45% 0.34%
domContentLoaded 1207.09 ms +2.6% -2.06% 1228.17 ms +1.38% -2.36% -1.72%
loaded 1208.35 ms +2.6% -2.07% 1229.45 ms +1.41% -2.36% -1.72%
firstContentfulPaint 484.7 ms +2.22% -1.28% 493.51 ms +2.21% -1.83% -1.79%
firstBlock 4582.6 ms +0.63% -0.63% 4621.52 ms +0.85% -0.88% -0.84%
type 18.34 ms +8.51% -2.67% 18.96 ms +4.59% -4.17% -3.27%
navigate 127.17 ms +5.43% -6.72% 124.31 ms +11.2% -2.45% 2.3%
loadPatterns 1260.13 ms +3% -2.25% 1259.24 ms +3.58% -4.69% 0.07%
loadPages 1018.44 ms +2.11% -2.88% 1008.66 ms +0.95% -1.25% 0.97%
wpTotal 503.18 ms +3.98% -6.68% 488.56 ms +3.86% -3.71% 2.99%
wpMemoryUsage 12.38 MB +0% -0% 12.34 MB +0% -0% 0.3%
wpDbQueries 43 +2.33% -0% 43 +2.33% -0% 0%

d000aa9 Run

Resolve the note-position sort conflict by keeping trunk's toSorted() and
the branch's comment about hidden blocks sharing a boundary rect.

Claude-Session: https://claude.ai/code/session_01RmMwj4CN9cF8b1Sx58Pvik
The 15.2.0 release renamed the Unreleased heading, stranding this PR's
entry under a published version and failing the changelog check.

Claude-Session: https://claude.ai/code/session_01RmMwj4CN9cF8b1Sx58Pvik

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Notes Phase 3 of the Gutenberg roadmap around block commenting [Package] Editor /packages/editor [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Re-selecting a hidden block loses its note position

1 participant