Skip to content

feat: block inserter — sidebar panel + slash command - #287

Merged
ViewFromTheBox merged 2 commits into
add/phase-onefrom
feature/273-block-inserter-sidebar-slash-command
Apr 14, 2026
Merged

ViewFromTheBox merged 2 commits into
add/phase-onefrom
feature/273-block-inserter-sidebar-slash-command

Conversation

@ViewFromTheBox

@ViewFromTheBox ViewFromTheBox commented Apr 14, 2026 •

Copy link
Copy Markdown
Member

Description

Implements Phase 1.11: gives users two ways to add blocks to the canvas — a searchable sidebar inserter panel and an in-editor slash command. Both consume a new inserter registry that seeds with the built-in text blocks from #272 and will merge in API-supplied definitions once #274 lands.

Closes: #273

Type of Change

  • New feature (adds new functionality)

Related Issue

Issue: #273

Motivation and Context

Phase 1 needs a discovery surface for inserting blocks. The sidebar covers the "I don't know what's available" flow, and the slash command covers the keyboard-first "I know what I want" flow. Both go through the same registry so new block types only need to be registered once to show up in both.

Changes Made

  • editor/inserter/ module
    • inserterRegistry.ts — cached-snapshot registry with registerInserterBlock, getInserterBlocks, subscribeInserterBlocks, and a parallel block-factory map. The snapshot is frozen and only rebuilt on mutation so useSyncExternalStore stays stable.
    • builtinInserterBlocks.ts — seeds paragraph + heading metadata and factories.
    • loadInserterBlocks.ts — async loader that seeds built-ins and optionally GET {apiBase}/blocks. Network errors and non-OK responses are swallowed so the editor always boots with a usable set.
    • insertBlockActions.ts — insertBlockAtSelection inserts after the current selection (or appends if nothing is selected) and selects the new block. replaceBlockWithInserterBlock swaps a block for a brand-new one via replaceBlocks so undo reverses the replacement in a single step.
    • filterInserterBlocks.ts — case-insensitive search over title, name, description, and keywords.
  • InserterPanel — sidebar component with a search input and a click-to-insert list. Subscribes to the registry via useSyncExternalStore and dispatches insertBlockAtSelection on click.
  • SlashCommandPopover — listbox popover with mouse + keyboard selection state.
  • ParagraphEdit integration — detects `/query` in the block's plain text from onUpdate, filters the inserter list, handles ArrowUp/ArrowDown/Escape via a capture-phase window keydown listener, routes Enter through Tiptap's keymap to commit the replacement, and suppresses the Backspace-merge path while the slash popover is open.
  • EditorShell layout — adds a ve-editor-shell__body flex container so the InserterPanel sits beside the canvas.
  • main.tsx boot — now calls loadInserterBlocks({ apiBase }).
  • History captures selection — HistorySnapshot is now { blocks, selection }. Undo and redo restore both, guarded by selectionExistsIn. This fixes the focus-loss after undoing a slash-command replacement: the restored snapshot's selection correctly points at the block the user was editing when the change happened, so the paragraph block re-mounts with isSelected=true and the focus effect places the caret back in the editor.

How Has This Been Tested?

Testing Environment:

  • Operating System: macOS Darwin 25.3.0
  • Browser: manually verified via npm run dev:editor
  • Project Version: visual-editor `1.0.0-spike`

Tests Performed:

  1. Sidebar panel lists Paragraph and Heading; clicking either inserts after the selected block (or appends if none is selected).
  2. Sidebar search filters the list live; empty state appears for no-match queries.
  3. Typing `/` in a paragraph opens the popover listing both built-in blocks.
  4. Typing `/head` filters the popover to Heading.
  5. ArrowDown / ArrowUp cycle the popover's selected item; Enter commits and replaces the paragraph with the chosen block; Escape closes the popover; clicking an item also commits.
  6. Cmd/Ctrl+Z after a slash-command replacement restores the original paragraph with the caret placed inside it, so typing resumes immediately.
  7. Cmd/Ctrl+Shift+Z (redo) jumps back to the inserted block.

