Skip to content

feat: A1 · Inspector sidebar (block + document settings) - #349

Merged
ViewFromTheBox merged 3 commits into
release/1.0from
feature/343-inspector-sidebar
Apr 21, 2026
Merged

ViewFromTheBox merged 3 commits into
release/1.0from
feature/343-inspector-sidebar

Conversation

@ViewFromTheBox

@ViewFromTheBox ViewFromTheBox commented Apr 21, 2026 •

Copy link
Copy Markdown
Member

Description

Replaces the M7 right-sidebar placeholder (#343) with a real tabbed inspector. The Block tab renders @wordpress/block-editor's <BlockInspector /> so core blocks get Color / Typography / Dimensions / Border / Advanced panels for free. The Document tab adds built-in Status & visibility, Featured image, Excerpt, and (opt-in) Discussion panels, plus two host-extension surfaces so developers can plug in their own panels.

Title has moved into the canvas (WordPress-style); slug and status have moved into the Document panel. The top bar keeps only save indicator + undo/redo + inserter/inspector toggles + preview + more-options menu.

Closes: #343

Type of Change

  • New feature (adds new functionality)

Related Issue

Issue: #343

Follow-ups filed (not blocking this PR):

Motivation and Context

V1 expansion plan (docs/plans/11-v1-expansion.md, Phase A) requires the inspector sidebar to ship with real block + document settings before 1.0.0. M7 (#317) left both sidebars as literal "lands in a later milestone" placeholder strings — not a shippable CMS experience. A1 delivers the block-settings half of that work.

Changes Made

New components

  • InspectorSidebar — ARIA-compliant tabbed sidebar shell. Block and Document tabs always visible; arrow-key navigation; focus moves to the active tab on mount; auto-switches to Block when a block gains selection; Block tab shows "Click on a block to view its settings." empty state when nothing is selected.
  • DocumentPanels — built-in panels:
    • Status & visibility (status select, slug input, optional author picker — rendered only when the host passes authorOptions)
    • Featured image — consumes the existing editor.MediaUpload slot-fill; will light up automatically when A3 (A3 · Media library integration #345) replaces the bridge stub
    • Excerpt — gated by supports.excerpt !== false
    • Discussion (comments toggle) — gated by supports.comments === true (default off, since host model compatibility varies)
  • PluginDocumentSettingPanel + ap.visual-editor.document-panels filter — two ways for host apps to contribute custom panels, both funnel into one slot. Exported from the package main.
  • PostTitle — large, auto-growing textarea rendered at the top of the canvas above the block list. Keyboard focus indicator passes WCAG contrast.

Changed

  • TopBar strips title/slug/status inputs and their callbacks; CSS and tests updated accordingly.
  • EditorApp wires everything; new mount props initialExcerpt, initialFeaturedImage, initialAuthorId, initialCommentsOpen, authorOptions, supports.
  • usePersistence splits save-buffering from state update. queueBlocksForSave(next) schedules a debounced save without calling setBlocks. onInput now uses that path so drag frames don't re-render the editor tree; onChange still mirrors for undo/redo.
  • Default autosave debounce bumped 800ms → 1500ms for better coalescing under rapid input.
  • Blade <x-visual-editor> component + VisualEditorComponent PHP class emit the new mount data attributes (JSON-encoded for arrays/objects).
  • Vue wrapper passes the new model fields and author options.
  • StrictMode wrapper dropped from the editor mount — upstream @wordpress/* v32/15 components aren't StrictMode-safe in dev mode and the double-invocation amplifies backpressure during drag-heavy interactions.

Block-support tuning

  • @wordpress/format-library imported for its side effect — registers core rich-text formats (bold, italic, link, inline code, etc.) so the block toolbar's inline formatting controls actually render inside RichText blocks.
  • settings.typography.fontFamilies.custom shape provided so the Font family picker appears in the Typography panel.
  • settings.typography.textAlign: true enables the toolbar text-alignment controls on paragraph/heading.
  • Minimal __experimentalFeatures shape — color (text/background, custom), typography (full set), alignWide. Layout/border/spacing/customGradient deliberately left off for this pass to reduce the surface area of the color-drag backpressure (they can be reintroduced alongside A1 follow-up · Iframe the editor canvas (fixes color-picker fast-drag crash) #347).
  • supports.color.enableContrastChecker = false applied globally via a blocks.registerBlockType filter. Upstream's deps-less useLayoutEffect in BlockColorContrastChecker is part of the backpressure picture; follow-up A1 follow-up · Restore contrast warning via artisanpack-ui/accessibility #348 reimplements contrast warning against the artisanpack-ui/accessibility package without DOM-measurement.

Gutenberg pin changes

Pinned the following down one minor line:

  • @wordpress/block-editor: 15.17.0 → 15.16.0
  • @wordpress/block-library: 9.44.0 → 9.43.0
  • @wordpress/blocks: 15.17.0 → 15.16.0
  • @wordpress/components: 32.6.0 → 32.5.0
  • @wordpress/core-data: 7.44.0 → 7.43.0
  • @wordpress/format-library: (new) → 5.43.0
  • @wordpress/patterns: 2.44.0 → 2.43.0
  • @wordpress/reusable-blocks: 5.44.0 → 5.43.0

15.17 shipped a state.blockListSettings store-shape regression that broke the editor when our integration tried to register blocks; 15.16 is stable.

How Has This Been Tested?

Testing Environment:

  • Operating System: macOS (Darwin 25.3.0)
  • Browser: Safari
  • PHP Version: 8.4.3
  • Project Version: 1.0.0 (release/1.0)

Tests Performed:

  1. Inspector sidebar opens via the top-bar inspector button; both tabs render; Document tab active on load.
  2. Selecting a block auto-activates the Block tab; clearing selection keeps Block tab with empty-state copy.
  3. Arrow-left/right on the tablist cycles tabs with focus; Tab indexes match ARIA tabs pattern.
  4. Paragraph block shows Color / Typography / Dimensions / Border / Advanced panels with working controls (font family, font size, line height, alignment, custom hex colors via input, preset colors via palette click).
  5. Bold/italic/link/inline-code toolbar items appear inside selected RichText blocks.
  6. PostTitle above the canvas accepts input and grows with the content; focus outline visible under keyboard focus.
  7. Top bar no longer exposes title/slug/status inputs; status/slug edits in the Document panel still round-trip through onMetadataChange.
  8. Save indicator updates after the 1.5s debounce; ⌘S flushes immediately; undo/redo still work (⌘Z / ⌘⇧Z).
  9. Autosave POSTs /visual-editor/api/... with the updated block tree.
  10. Vue wrapper mounts correctly with the new model fields.

Known limitation (see #347): fast-drag on the gradient color picker crashes the block with "Maximum update depth exceeded" inside Gutenberg's block-support render pipeline. Presets, slow drag, hex input, numeric RGB/HSL input all work. The fix is iframing the canvas (which is what WordPress itself does) — tracked as #347.

Accessibility Tests Run

  • Keyboard navigation tested — ARIA tabs pattern on inspector tablist; PostTitle focus-visible outline; existing top-bar keyboard-shortcuts menu behavior preserved
  • Screen reader tested — aria-label, aria-selected, aria-controls, role="tab"/"tabpanel"/"complementary" all correctly wired
  • Color contrast verified — inspector active-tab underline uses DaisyUI --color-primary; PostTitle focus ring uses same. DaisyUI tokens are contrast-tested at the host level
  • ARIA labels checked — "Inspector", "Inspector tabs", "Block", "Document", plus the existing "Editor top bar" and "More options" labels translated via __() with TEXT_DOMAIN

Details: Contrast warning on text/background combinations is temporarily off (filter disables the upstream checker) — tracked as #348 for re-introduction via artisanpack-ui/accessibility.

Tests Added

  • Unit tests added/updated — 30 new JS tests, 2 new PHP tests
  • Integration tests added/updated — top-bar tests updated for removed inputs
  • All tests passing — 475 JS / 152 PHP, all green

Test details:

  • inspector-sidebar.test.tsx (10 tests) — tab visibility, selection auto-switch, empty-state, arrow-key focus, tab click handling, initial focus
  • document-panels.test.tsx (13 tests) — status/slug/author change flows, support gates (excerpt/featuredImage/comments), featured-image onSelect flow, filter-registered panels, author-option type preservation
  • plugin-document-setting-panel.test.tsx (5 tests) — filter ordering with stable sort, malformed entry filtering, slot-fill render
  • post-title.test.tsx (4 tests) — render, placeholder handling, onChange wiring
  • VisualEditorComponentTest.php (+2) — document-panel data-attr emission, omission when props unset

Documentation

Documentation details: The PluginDocumentSettingPanel, DOCUMENT_PANELS_FILTER, and DocumentPanelSpec exports are fully JSDoc'd with usage guidance for both slot-fill and hooks-based extension patterns.

Pre-Submission Checklist

  • Followed contributing guidelines
  • Checked for other open PRs for same update
  • Code passes all tests
  • Code has been linted (CodeRabbit --base release/1.0 returned "No findings" after addressing the initial PostTitle focus-visible recommendation)
  • Accessibility tests completed
  • Code follows project style guide
  • Self-review completed
  • Comments added for complex code
  • No new warnings generated

Summary by CodeRabbit

  • New Features

    • Rich inline text formats, in-canvas editable post title, accessible inspector with Block/Document tabs, and enhanced document metadata panels (status/slug, excerpt, featured image, author, discussion). Panels are plugin-extensible.
  • Style

    • Updated sidebar layouts and new inspector/post-title/featured-image styling for improved layout and accessibility.
  • Tests

    • Added comprehensive unit and accessibility tests covering inspector, document panels, post title, and related utilities.
  • Chores

    • Updated WordPress package dependencies and expanded mount/config options to support document metadata.

Replaces the M7 placeholder right sidebar with a real tabbed inspector:

- `InspectorSidebar` shell with ARIA tabs pattern (Block / Document),
  arrow-key navigation, focus management, and an empty-state message
  on the Block tab when nothing is selected.
- Block tab renders `@wordpress/block-editor`'s `<BlockInspector />`
  so core blocks get Color / Typography / Dimensions / Border /
  Advanced panels for free via block supports.
- Document tab with built-in panels: Status & visibility (status,
  slug, optional author picker), Featured image (consumes the media
  bridge — will light up automatically when A3 lands), Excerpt, and
  Discussion (conditional on host opt-in). Panels are independently
  gated via `EditorAppProps.supports`.
- Host extension API: `<PluginDocumentSettingPanel>` slot-fill
  component *and* the `ap.visual-editor.document-panels` filter;
  both funnel into a single slot so WP-style React devs and Laravel-
  style hooks devs get consistent surfaces.
- Title moves from the top bar into the canvas (WP-style), slug and
  status move from the top bar into the Document sidebar panel.
  Top bar is now save-indicator + undo/redo + inserter/inspector
  toggles + preview + menu only.
- `PostTitle` component renders a focus-visible, auto-grow textarea
  above the block list.
- `@wordpress/format-library` added and imported for its side effect
  so bold/italic/link/inline-code formats register and the block
  toolbar's inline formatting controls show up inside RichText.
- `settings.typography.fontFamilies.custom` shape provided so the
  Font family picker renders alongside size / weight / line height.
- `supports.color.enableContrastChecker = false` applied globally via
  a `blocks.registerBlockType` filter — upstream's deps-less
  `useLayoutEffect` in `BlockColorContrastChecker` was contributing
  to the color-drag crash. Follow-up #348 re-introduces the warning
  using `artisanpack-ui/accessibility` without the DOM-measurement
  loop.
- `usePersistence.queueBlocksForSave` splits save-buffering from
  `setBlocks`. `onInput` (drag frames, mid-typing) now buffers for
  save without re-rendering the editor tree; `onChange` (persistent
  commits) still mirrors into React state for undo/redo.
- Default autosave debounce bumped 800ms → 1500ms to further reduce
  backpressure during rapid color-picker input.
- StrictMode wrapper dropped from `main.tsx` — v32/15 @wordpress/*
  internals aren't StrictMode-safe and the dev-mode double-invocation
  was amplifying the backpressure.
- Gutenberg packages pinned to `.43 / 15.16 / 32.5` (down from
  `.44 / 15.17 / 32.6`). 15.17 shipped a `blockListSettings` store-
  shape regression that broke the editor with our integration.
- Blade component and Vue wrapper emit/pass the new mount props
  (excerpt, featured image, author, comments-open, author options,
  supports).

Known limitation: fast-drag on the gradient color picker still
crashes the block with "Maximum update depth exceeded" inside
Gutenberg's block-support hooks. Presets, slow drag, hex input, and
numeric RGB/HSL input all work. Follow-up #347 iframes the canvas,
which matches WordPress's own architecture and will eliminate the
backpressure.

Tests: +30 JS tests (inspector sidebar, document panels, extension
API, PostTitle) and +2 PHP tests (Blade data-attr emission). Full
suite green: 475 JS, 152 PHP.

Closes #343

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 21, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a tabbed inspector sidebar (Block / Document) with built-in and plugin-extensible document panels (status/slug/author, featured image, excerpt, discussion), moves the post title into the canvas, extends mount/config/Vue/Blade/PHP inputs for document metadata, refactors debounced persistence, updates test setup (matchMedia polyfill) and WordPress deps, and adds tests and styles.

Changes

Cohort / File(s) Summary
Package & Test Infra
package.json, resources/js/visual-editor/_legacy/editor/test-setup.ts
Adjusted @wordpress/* dependency pins and added several @wordpress packages; added a window.matchMedia polyfill in test setup for jsdom compatibility.
Inspector & Document Panels (UI, types, CSS, tests)
resources/js/visual-editor/editor/inspector-sidebar.tsx, resources/js/visual-editor/editor/inspector-sidebar.css, resources/js/visual-editor/editor/document-panels.tsx, resources/js/visual-editor/editor/post-title.tsx, resources/js/visual-editor/editor/post-title.css, resources/js/visual-editor/editor/__tests__/*
New InspectorSidebar (ARIA tabs, keyboard nav, mount-while-hidden panels) and DocumentPanels component with exported types; PostTitle component and styles; comprehensive tests for inspector, document panels, post title, and featured-image flow.
Plugin Extension Point
resources/js/visual-editor/editor/plugin-document-setting-panel.tsx, resources/js/visual-editor/editor/__tests__/plugin-document-setting-panel.test.tsx
Added slot/fill DocumentPanelSlot + PluginDocumentSettingPanel, DOCUMENT_PANELS_FILTER, getFilteredDocumentPanels() with validation, ordering, and deduping; tests for filter handling and rendering.
Editor Integration & Exports
resources/js/visual-editor/editor/editor-app.tsx, resources/js/visual-editor/editor/editor-app.css, resources/js/visual-editor/editor/main.tsx
Integrated inspector and document panels into EditorApp, imported @wordpress/format-library and hooks filter to disable contrast checker for blocks, expanded editor props/types to include document metadata, re-exported panel utilities, and adjusted sidebar CSS variants.
Persistence & State
resources/js/visual-editor/editor/use-persistence.ts
Introduced queueBlocksForSave() to debounce persistence (debounce increased 800→1500ms); refactored onBlocksChange to update state and schedule debounced save so emitted change carries latest buffered blocks.
Top Bar Simplification
resources/js/visual-editor/editor/top-bar.tsx, resources/js/visual-editor/editor/top-bar.css, resources/js/visual-editor/editor/__tests__/top-bar.test.tsx
Removed title/slug/status controls and related props/CSS from TopBar; tests updated to assert those inputs are no longer rendered.
Host Integration (mount / Vue / Blade / PHP / tests)
resources/js/visual-editor/editor/vue.ts, resources/js/visual-editor/editor/main.tsx, resources/views/components/editor.blade.php, src/View/Components/VisualEditorComponent.php, tests/Feature/VisualEditor/VisualEditorComponentTest.php
Extended mount config/types and Vue wrapper props/model with initial excerpt/featuredImage/authorId/commentsOpen plus authorOptions/supports; added parseJsonDataset() and normalizeAuthorId(); Blade template and PHP component expose conditional data-* attributes and constructor/public properties; feature tests added.
Accessibility & Keyboard/Focus Behavior (tests)
resources/js/visual-editor/editor/__tests__/inspector-sidebar.test.tsx, other __tests__
Added tests for ARIA tab semantics, focus management, keyboard navigation, tab switching behavior, and document-panel interactions (including MediaUpload interactions via mocks).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant InspectorSidebar
    participant DocumentPanels
    participant MediaUpload
    participant EditorApp

    User->>InspectorSidebar: Open Document tab / click "Set featured image"
    InspectorSidebar->>DocumentPanels: Delegate featured-image action
    DocumentPanels->>MediaUpload: Open picker (MediaUpload)
    MediaUpload-->>DocumentPanels: onSelect(media)
    DocumentPanels->>EditorApp: onFeaturedImageChange(validatedMedia)
    EditorApp-->>EditorApp: update local state & emit metadata change
    EditorApp-->>User: UI updates (preview shown)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped in with tabs both bright and sly,

Block to the left, Document on the right,
Images picked, excerpts penned with delight,
I tunneled through tests and styles all night,
Now the editor hums — nibble, click, and write. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: implementing the inspector sidebar with block and document settings panels, which is the main objective of this PR.
Linked Issues check ✅ Passed All acceptance criteria from issue #343 are met: tabbed sidebar with Block/Document tabs, document panels (Status, Featured image, Excerpt, Discussion, extension slot), Block tab renders InspectorControls, empty-state handling, top-bar toggle wiring, DaisyUI theming, ARIA compliance, and comprehensive JS tests.
Out of Scope Changes check ✅ Passed All changes are scoped to the inspector sidebar implementation: new components (InspectorSidebar, DocumentPanels, PluginDocumentSettingPanel, PostTitle), related styling, persistence refactoring, and comprehensive tests. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/343-inspector-sidebar

Comment @coderabbitai help to get the list of available commands and usage tips.

@ViewFromTheBox ViewFromTheBox moved this from Backlog to In Review in ArtisanPack UI Overview Apr 21, 2026
@ViewFromTheBox ViewFromTheBox linked an issue Apr 21, 2026 that may be closed by this pull request
15 tasks
@ViewFromTheBox
ViewFromTheBox marked this pull request as ready for review April 21, 2026 15:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
resources/js/visual-editor/editor/top-bar.tsx (1)

155-181: ⚠️ Potential issue | 🟠 Major

Global undo/redo shortcut handling can hijack native text editing undo.

Line 164 prevents default for Cmd/Ctrl+Z regardless of focused target. With metadata inputs now in the canvas/sidebar, this can block native undo/redo while typing in form fields/contenteditable areas.

Proposed fix
+function isEditableTarget(target: EventTarget | null): boolean {
+    if (!(target instanceof HTMLElement)) {
+        return false;
+    }
+
+    if (target.isContentEditable) {
+        return true;
+    }
+
+    const tag = target.tagName;
+    return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
+}
+
 useEffect(() => {
     function handleShortcut(event: KeyboardEvent): void {
         if (!isModifier(event)) {
             return;
         }
 
         const key = event.key.toLowerCase();
 
         if (key === 'z') {
-            event.preventDefault();
+            if (isEditableTarget(event.target)) {
+                return;
+            }
 
             if (event.shiftKey) {
                 if (canRedo) {
+                    event.preventDefault();
                     onRedo();
                 }
             } else if (canUndo) {
+                event.preventDefault();
                 onUndo();
             }
 
             return;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/visual-editor/editor/top-bar.tsx` around lines 155 - 181, The
global shortcut handler in useEffect's handleShortcut is preventing native
undo/redo while typing; update handleShortcut (used with isModifier, onUndo,
onRedo, onSave, canUndo, canRedo) to first detect whether event.target is an
editable element (e.g., HTMLInputElement, HTMLTextAreaElement, or an element
with isContentEditable true) or an element with a role/attribute indicating form
input, and if so, do nothing (return) so native browser undo/redo is preserved;
only call event.preventDefault() and invoke onUndo/onRedo/onSave when the target
is not editable and the corresponding canUndo/canRedo/onSave checks pass.
🧹 Nitpick comments (1)
tests/Feature/VisualEditor/VisualEditorComponentTest.php (1)

152-154: Consider asserting JSON payload content, not just attribute presence.

Line 152 through Line 154 only checks that the attributes exist; adding value-level assertions (even partial key/value checks) would better catch serialization regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Feature/VisualEditor/VisualEditorComponentTest.php` around lines 152 -
154, The current assertions in VisualEditorComponentTest (the $html checks using
->and( $html )->toContain('data-featured-image='), 'data-author-options=' and
'data-supports=') only verify attribute existence; update the test to extract
each attribute's value from $html (e.g., via regex or a DOM parser), json_decode
the attribute string and assert on specific keys/values (for example check that
featured-image contains expected keys, author-options includes expected author
id or name, and supports contains expected booleans), replacing or augmenting
the toContain assertions so serialization regressions are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Around line 73-81: package.json is missing an explicit dependency on
`@wordpress/hooks` which is imported across multiple modules (e.g.,
plugin-document-setting-panel.tsx, editor-app.tsx, media-bridge/state.ts) and
can cause resolution/hoisting issues; add "@wordpress/hooks" to the dependencies
in package.json using a version that matches the major version used by the other
`@wordpress/`* packages in this file (e.g., align with "15.x" or the major used by
`@wordpress/blocks/`@wordpress/block-editor), ensuring the package name
"@wordpress/hooks" is declared so installers resolve a single compatible copy.

In `@resources/js/visual-editor/editor/document-panels.tsx`:
- Line 145: The memo currently freezes filter-registered panels by calling
useMemo(() => getFilteredDocumentPanels(), []) with an empty dependency array;
update this so getFilteredDocumentPanels() is recomputed whenever the panel
filters change (e.g., remove the empty deps to call it on every render or add
the appropriate reactive dependency such as the filter registry/state that backs
ap.visual-editor.document-panels). Ensure the variable filteredPanels continues
to be assigned from getFilteredDocumentPanels() and that useMemo (or no memo)
references the filter registry/state so panels registered after mount appear.

In `@resources/js/visual-editor/editor/editor-app.tsx`:
- Around line 161-199: The preset label strings in DEFAULT_PALETTE,
DEFAULT_FONT_SIZES, and DEFAULT_FONT_FAMILIES are user-facing and must be
localized; wrap each name value (e.g., 'Base content', 'Small', 'System', etc.)
with the translation helper __() just like other UI strings (e.g., name:
__('Base content')). Keep slugs and other non-display fields unchanged, and
ensure the translation import/usage is consistent with the rest of the file.
- Around line 452-470: handleInput currently only calls
queueBlocksForSave(next), leaving the controlled value passed to
BlockEditorProvider stale; add a lightweight transient state update so the
provider's value stays in sync without forcing the heavy re-render cascade.
Specifically, introduce a transientBlocks state (e.g., transientBlocks,
setTransientBlocks) or a shallow-ref-backed state used as the value for
BlockEditorProvider, update setTransientBlocks(next) inside handleInput
alongside queueBlocksForSave(next), and keep the existing onChange handler to
commit to the main blocks state (setBlocks/commitBlocks) and push history;
ensure the transient value is decoupled from the persistent state so
sidebars/inspector-heavy effects are not retriggered on every frame.

In `@resources/js/visual-editor/editor/inspector-sidebar.tsx`:
- Around line 214-246: The current conditional rendering unmounts the inactive
tabpanel causing PanelBody and plugin state to reset and leaving aria-controls
dangling; modify the render logic so both tabpanels (the block panel containing
BlockInspector and the document panel rendering documentContent) are always
mounted using their existing ids blockPanelId and documentPanelId, and toggle
visibility of the inactive panel (e.g., via a hidden class, aria-hidden, or
style display:none) based on activeTab so state is preserved and aria references
remain valid.

In `@resources/js/visual-editor/editor/main.tsx`:
- Around line 133-165: initialAuthorId is left as the raw dataset string which
can turn numeric ids like "1" into strings; normalize it before returning by
locating the initialAuthorId variable and replacing its use with a normalized
value: if initialAuthorId is falsy string treat as undefined; otherwise, if
authorOptions is available, find an AuthorOption in authorOptions where
option.value === initialAuthorId or option.value === Number(initialAuthorId) and
set initialAuthorId to that option.value (preserving its original number|string
type); if no match, convert numeric-looking strings to Number(initialAuthorId)
otherwise keep the string; finally use this normalized initialAuthorId in the
returned object instead of the raw dataset value so onMetadataChange emits the
correct type.

In `@resources/js/visual-editor/editor/plugin-document-setting-panel.tsx`:
- Around line 115-139: The returned list from getFilteredDocumentPanels (the
pipeline starting from the valid variable and ending with .map(({ panel }) =>
panel)) can contain duplicate DocumentPanelSpec.id values which will cause
duplicate React keys; after filtering and sorting but before the final .map,
deduplicate by id (use the DocumentPanelSpec.id field) so only one panel per id
remains—implement by iterating the sorted array and inserting into a Map keyed
by panel.id (or use a reducer) so that the chosen policy (e.g., last-wins or
first-wins) is explicit; update the pipeline that uses valid, index, sort, and
final map to return the deduplicated array of panels.

In `@resources/js/visual-editor/editor/post-title.css`:
- Around line 49-50: The CSS rule in
resources/js/visual-editor/editor/post-title.css currently uses the deprecated
property `word-break: break-word`; update that rule (the selector block
containing `overflow: hidden; word-break: break-word;`) to remove `word-break:
break-word` and instead add `overflow-wrap: anywhere` so text wrapping behavior
is preserved and compliant with current CSS specs.
- Around line 63-70: The .screen-reader-text rule uses the deprecated clip
property; replace clip: rect(0, 0, 0, 0); with a modern clip-path-based
visually-hidden pattern (e.g., clip-path: inset(50%); and include a prefixed
fallback like -webkit-clip-path: inset(50%)); keep the other properties
(position, width, height, margin, padding, overflow) so the element remains
accessible and visually hidden; update the .screen-reader-text CSS block
accordingly.

---

Outside diff comments:
In `@resources/js/visual-editor/editor/top-bar.tsx`:
- Around line 155-181: The global shortcut handler in useEffect's handleShortcut
is preventing native undo/redo while typing; update handleShortcut (used with
isModifier, onUndo, onRedo, onSave, canUndo, canRedo) to first detect whether
event.target is an editable element (e.g., HTMLInputElement,
HTMLTextAreaElement, or an element with isContentEditable true) or an element
with a role/attribute indicating form input, and if so, do nothing (return) so
native browser undo/redo is preserved; only call event.preventDefault() and
invoke onUndo/onRedo/onSave when the target is not editable and the
corresponding canUndo/canRedo/onSave checks pass.

---

Nitpick comments:
In `@tests/Feature/VisualEditor/VisualEditorComponentTest.php`:
- Around line 152-154: The current assertions in VisualEditorComponentTest (the
$html checks using ->and( $html )->toContain('data-featured-image='),
'data-author-options=' and 'data-supports=') only verify attribute existence;
update the test to extract each attribute's value from $html (e.g., via regex or
a DOM parser), json_decode the attribute string and assert on specific
keys/values (for example check that featured-image contains expected keys,
author-options includes expected author id or name, and supports contains
expected booleans), replacing or augmenting the toContain assertions so
serialization regressions are caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 56016fc9-fc6b-4248-90ba-6c79a5873fd3

📥 Commits

Reviewing files that changed from the base of the PR and between 8d8bcf0 and 44af2c6.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !package-lock.json
📒 Files selected for processing (23)
  • package.json
  • resources/js/visual-editor/_legacy/editor/test-setup.ts
  • resources/js/visual-editor/editor/__tests__/document-panels.test.tsx
  • resources/js/visual-editor/editor/__tests__/inspector-sidebar.test.tsx
  • resources/js/visual-editor/editor/__tests__/plugin-document-setting-panel.test.tsx
  • resources/js/visual-editor/editor/__tests__/post-title.test.tsx
  • resources/js/visual-editor/editor/__tests__/top-bar.test.tsx
  • resources/js/visual-editor/editor/document-panels.tsx
  • resources/js/visual-editor/editor/editor-app.css
  • resources/js/visual-editor/editor/editor-app.tsx
  • resources/js/visual-editor/editor/inspector-sidebar.css
  • resources/js/visual-editor/editor/inspector-sidebar.tsx
  • resources/js/visual-editor/editor/main.tsx
  • resources/js/visual-editor/editor/plugin-document-setting-panel.tsx
  • resources/js/visual-editor/editor/post-title.css
  • resources/js/visual-editor/editor/post-title.tsx
  • resources/js/visual-editor/editor/top-bar.css
  • resources/js/visual-editor/editor/top-bar.tsx
  • resources/js/visual-editor/editor/use-persistence.ts
  • resources/js/visual-editor/editor/vue.ts
  • resources/views/components/editor.blade.php
  • src/View/Components/VisualEditorComponent.php
  • tests/Feature/VisualEditor/VisualEditorComponentTest.php

Comment thread package.json
Comment thread resources/js/visual-editor/editor/document-panels.tsx Outdated
Comment thread resources/js/visual-editor/editor/editor-app.tsx
Comment on lines 452 to +470
const handleInput = useCallback(
(next: BlockInstance[]): void => {
onBlocksChange(next);
// `onInput` fires on intermediate, non-persistent changes —
// color-picker drag frames, mid-typing rich-text updates, etc.
// Calling `setBlocks(next)` here re-renders the entire editor
// tree (TopBar, InspectorSidebar, BlockInspector panels) on
// every frame, which triggers a runaway layout-effect cascade
// inside Gutenberg's block-support hooks and crashes the
// block with "Maximum update depth exceeded" (#343 A1).
//
// The block-editor store already has the latest tree from
// the dispatch that triggered this callback, so the canvas
// stays in sync without our mirror. We only need to queue a
// debounced save — undo/redo still works because `onChange`
// (fired on the persistent commit at the end of the drag)
// does the React state update and history push.
queueBlocksForSave(next);
},
[onBlocksChange]
[queueBlocksForSave]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For @wordpress/block-editor's BlockEditorProvider, is the valueprop a controlled value that should stay synchronized from bothonInputandonChange callbacks? Please cite Gutenberg docs or source/examples.

💡 Result:

Yes. BlockEditorProvider’s value prop is a controlled value (an array of blocks) and it is expected to be kept in sync with whatever state you manage externally. The provider is implemented as a controlled input: it “expected to receive a value of a blocks array” and it “calls onChange and/or onInput when the user interacts to change blocks”, so your callbacks must update the value you pass back in. Regarding whether you need to synchronize updates from both callbacks: the docs distinguish them as “persistent” (onChange) vs “non-persistent” (onInput) changes; however, they also state “While value updates should always be reflected immediately (onInput), you may only want history entries to reflect change milestones (onChange).” So the general pattern is: update the value immediately from onInput, and optionally treat onChange differently (e.g., persistence/history), but you should ensure the authoritative value you pass back stays synchronized with what the editor is doing. Authoritative sources: - BlockEditorProvider package docs (provider README) describe it as a controlled input with value being the current blocks array, and that it calls onChange/onInput on user interaction. - The same README explains the expected sync semantics: updates should be reflected immediately via onInput, while onChange is for persistent milestones. - The @wordpress/block-editor package usage docs provide examples where onInput and onChange both update the external blocks state that is then fed back into value. Example (from docs; both callbacks update the same state): const [ blocks, updateBlocks ] = useState( [] ); return ( <BlockEditorProvider value={ blocks } onInput={ ( nextBlocks ) => updateBlocks( nextBlocks ) } onChange={ ( nextBlocks ) => updateBlocks( nextBlocks ) } > ... );

Citations:


BlockEditorProvider's controlled value must stay in sync with onInput changes.

Per Gutenberg's BlockEditorProvider documentation, value is a controlled prop that should be updated immediately from onInput callbacks. The current code at line 468 skips the state update (calling only queueBlocksForSave) to avoid rerender cascades, but this leaves the value passed to BlockEditorProvider stale during transient edits. If the shell re-renders before onChange commits, it can replay a stale block tree into the provider and silently drop in-progress typing or drag state.

The decorator's comment correctly identifies the rerender cost problem, but the solution must sync the controlled value without triggering cascades—likely by decoupling the save queue from the editor state update or using a different synchronization strategy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/visual-editor/editor/editor-app.tsx` around lines 452 - 470,
handleInput currently only calls queueBlocksForSave(next), leaving the
controlled value passed to BlockEditorProvider stale; add a lightweight
transient state update so the provider's value stays in sync without forcing the
heavy re-render cascade. Specifically, introduce a transientBlocks state (e.g.,
transientBlocks, setTransientBlocks) or a shallow-ref-backed state used as the
value for BlockEditorProvider, update setTransientBlocks(next) inside
handleInput alongside queueBlocksForSave(next), and keep the existing onChange
handler to commit to the main blocks state (setBlocks/commitBlocks) and push
history; ensure the transient value is decoupled from the persistent state so
sidebars/inspector-heavy effects are not retriggered on every frame.

Comment thread resources/js/visual-editor/editor/inspector-sidebar.tsx Outdated
Comment thread resources/js/visual-editor/editor/main.tsx
Comment thread resources/js/visual-editor/editor/plugin-document-setting-panel.tsx
Comment thread resources/js/visual-editor/editor/post-title.css Outdated
Comment thread resources/js/visual-editor/editor/post-title.css Outdated
- Declare `@wordpress/hooks@4.43.0` explicitly in package.json — we
  import `addFilter` / `applyFilters` across editor-app, media-bridge,
  and the extension API, and relying on transitive resolution was
  fragile (got picked up through multiple nested copies).
- Remove `useMemo([])` around `getFilteredDocumentPanels()` —
  `@wordpress/hooks` filters are global module state with no React-
  level subscription, so memoizing froze the panel list at mount and
  hid any panels a host registers after that point. Now recomputed
  each render (one cheap `applyFilters` call).
- Wrap `DEFAULT_PALETTE`, `DEFAULT_FONT_SIZES`, and
  `DEFAULT_FONT_FAMILIES` name labels in `__()` so the pot-extraction
  tooling picks them up.
- Keep both inspector tabpanels mounted; toggle `hidden` on the
  inactive one so `PanelBody` open-state, filter-registered plugin
  state, and ARIA references survive tab switches.
- Normalize `initialAuthorId` from the Blade dataset: match against
  `authorOptions` first (preserving the host-declared number|string
  type), fall back to numeric coercion when the string looks numeric,
  otherwise leave as a string. `onMetadataChange` now emits the
  correct type for Laravel hosts that store integer author ids.
- Deduplicate filter-registered panels by id with a last-wins policy,
  matching how `@wordpress/hooks` itself composes filters.
- Swap deprecated CSS: `word-break: break-word` → `overflow-wrap:
  anywhere`; `clip: rect(...)` → `clip-path: inset(50%)` with
  `-webkit-clip-path` for older Safari.
- Add a rationale comment to `handleInput` explaining why letting the
  controlled `value` go stale between commits is intentional. Passing
  `setTransientBlocks(next)` on every drag frame — as one review
  suggestion proposed — would reintroduce the render cascade that
  causes "Maximum update depth exceeded" during color drag; there's
  no React pattern that feeds a reactive `value` prop without
  triggering a render, and the block-editor store already keeps
  itself in sync, so the mirror isn't needed.

Tests: +2 for tabpanel `hidden` toggling, +1 for panel dedupe, +6 for
`normalizeAuthorId`. Full suite: 483 JS / 152 PHP, all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@resources/js/visual-editor/editor/main.tsx`:
- Around line 142-157: The parsed authorOptions value from parseJsonDataset may
be valid JSON but not an array; before calling normalizeAuthorId or using .find,
validate that authorOptions is an actual array (e.g.
Array.isArray(authorOptions)) and if not treat it as null (or an empty array) so
downstream code like normalizeAuthorId(rawAuthorId, authorOptions) and any .find
calls won’t throw; update the initialization around authorOptions and the
use-site for initialAuthorId to pass the safe/normalized value instead of the
raw parsed result.

In `@resources/js/visual-editor/editor/plugin-document-setting-panel.tsx`:
- Around line 141-152: The dedup logic using `deduped.set(panel.id, panel)`
doesn't move an existing key to its new sorted position because Map#set
preserves insertion order; update the loop in plugin-document-setting-panel.tsx
to, for each `panel` in `sorted`, if `deduped.has(panel.id)` first
`deduped.delete(panel.id)` then `deduped.set(panel.id, panel)` so the later
duplicate is reinserted at the correct position; keep returning
`Array.from(deduped.values())`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3cd231c3-4828-4c3e-b4e2-c0f26527c6dc

📥 Commits

Reviewing files that changed from the base of the PR and between 44af2c6 and cfa72bc.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !package-lock.json
📒 Files selected for processing (10)
  • package.json
  • resources/js/visual-editor/editor/__tests__/inspector-sidebar.test.tsx
  • resources/js/visual-editor/editor/__tests__/main.test.ts
  • resources/js/visual-editor/editor/__tests__/plugin-document-setting-panel.test.tsx
  • resources/js/visual-editor/editor/document-panels.tsx
  • resources/js/visual-editor/editor/editor-app.tsx
  • resources/js/visual-editor/editor/inspector-sidebar.tsx
  • resources/js/visual-editor/editor/main.tsx
  • resources/js/visual-editor/editor/plugin-document-setting-panel.tsx
  • resources/js/visual-editor/editor/post-title.css
✅ Files skipped from review due to trivial changes (5)
  • package.json
  • resources/js/visual-editor/editor/post-title.css
  • resources/js/visual-editor/editor/inspector-sidebar.tsx
  • resources/js/visual-editor/editor/tests/inspector-sidebar.test.tsx
  • resources/js/visual-editor/editor/document-panels.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • resources/js/visual-editor/editor/tests/plugin-document-setting-panel.test.tsx
  • resources/js/visual-editor/editor/editor-app.tsx

Comment thread resources/js/visual-editor/editor/main.tsx Outdated
Comment thread resources/js/visual-editor/editor/plugin-document-setting-panel.tsx
- `readMountConfig` now guards `authorOptions` with `Array.isArray` —
  if a host mis-emits `data-author-options` as a non-array value
  (object, primitive, etc.), we treat it as null instead of crashing
  `normalizeAuthorId`'s `.find` or the SelectControl props builder
  downstream.
- `getFilteredDocumentPanels` dedupe now deletes-before-re-inserting
  when it sees a duplicate id. `Map#set` on an existing key updates
  the value but preserves the original insertion position; since our
  last-wins policy should also move the panel to its *later*
  position, the delete-then-set sequence is required for ordering to
  reflect the most recent registration.

Tests updated to assert the positional part of dedupe (`['unique',
'shared']`, not just `length === 2`) and that `normalizeAuthorId`
tolerates the null fallback.

Full suite: 484 JS / 152 PHP, all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
resources/js/visual-editor/editor/plugin-document-setting-panel.tsx (1)

26-27: Consider consolidating imports from the same package.

Both imports are from @wordpress/components and could be combined into a single statement.

♻️ Suggested consolidation
-import { PanelBody } from '@wordpress/components';
-import { createSlotFill } from '@wordpress/components';
+import { PanelBody, createSlotFill } from '@wordpress/components';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/visual-editor/editor/plugin-document-setting-panel.tsx` around
lines 26 - 27, Consolidate the two separate imports from `@wordpress/components`
into a single import statement: replace the two lines importing PanelBody and
createSlotFill with one line importing both (e.g., import { PanelBody,
createSlotFill } from '@wordpress/components';) and remove the duplicate import;
this change affects the top-level imports in plugin-document-setting-panel.tsx
and requires no further code changes if PanelBody and createSlotFill are only
used by name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@resources/js/visual-editor/editor/plugin-document-setting-panel.tsx`:
- Around line 26-27: Consolidate the two separate imports from
`@wordpress/components` into a single import statement: replace the two lines
importing PanelBody and createSlotFill with one line importing both (e.g.,
import { PanelBody, createSlotFill } from '@wordpress/components';) and remove
the duplicate import; this change affects the top-level imports in
plugin-document-setting-panel.tsx and requires no further code changes if
PanelBody and createSlotFill are only used by name.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 260a6f86-4221-4450-a00f-9e9314514a60

📥 Commits

Reviewing files that changed from the base of the PR and between cfa72bc and 1be45cd.

📒 Files selected for processing (4)
  • resources/js/visual-editor/editor/__tests__/main.test.ts
  • resources/js/visual-editor/editor/__tests__/plugin-document-setting-panel.test.tsx
  • resources/js/visual-editor/editor/main.tsx
  • resources/js/visual-editor/editor/plugin-document-setting-panel.tsx
✅ Files skipped from review due to trivial changes (1)
  • resources/js/visual-editor/editor/tests/plugin-document-setting-panel.test.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • resources/js/visual-editor/editor/tests/main.test.ts
  • resources/js/visual-editor/editor/main.tsx

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

A1 · Inspector sidebar (block + document settings)

1 participant