Accessibility Tests Run

  • Keyboard navigation tested (tab into sidebar search; arrow keys inside the slash popover; Escape to dismiss)
  • Screen reader tested
  • Color contrast verified (sidebar and popover use the existing editor palette)
  • ARIA labels checked (sidebar is an `` with `aria-label`, search input has `aria-label`; popover uses `role="listbox"` with each item `role="option"` and `aria-selected`)

Details:

The search input has both a visually-hidden `<span class="ve-sr-only">` label and an explicit `aria-label`. Popover items expose their selected state via `aria-selected` and a data attribute used for tests. Mouse-down on the popover preventDefaults to avoid stealing focus from the contenteditable mid-interaction.

Tests Added

  • Unit tests added/updated
  • Integration tests added/updated
  • All tests passing

Test details:

34 new Vitest specs across five files, plus updates to the history test:

  • `editor/inserter/tests/inserterRegistry.test.ts` — stable snapshot reference, snapshot invalidation on mutation, subscriber notification, built-in seeding, filter matching (title/name/description/keywords, case-insensitive), and `loadInserterBlocks` paths (no apiBase, successful fetch, network failure, non-OK response).
  • `editor/inserter/tests/insertBlockActions.test.ts` — append when unselected, insert after selection, selects new block, unknown factory returns null, replacement selects new block, replacement is a single undo entry.
  • `editor/components/tests/InserterPanel.test.tsx` — lists built-ins, filters by query, shows empty state, click inserts after selection, click appends when unselected.
  • `editor/blocks/tests/slashCommand.test.tsx` — popover opens on `/`, filters by text after slash, ArrowDown/ArrowUp cycle the selection, Enter commits and replaces the paragraph, Escape closes, click commits.
  • `editor/store/tests/editorStoreHistory.test.ts` — three new specs covering the new selection capture: restored on undo, restored on redo, and the guarded-fallback path.

Full suite: 221 tests passing.

Documentation

  • Inline code documentation added
  • README updated
  • Wiki updated
  • API documentation updated

Documentation details:

JSDoc on the public helpers (`loadInserterBlocks`, `insertBlockAtSelection`, `replaceBlockWithInserterBlock`, `filterInserterBlocks`, `registerBuiltinInserterBlocks`). Wiki / README updates land with the Phase 1 closeout.

Pre-Submission Checklist

  • Followed contributing guidelines
  • Checked for other open PRs for same update
  • Code passes all tests
  • Code has been linted (TypeScript strict, CodeRabbit clean)
  • Accessibility tests completed (ARIA labels + keyboard paths)
  • Code follows project style guide
  • Self-review completed
  • Comments added for complex code
  • No new warnings generated

Summary by CodeRabbit

  • New Features

    • Block inserter sidebar with searchable block library.
    • Slash command menu (type /) with live filtering and mouse/keyboard selection.
    • Keyboard navigation for inserter and slash menu (Arrow keys, Enter, Escape).
  • Bug Fixes

    • Undo/redo now restores selection state correctly alongside content changes.
  • Style

    • New layout and styling for inserter panel and slash-command popover.
  • Tests

    • Added comprehensive tests for inserter, slash command, insert/replace behaviors, and history.

Adds the first two ways to insert new blocks into the canvas: a sidebar
panel with a searchable block list, and an in-editor slash command that
replaces the current paragraph with a chosen block type.

- New `editor/inserter/` module: cached-snapshot registry, built-in
  seeds for paragraph/heading, block factories, API loader with silent
  fallback (for #274), `insertBlockAtSelection`, and an atomic
  `replaceBlockWithInserterBlock` that commits replacement in a single
  undo step.
- `InserterPanel` renders a searchable, click-to-insert list wired into
  the editor shell next to the canvas.
- `SlashCommandPopover` renders the slash menu; `ParagraphEdit`
  detects `/query` in the block's plain text, filters the list,
  handles ArrowUp/ArrowDown/Escape via a window listener, routes Enter
  through Tiptap's keymap to commit the replacement, and suppresses
  the backspace-merge path while the slash popover is open.
- History snapshots now capture `{ blocks, selection }` so undo/redo
  restore the caret to the block that was being edited at the time of
  the change. This fixes the focus-loss after undoing a slash command
  replacement — without it, the replaced block's clientId was gone
  and nothing was selected, leaving the editor unfocused.
- Boot sequence calls `loadInserterBlocks({ apiBase })`.

Closes #273
@coderabbitai

coderabbitai Bot commented Apr 14, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3a90c430-a069-4bfd-aa1b-ca0c33fd4085

📥 Commits

Reviewing files that changed from the base of the PR and between a4b2044 and 01be5a9.

📒 Files selected for processing (8)
  • resources/js/visual-editor/editor/blocks/paragraph/edit.tsx
  • resources/js/visual-editor/editor/components/EditorShell.tsx
  • resources/js/visual-editor/editor/components/SlashCommandPopover.tsx
  • resources/js/visual-editor/editor/components/editor.css
  • resources/js/visual-editor/editor/inserter/__tests__/insertBlockActions.test.ts
  • resources/js/visual-editor/editor/inserter/__tests__/inserterRegistry.test.ts
  • resources/js/visual-editor/editor/inserter/insertBlockActions.ts
  • resources/js/visual-editor/editor/inserter/loadInserterBlocks.ts
✅ Files skipped from review due to trivial changes (3)
  • resources/js/visual-editor/editor/inserter/tests/insertBlockActions.test.ts
  • resources/js/visual-editor/editor/inserter/loadInserterBlocks.ts
  • resources/js/visual-editor/editor/inserter/tests/inserterRegistry.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • resources/js/visual-editor/editor/components/EditorShell.tsx
  • resources/js/visual-editor/editor/components/SlashCommandPopover.tsx
  • resources/js/visual-editor/editor/components/editor.css
  • resources/js/visual-editor/editor/blocks/paragraph/edit.tsx
  • resources/js/visual-editor/editor/inserter/insertBlockActions.ts

📝 Walkthrough

Walkthrough

Adds a block inserter system: a searchable sidebar InserterPanel, an in-editor SlashCommandPopover, a lightweight inserter registry (with builtin paragraph/heading and optional API loading), insertion/replace actions, paragraph block slash integration, and history snapshots updated to include selection for undo/redo.

Changes

Cohort / File(s) Summary
Sidebar Inserter
resources/js/visual-editor/editor/components/InserterPanel.tsx, resources/js/visual-editor/editor/components/__tests__/InserterPanel.test.tsx, resources/js/visual-editor/editor/components/index.ts
New InserterPanel component with searchable list wired to the inserter registry; tests for rendering, filtering, empty state, and insert behavior; re-export added.
Slash Command UI & Paragraph Integration
resources/js/visual-editor/editor/components/SlashCommandPopover.tsx, resources/js/visual-editor/editor/blocks/paragraph/edit.tsx, resources/js/visual-editor/editor/blocks/__tests__/slashCommand.test.tsx
New SlashCommandPopover component and paragraph editor changes to detect /, maintain query/index, handle Arrow/Enter/Escape, render popover with filtered blocks; tests for open/filter/nav/select/dismiss flows.
Inserter Registry & Utilities
resources/js/visual-editor/editor/inserter/inserterRegistry.ts, resources/js/visual-editor/editor/inserter/filterInserterBlocks.ts, resources/js/visual-editor/editor/inserter/builtinInserterBlocks.ts, resources/js/visual-editor/editor/inserter/index.ts
New lightweight inserter registry with snapshot caching, subscription API, block factory registry, filter helper, builtin registration for paragraph/heading, and barrel exports.
Block Insertion Actions & Loading
resources/js/visual-editor/editor/inserter/insertBlockActions.ts, resources/js/visual-editor/editor/inserter/loadInserterBlocks.ts, resources/js/visual-editor/editor/inserter/__tests__/insertBlockActions.test.ts, resources/js/visual-editor/editor/inserter/__tests__/inserterRegistry.test.ts
Actions to insert or replace blocks at selection, loader to seed builtins and optionally fetch API blocks (sanitizes payloads), and tests covering insertion semantics and registry behavior.
Editor Shell & Styling
resources/js/visual-editor/editor/components/EditorShell.tsx, resources/js/visual-editor/editor/components/editor.css
EditorShell layout updated to include a body wrapper and InserterPanel sibling; undo/redo keydown early-return for text inputs; CSS for inserter sidebar and slash popover added.
Editor Boot & Exports
resources/js/visual-editor/editor/main.tsx
Calls loadInserterBlocks({ apiBase }) during startup to seed the inserter registry before store creation.
History / Store Changes
resources/js/visual-editor/editor/store/editorStore.ts, resources/js/visual-editor/editor/store/types.ts, resources/js/visual-editor/editor/store/__tests__/editorStoreHistory.test.ts
History snapshot shape changed to { blocks, selection }; commit/undo/redo updated to store and restore selection alongside blocks; tests added for selection capture/restore.
Tests added
resources/js/visual-editor/editor/blocks/__tests__/slashCommand.test.tsx, resources/js/visual-editor/editor/components/__tests__/InserterPanel.test.tsx, resources/js/visual-editor/editor/inserter/__tests__/*, resources/js/visual-editor/editor/store/__tests__/editorStoreHistory.test.ts
Multiple Vitest suites added to cover inserter registry, filtering, loading, insert/replace actions, sidebar behavior, slash-command interactions, and history/selection behavior.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ParagraphEditor as Paragraph<br/>Editor
    participant Popover as SlashCommand<br/>Popover
    participant Store as Editor<br/>Store
    participant Factory as Block<br/>Factory

    User->>ParagraphEditor: Type "/"
    ParagraphEditor->>Store: Read inserter blocks (subscribe/get)
    Store-->>ParagraphEditor: Provide filtered blocks
    ParagraphEditor->>Popover: Render popover with blocks
    Popover-->>User: Display menu

    User->>Popover: ArrowDown / ArrowUp
    Popover->>Popover: Update selectedIndex
    User->>Popover: Press Enter
    Popover->>Factory: Request block factory for selection
    Factory-->>Store: New block instance created
    Store->>ParagraphEditor: Replace paragraph with new block
    ParagraphEditor-->>User: Popover closed, block inserted
Loading
sequenceDiagram
    actor User
    participant UI as InserterPanel
    participant Registry as Inserter<br/>Registry
    participant Store as Editor<br/>Store
    participant Factory as Block<br/>Factory

    UI->>Registry: Subscribe / get inserter blocks
    Registry-->>UI: Block list
    User->>UI: Type query in search
    UI->>Registry: Filter locally (filterInserterBlocks)
    UI-->>User: Show filtered list
    User->>UI: Click block
    UI->>Factory: Create block via factory
    Factory-->>Store: Insert new block at selection
    Store-->>User: Editor updated with new block
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped in and typed a tiny slash,

Blocks appeared in a joyful flash.
Sidebar hums with paragraph and head,
History keeps each hop ahead.
A rabbit cheers — insert, replace, and dash!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.15% 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 pull request title 'feat: block inserter — sidebar panel + slash command' directly and clearly summarizes the main changes: adding a block inserter with two key features.
Linked Issues check ✅ Passed All objectives from #273 are met: registry fetching with fallback, InserterPanel sidebar with search and click-to-insert, slash command popover with keyboard navigation, and comprehensive Vitest test coverage for both paths.
Out of Scope Changes check ✅ Passed All changes align with PR objectives. No out-of-scope features like drag-to-insert, block categories, or recently-used blocks were added; history snapshot enhancements support the feature requirements.

✏️ 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/273-block-inserter-sidebar-slash-command

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 14, 2026
@ViewFromTheBox ViewFromTheBox linked an issue Apr 14, 2026 that may be closed by this pull request
@ViewFromTheBox
ViewFromTheBox marked this pull request as ready for review April 14, 2026 18:43

@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: 7

🧹 Nitpick comments (1)
resources/js/visual-editor/editor/inserter/__tests__/inserterRegistry.test.ts (1)

39-48: Strengthen unsubscribe behavior assertion.

You unsubscribe on Line 47, but the test does not verify listener silence afterward. Add a second mutation and assert call count is unchanged.

Suggested test tightening
 it('notifies subscribers when the registry changes', () => {
     const listener = vi.fn();
     const unsubscribe = subscribeInserterBlocks(listener);

     registerBuiltinInserterBlocks();

-    expect(listener).toHaveBeenCalled();
+    expect(listener).toHaveBeenCalledTimes(1);

     unsubscribe();
+    registerBuiltinInserterBlocks();
+    expect(listener).toHaveBeenCalledTimes(1);
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@resources/js/visual-editor/editor/inserter/__tests__/inserterRegistry.test.ts`
around lines 39 - 48, After unsubscribing, the test should assert the listener
is no longer called: keep the existing subscribeInserterBlocks(listener) and
registerBuiltinInserterBlocks() call to trigger the first notification, then
call unsubscribe() and perform another registry mutation (e.g., call
registerBuiltinInserterBlocks() again or any other registry-changing function)
and assert the listener's call count did not increase (use toHaveBeenCalledTimes
or equivalent) to confirm unsubscribe removed the listener.
🤖 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/blocks/paragraph/edit.tsx`:
- Around line 63-76: The slash menu is being activated from persisted content
because slashQuery (seeded via detectSlashQuery) stays set even when the editor
is not focused, and global key handlers (the window listener referenced
alongside filteredSlashBlocks) remain active; update the logic so the menu only
opens and global keys are handled when the editor has active focus/selection or
clear slashQuery on blur. Concretely, in the paragraph edit component ensure you
track editor focus/selection (or use the existing selection/focus API) and gate
usage of slashQuery/filteredSlashBlocks and registration of the global keydown
listener on that focused state, or add a blur handler that calls
setSlashQuery(null) and unregisters the window listener; reference the
detectSlashQuery, slashQuery, setSlashQuery, filteredSlashBlocks, and the code
that subscribes/registers the window key listener to implement this change.

In `@resources/js/visual-editor/editor/components/editor.css`:
- Around line 211-219: The inserter lists can grow beyond the viewport because
.ve-inserter-panel uses max-height: 100% which is ineffective without a sized
parent and the slash popover has no vertical bounds; update .ve-inserter-panel
and the slash popover selector (the component that renders the “/” menu) to
include a viewport-aware max-height (e.g. max-height: calc(100vh - 120px) or a
sensible vh value) and overflow-y: auto (and box-sizing: border-box if needed)
so both menus get a real scrollable vertical bound and items remain reachable by
mouse and keyboard.

In `@resources/js/visual-editor/editor/components/EditorShell.tsx`:
- Around line 28-33: Global undo/redo keydown handler in EditorShell is
capturing Ctrl/Cmd+Z/Y even when focus is inside InserterPanel's search input;
update the window-level keydown handler (the global shortcut handler in
EditorShell) to early-return if the event target is an editable element or
inside the inserter (check event.target via
HTMLInputElement/HTMLTextAreaElement, element.isContentEditable, or
element.closest('.ve-inserter-panel' or the InserterPanel root/class)), so
native undo/redo works in the search field and only fall back to editor history
shortcuts when focus is outside editable inputs.

In `@resources/js/visual-editor/editor/components/SlashCommandPopover.tsx`:
- Around line 17-23: SlashCommandPopover currently renders role="listbox" and
role="option" but never receives focus nor exposes an aria-activedescendant
linked to the focused editor; either wire it properly or drop listbox semantics.
To fix: in the SlashCommandPopover component make the popover container
focusable (tabIndex={0}) and focus it when the popover opens, maintain an
activeOptionId state and set aria-activedescendant={activeOptionId} on the
container, ensure each option (role="option") has a stable id (e.g.
`option-${item.id}`), and update activeOptionId on ArrowUp/ArrowDown keyboard
handlers so screen readers see changes; alternatively, if you cannot manage
focus/active id, remove role="listbox" and role="option" and use
role="presentation" or no role.

In `@resources/js/visual-editor/editor/inserter/insertBlockActions.ts`:
- Around line 29-35: The helper always inserts after the selected block (using
state.selection.clientId → currentIndex → insertIndex = currentIndex + 1) and
ignores the caret edge; update the logic to read state.selection.edge and
compute insertIndex accordingly: if edge === 'start' insertIndex should be
currentIndex (i.e., before the block), otherwise insertIndex should be
currentIndex + 1 (after); also handle a missing/undefined edge by defaulting to
'end' so existing behavior is preserved.

In `@resources/js/visual-editor/editor/inserter/loadInserterBlocks.ts`:
- Around line 39-45: The code currently calls registerInserterBlock(block) for
every API-returned block, which exposes blocks that lack a runtime factory and
thus are non-insertable by insertBlockAtSelection and
replaceBlockWithInserterBlock; update loadInserterBlocks (the loop handling
data.blocks) to only register blocks that have an associated factory (use
getBlockFactory(block.name) to check) or alternatively derive and register a
minimal factory from the block payload before calling registerInserterBlock;
ensure you reference registerInserterBlock, getBlockFactory,
insertBlockAtSelection and replaceBlockWithInserterBlock when implementing the
conditional registration or factory derivation so API-only entries are not
surfaced until insertable.
- Around line 39-45: filterInserterBlocks()/loadInserterBlocks currently assumes
optional fields are strings/arrays and lowercases them blindly, which throws on
malformed payloads; before calling registerInserterBlock (or inside
filterInserterBlocks) validate and sanitize InserterBlock fields: ensure
description is a string before calling .toLowerCase() (otherwise set it to
undefined or ''), ensure keywords is an array and replace it with
keywords.filter(k => typeof k === 'string').map(k => k.toLowerCase()), and
ignore/omit any non-string keyword entries; apply these checks to the block
objects created from data.blocks so registerInserterBlock receives only safe,
normalized fields.

---

Nitpick comments:
In
`@resources/js/visual-editor/editor/inserter/__tests__/inserterRegistry.test.ts`:
- Around line 39-48: After unsubscribing, the test should assert the listener is
no longer called: keep the existing subscribeInserterBlocks(listener) and
registerBuiltinInserterBlocks() call to trigger the first notification, then
call unsubscribe() and perform another registry mutation (e.g., call
registerBuiltinInserterBlocks() again or any other registry-changing function)
and assert the listener's call count did not increase (use toHaveBeenCalledTimes
or equivalent) to confirm unsubscribe removed the listener.
🪄 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: c4f2d5a2-9bee-40c0-8c02-a07fce0f593a

📥 Commits

Reviewing files that changed from the base of the PR and between 5590b80 and a4b2044.

📒 Files selected for processing (20)
  • resources/js/visual-editor/editor/blocks/__tests__/slashCommand.test.tsx
  • resources/js/visual-editor/editor/blocks/paragraph/edit.tsx
  • resources/js/visual-editor/editor/components/EditorShell.tsx
  • resources/js/visual-editor/editor/components/InserterPanel.tsx
  • resources/js/visual-editor/editor/components/SlashCommandPopover.tsx
  • resources/js/visual-editor/editor/components/__tests__/InserterPanel.test.tsx
  • resources/js/visual-editor/editor/components/editor.css
  • resources/js/visual-editor/editor/components/index.ts
  • resources/js/visual-editor/editor/inserter/__tests__/insertBlockActions.test.ts
  • resources/js/visual-editor/editor/inserter/__tests__/inserterRegistry.test.ts
  • resources/js/visual-editor/editor/inserter/builtinInserterBlocks.ts
  • resources/js/visual-editor/editor/inserter/filterInserterBlocks.ts
  • resources/js/visual-editor/editor/inserter/index.ts
  • resources/js/visual-editor/editor/inserter/insertBlockActions.ts
  • resources/js/visual-editor/editor/inserter/inserterRegistry.ts
  • resources/js/visual-editor/editor/inserter/loadInserterBlocks.ts
  • resources/js/visual-editor/editor/main.tsx
  • resources/js/visual-editor/editor/store/__tests__/editorStoreHistory.test.ts
  • resources/js/visual-editor/editor/store/editorStore.ts
  • resources/js/visual-editor/editor/store/types.ts

Comment thread resources/js/visual-editor/editor/blocks/paragraph/edit.tsx Outdated
Comment thread resources/js/visual-editor/editor/components/editor.css
Comment on lines +28 to +33
<div className="ve-editor-shell__body">
<InserterPanel />
<Canvas>
<BlockList />
</Canvas>
</div>

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

Global undo/redo shortcuts will hijack InserterPanel search input editing.

Line 29 introduces a text input path, but the window-level handler still captures Ctrl/Cmd+Z/Y unconditionally. This prevents native undo/redo in the search field and triggers editor history operations instead.

Proposed fix
 function KeyboardBindings() {
     const store = useEditorStore();

     useEffect(() => {
+        function isFormFieldTarget(target: EventTarget | null): boolean {
+            if (!(target instanceof HTMLElement)) {
+                return false;
+            }
+            const tag = target.tagName;
+            return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
+        }
+
         function handleKeyDown(event: KeyboardEvent) {
+            if (isFormFieldTarget(event.target)) {
+                return;
+            }
             if (!isUndoRedoKey(event)) {
                 return;
             }

             const isRedo = event.shiftKey || event.key.toLowerCase() === 'y';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/visual-editor/editor/components/EditorShell.tsx` around lines 28
- 33, Global undo/redo keydown handler in EditorShell is capturing Ctrl/Cmd+Z/Y
even when focus is inside InserterPanel's search input; update the window-level
keydown handler (the global shortcut handler in EditorShell) to early-return if
the event target is an editable element or inside the inserter (check
event.target via HTMLInputElement/HTMLTextAreaElement,
element.isContentEditable, or element.closest('.ve-inserter-panel' or the
InserterPanel root/class)), so native undo/redo works in the search field and
only fall back to editor history shortcuts when focus is outside editable
inputs.

Comment thread resources/js/visual-editor/editor/inserter/insertBlockActions.ts Outdated
Comment thread resources/js/visual-editor/editor/inserter/loadInserterBlocks.ts Outdated
- Paragraph edit: stop seeding slash query from persisted content; clear
  on editor blur so the popover never activates outside an active edit.
- Inserter panel + slash popover: add viewport-aware max-height and
  overflow-y so long lists stay reachable via mouse and keyboard.
- Editor shell: defer Cmd/Ctrl+Z and Cmd/Ctrl+Y to native browser
  history when focus is inside an <input> or <textarea> so the sidebar
  search field keeps its own undo stack.
- SlashCommandPopover: drop the listbox/option ARIA roles — focus stays
  in the Tiptap contenteditable, so the roles were unanchored.
- insertBlockAtSelection: honor `selection.edge`; insert before the
  selected block when the caret is at its start, after when at end.
- loadInserterBlocks: skip API-returned blocks that have no client
  factory (otherwise they render as dead list items) and sanitize the
  payload so malformed description/keywords fields don't throw.
@ViewFromTheBox
ViewFromTheBox merged commit c9969c4 into add/phase-one Apr 14, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from In Review to Done in ArtisanPack UI Overview Apr 14, 2026
@ViewFromTheBox
ViewFromTheBox deleted the feature/273-block-inserter-sidebar-slash-command branch April 14, 2026 19:04
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.

Phase 1.11: Block inserter — sidebar panel + slash command

1 participant