Skip to content

Conversation

aaryan610
Copy link
Member

@aaryan610 aaryan610 commented Jul 31, 2025

Description

This PR upgrades Tiptap to v3

Type of Change

  • Improvement (change that would cause existing functionality to not work as expected)
  • Code refactoring

Summary by CodeRabbit

  • New Features

    • Editor upgraded to Tiptap v3 with refreshed extensions and typings.
  • Bug Fixes

    • Prevented upload/insert errors by guarding undefined positions.
    • Improved code-block rendering semantics.
    • Removed legacy inline table toolbox to stabilize table behavior.
  • Refactor

    • Menus and popups repositioned using a new positioning system for more reliable placement.
    • Storage access streamlined for more consistent editor state.
  • Chores

    • Removed deprecated extensions and updated dependencies.

Copy link
Contributor

coderabbitai bot commented Jul 31, 2025

Walkthrough

Tiptap packages upgraded v2 → v3 and the getExtensionStorage helper was removed; codebase migrated to direct editor.storage access, multiple extensions and menus refactored (tippy → Floating UI), table toolbox/UI removed, and TypeScript module augmentations for extension storage were added.

Changes

Cohort / File(s) Change Summary
Tiptap Dependency Upgrades
apps/live/package.json, packages/editor/package.json
Bumped @tiptap packages from v2 → v3, added @tiptap/extensions/@tiptap/y-tiptap, replaced many v2 extensions with v3 equivalents and removed deprecated ones.
Removed Helper
packages/editor/src/core/helpers/get-extension-storage.ts
Deleted generic getExtensionStorage helper; callers migrated to editor.storage.<extension>.
Direct Storage Access / Storage Typings
packages/editor/src/core/.../editor-ref.ts, .../hooks/use-editor.ts, .../extensions/*/extension-config.ts, .../utility.ts, .../image/extension.tsx, .../headings-list.ts, packages/editor/src/ce/types/storage.ts
Replaced getExtensionStorage(...) with direct editor.storage reads; added/updated module augmentations to extend @tiptap/core Storage for multiple extensions; removed ExtensionStorageMap type.
Custom Image refactor
packages/editor/src/core/extensions/custom-image/... (components/*, uploader.tsx, utils.ts, extension-config.ts)
Switched storage reads to editor.storage.imageComponent / editor.storage.utility, added guards for undefined getPos, simplified node-view props and maxFileSize access.
Menus & Suggestions: tippy → Floating UI
packages/editor/src/core/extensions/mentions/utils.ts, .../slash-commands/root.tsx, .../components/menus/block-menu.tsx, .../components/menus/ai-menu.tsx, .../components/menus/bubble-menu/root.tsx, .../helpers/floating-ui.ts
Replaced tippy-based popups with Floating UI positioning and DOM-mounted renderers (FloatingPortal, virtual refs, computePosition); added updateFloatingUIFloaterPosition helper; adjusted lifecycle hooks and render flows.
Table UI Simplification & Styles
packages/editor/src/core/extensions/table/table/table-view.tsx, packages/editor/src/core/extensions/table/table/table.ts, packages/editor/src/styles/table.css
Removed interactive table toolbox and control UI code and CSS; simplified TableView API surface and types; retained column sizing/rendering.
Editor Hooks & File Upload changes
packages/editor/src/core/hooks/use-file-upload.ts, .../custom-image/components/upload-status.tsx, .../custom-image/components/block.tsx, .../custom-image/components/node-view.tsx
Changed drop/upload APIs to accept getPos functions (NodeViewProps["getPos"]); added pos guards; replaced helper usage with editor.storage access.
Misc. extension & node-view tweaks
packages/editor/src/core/extensions/code/code-block-lowlight.ts, .../code/code-block-node-view.tsx, .../core-without-props.ts, .../extensions.ts, .../mentions/extension-config.ts, .../enter-key.ts, .../extensions/starter-kit.ts, .../plugins/markdown-clipboard.ts
Added nullish fallback for parent options, changed NodeViewContent codediv, adjusted imports (default→named), removed underline extension, removed mention default class, changed StarterKit options, added // @ts-expect-error for markdown serializer.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant Editor
    participant Suggestion
    participant Storage as Editor.storage
    participant FloatingUI

    User->>Editor: trigger suggestion/menu (mention/slash/block)
    Editor->>Suggestion: Suggestion.onStart/onUpdate
    Suggestion->>Storage: read extension data via editor.storage.<ext>
    Suggestion->>FloatingUI: computePosition(posToDOMRect(selection))
    FloatingUI-->>Suggestion: position coords
    Suggestion->>Editor: mount menu (FloatingPortal / DOM)
    User->>Suggestion: interact (navigate/select/close)
    Suggestion->>Editor: execute command (insert/close)
    Editor->>Storage: update storage state (e.g., activeDropbarExtensions)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Palanikannan1437
  • VipinDevelops
  • sriramveeraghanta

Poem

I hopped through code with tiny paws,
Replaced helpers, tuned storage laws.
Floating menus find their place,
Tables trimmed with cleaner grace.
Rabbit cheers — editor leveled up! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/tiptap-v3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

makeplane bot commented Jul 31, 2025

Pull Request Linked with Plane Work Items

Comment Automatically Generated by Plane

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/editor/src/core/plugins/markdown-clipboard.ts (1)

12-12: Appropriate temporary fix for outdated type definitions.

The @ts-expect-error directive with descriptive comment is a reasonable temporary solution for handling outdated tiptap-markdown type definitions during the v3 upgrade.

Consider creating a follow-up task to remove this suppression once the tiptap-markdown package is updated with compatible type definitions for Tiptap v3.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc49a2c and 1592b59.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (31)
  • apps/live/package.json (1 hunks)
  • packages/editor/package.json (1 hunks)
  • packages/editor/src/ce/types/storage.ts (0 hunks)
  • packages/editor/src/core/components/menus/bubble-menu/root.tsx (9 hunks)
  • packages/editor/src/core/extensions/code/code-block-lowlight.ts (1 hunks)
  • packages/editor/src/core/extensions/code/code-block-node-view.tsx (1 hunks)
  • packages/editor/src/core/extensions/core-without-props.ts (1 hunks)
  • packages/editor/src/core/extensions/custom-image/components/block.tsx (1 hunks)
  • packages/editor/src/core/extensions/custom-image/components/node-view.tsx (2 hunks)
  • packages/editor/src/core/extensions/custom-image/components/upload-status.tsx (1 hunks)
  • packages/editor/src/core/extensions/custom-image/components/uploader.tsx (4 hunks)
  • packages/editor/src/core/extensions/custom-image/extension-config.ts (2 hunks)
  • packages/editor/src/core/extensions/custom-image/utils.ts (1 hunks)
  • packages/editor/src/core/extensions/custom-link/extension.tsx (1 hunks)
  • packages/editor/src/core/extensions/emoji/suggestion.ts (3 hunks)
  • packages/editor/src/core/extensions/enter-key.ts (1 hunks)
  • packages/editor/src/core/extensions/extensions.ts (1 hunks)
  • packages/editor/src/core/extensions/headings-list.ts (2 hunks)
  • packages/editor/src/core/extensions/image/extension.tsx (2 hunks)
  • packages/editor/src/core/extensions/mentions/extension-config.ts (0 hunks)
  • packages/editor/src/core/extensions/mentions/utils.ts (2 hunks)
  • packages/editor/src/core/extensions/placeholder.ts (2 hunks)
  • packages/editor/src/core/extensions/starter-kit.ts (1 hunks)
  • packages/editor/src/core/extensions/table/table/table-view.tsx (6 hunks)
  • packages/editor/src/core/extensions/table/table/table.ts (1 hunks)
  • packages/editor/src/core/extensions/utility.ts (2 hunks)
  • packages/editor/src/core/helpers/editor-ref.ts (4 hunks)
  • packages/editor/src/core/helpers/get-extension-storage.ts (0 hunks)
  • packages/editor/src/core/hooks/use-editor.ts (3 hunks)
  • packages/editor/src/core/hooks/use-file-upload.ts (3 hunks)
  • packages/editor/src/core/plugins/markdown-clipboard.ts (1 hunks)
💤 Files with no reviewable changes (3)
  • packages/editor/src/core/extensions/mentions/extension-config.ts
  • packages/editor/src/core/helpers/get-extension-storage.ts
  • packages/editor/src/ce/types/storage.ts
🧰 Additional context used
🧠 Learnings (11)
packages/editor/src/core/extensions/core-without-props.ts (2)

Learnt from: lifeiscontent
PR: #7164
File: packages/ui/.storybook/main.ts:24-47
Timestamp: 2025-06-04T16:22:44.344Z
Learning: In packages/ui/.storybook/main.ts, the webpackFinal function intentionally overrides the CSS loader strategy by finding and replacing existing CSS rules. This is a temporary workaround for a known upstream issue in Storybook's CSS handling that has been communicated to the Storybook maintainers. The current implementation should not be changed until the upstream issue is resolved.

Learnt from: mathalav55
PR: #6107
File: web/ce/components/workspace-notifications/sidebar/notification-card/options/archive.tsx:11-14
Timestamp: 2024-11-28T07:02:54.664Z
Learning: When components are still located in core, it's appropriate for files to import them using @/components/..., and the migration to the new import paths is not necessary in such cases.

packages/editor/src/core/extensions/extensions.ts (1)

Learnt from: lifeiscontent
PR: #7164
File: packages/ui/.storybook/main.ts:24-47
Timestamp: 2025-06-04T16:22:44.344Z
Learning: In packages/ui/.storybook/main.ts, the webpackFinal function intentionally overrides the CSS loader strategy by finding and replacing existing CSS rules. This is a temporary workaround for a known upstream issue in Storybook's CSS handling that has been communicated to the Storybook maintainers. The current implementation should not be changed until the upstream issue is resolved.

packages/editor/src/core/extensions/custom-image/components/block.tsx (1)

Learnt from: SatishGandham
PR: #5864
File: packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts:60-60
Timestamp: 2024-10-22T08:03:04.373Z
Learning: In packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts, the getImageSource command returns a string directly, not a function.

packages/editor/src/core/extensions/custom-image/utils.ts (2)

Learnt from: SatishGandham
PR: #5864
File: packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts:60-60
Timestamp: 2024-10-22T08:03:04.373Z
Learning: In packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts, the getImageSource command returns a string directly, not a function.

Learnt from: mathalav55
PR: #6107
File: web/ce/components/workspace-notifications/sidebar/notification-card/options/archive.tsx:11-14
Timestamp: 2024-11-28T07:02:54.664Z
Learning: When components are still located in core, it's appropriate for files to import them using @/components/..., and the migration to the new import paths is not necessary in such cases.

packages/editor/src/core/extensions/custom-image/components/upload-status.tsx (2)

Learnt from: SatishGandham
PR: #5864
File: packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts:60-60
Timestamp: 2024-10-22T08:03:04.373Z
Learning: In packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts, the getImageSource command returns a string directly, not a function.

Learnt from: mathalav55
PR: #6107
File: web/ce/components/workspace-notifications/sidebar/notification-card/options/archive.tsx:11-14
Timestamp: 2024-11-28T07:02:54.664Z
Learning: When components are still located in core, it's appropriate for files to import them using @/components/..., and the migration to the new import paths is not necessary in such cases.

packages/editor/src/core/extensions/custom-image/extension-config.ts (1)

Learnt from: SatishGandham
PR: #5864
File: packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts:60-60
Timestamp: 2024-10-22T08:03:04.373Z
Learning: In packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts, the getImageSource command returns a string directly, not a function.

packages/editor/src/core/extensions/custom-image/components/uploader.tsx (1)

Learnt from: SatishGandham
PR: #5864
File: packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts:60-60
Timestamp: 2024-10-22T08:03:04.373Z
Learning: In packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts, the getImageSource command returns a string directly, not a function.

packages/editor/src/core/extensions/utility.ts (1)

Learnt from: SatishGandham
PR: #5864
File: packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts:60-60
Timestamp: 2024-10-22T08:03:04.373Z
Learning: In packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts, the getImageSource command returns a string directly, not a function.

packages/editor/src/core/extensions/image/extension.tsx (1)

Learnt from: SatishGandham
PR: #5864
File: packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts:60-60
Timestamp: 2024-10-22T08:03:04.373Z
Learning: In packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts, the getImageSource command returns a string directly, not a function.

packages/editor/src/core/extensions/custom-image/components/node-view.tsx (1)

Learnt from: SatishGandham
PR: #5864
File: packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts:60-60
Timestamp: 2024-10-22T08:03:04.373Z
Learning: In packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts, the getImageSource command returns a string directly, not a function.

packages/editor/src/core/hooks/use-editor.ts (1)

Learnt from: SatishGandham
PR: #5864
File: packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts:60-60
Timestamp: 2024-10-22T08:03:04.373Z
Learning: In packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts, the getImageSource command returns a string directly, not a function.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build and lint web apps
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Build and lint web apps
  • GitHub Check: Build and lint web apps
🔇 Additional comments (54)
packages/editor/src/core/extensions/core-without-props.ts (1)

3-3: LGTM! Correct import for Tiptap v3.

The change from default import to named import for TextStyle is correct and aligns with Tiptap v3's API changes where extensions are now exported as named exports.

apps/live/package.json (1)

29-30: Verify Tiptap v3 API usage in the live app

We’ve identified direct imports of Tiptap APIs in your helper:

• apps/live/src/core/helpers/page.ts

  • import { getSchema } from "@tiptap/core"
  • import { generateHTML, generateJSON } from "@tiptap/html"

Since you’ve jumped from v2.x → v3.x, please:

  • Cross-check your calls to getSchema, generateHTML, and generateJSON against the official v3 migration guide to ensure no signature or behavior changes have been overlooked.
  • Double-verify that any extensions (none were detected by our scan) have also been updated or renamed per v3.
  • Run your editor tests and manually smoke-test the live editor to catch any runtime breakages.
packages/editor/src/core/extensions/custom-image/components/block.tsx (1)

192-192: Excellent defensive programming practice.

The guard clause prevents runtime errors when getPos() returns undefined, which can happen in certain edge cases. This improves the robustness of the image selection handling.

packages/editor/src/core/extensions/table/table/table.ts (1)

259-259: LGTM! Improved type inference in Tiptap v3.

The removal of explicit type assertions indicates that Tiptap v3 has better type definitions that allow the TableView constructor to properly infer parameter types without manual casting.

packages/editor/src/core/extensions/extensions.ts (2)

4-4: LGTM! Import updated correctly for Tiptap v3.

The TextStyle import has been correctly updated to use named import syntax from @tiptap/extension-text-style, which aligns with the Tiptap v3 API changes.


6-6: LGTM! Extension consolidated import for Tiptap v3.

The CharacterCount import has been correctly updated to use the consolidated @tiptap/extensions package, which is the new approach in Tiptap v3 where multiple extensions are bundled together.

packages/editor/src/core/extensions/starter-kit.ts (1)

44-44: LGTM! StarterKit configuration updated for Tiptap v3.

The configuration property has been correctly updated from history: false to undoRedo: false, which reflects the API changes in Tiptap v3's StarterKit extension.

packages/editor/src/core/extensions/code/code-block-node-view.tsx (1)

59-59: LGTM! Semantic HTML element updated for better rendering.

The change from code to div element maintains the same functionality with the whitespace-pre-wrap class while potentially improving rendering behavior. The semantic meaning is preserved since it's already wrapped in a pre element.

packages/editor/src/core/extensions/custom-image/utils.ts (1)

15-15: LGTM! Simplified extension storage access.

The function has been correctly refactored to use direct access to editor.storage.imageComponent?.fileMap instead of the getExtensionStorage helper, which simplifies the code and removes unnecessary indirection.

packages/editor/src/core/extensions/enter-key.ts (1)

12-12: LGTM! Extension storage access simplified.

The code has been correctly updated to use direct access to this.editor.storage.utility.activeDropbarExtensions instead of the getExtensionStorage helper, which aligns with the broader refactoring effort to simplify extension storage access patterns.

packages/editor/src/core/extensions/custom-image/components/upload-status.tsx (1)

19-19: LGTM! Correct implementation of direct storage access.

The change from using getExtensionStorage helper to direct editor.storage.utility?.assetsUploadStatus?.[nodeId] access is correct and aligns with the Tiptap v3 upgrade pattern. The optional chaining ensures safe property access.

packages/editor/src/core/extensions/custom-image/extension-config.ts (2)

6-11: LGTM! Proper import expansion for storage typing.

The expanded import correctly includes CustomImageExtensionStorage needed for the module augmentation.


20-22: LGTM! Correct module augmentation for type safety.

The Storage interface augmentation properly provides typed access to the custom image extension storage, enabling safe direct access via editor.storage[CORE_EXTENSIONS.CUSTOM_IMAGE].

packages/editor/src/core/extensions/placeholder.ts (2)

1-1: LGTM! Correct import update for Tiptap v3.

The import change from @tiptap/extension-placeholder to @tiptap/extensions aligns with Tiptap v3's consolidated package structure.


20-20: LGTM! Proper direct storage access implementation.

The change from using getExtensionStorage to direct editor.storage.utility?.uploadInProgress access is correct and follows the new storage access pattern consistently used throughout the codebase.

packages/editor/src/core/extensions/mentions/utils.ts (2)

32-32: LGTM! Correct direct storage access for adding extension.

The change to direct props.editor.storage.utility.activeDropbarExtensions.push(...) properly replaces the helper function while maintaining the same functionality.


68-71: LGTM! Proper direct storage access with destructuring.

The destructuring assignment and direct array manipulation correctly replaces the helper function usage while maintaining the same logic for removing the mention extension from activeDropbarExtensions.

packages/editor/src/core/extensions/image/extension.tsx (2)

2-3: LGTM! Necessary import for module augmentation.

The import of CORE_EXTENSIONS is correctly added to support the Storage interface augmentation.


12-16: LGTM! Proper Storage interface augmentation.

The module augmentation correctly extends the Storage interface with typed access to the image extension storage, enabling safe direct access via editor.storage[CORE_EXTENSIONS.IMAGE].

packages/editor/src/core/extensions/code/code-block-lowlight.ts (1)

14-19: LGTM! Improved defensive programming for parent options.

The addition of nullish coalescing fallback with explicit defaults is a good defensive programming practice, especially during major version upgrades where parent extension behavior might change.

packages/editor/package.json (1)

48-66: Confirmed: TipTap v3 extensions correctly consolidated

No remaining imports of @tiptap/extension-character-count or @tiptap/extension-placeholder were detected. Both CharacterCount and Placeholder are now imported from @tiptap/extensions and used in:

• packages/editor/src/core/extensions/placeholder.ts
• packages/editor/src/core/extensions/extensions.ts

No further changes needed.

packages/editor/src/core/extensions/headings-list.ts (1)

6-10: LGTM! Proper TypeScript module augmentation for storage typing.

The module augmentation correctly extends the Storage interface for type safety when accessing extension storage directly via editor.storage.

packages/editor/src/core/extensions/utility.ts (2)

18-18: LGTM! Improved type safety with constant-based key.

Using CORE_EXTENSIONS.UTILITY instead of a string literal improves type safety and ensures consistency across the codebase.


31-34: LGTM! Consistent storage interface augmentation.

The Storage interface augmentation follows the same pattern as other extensions and ensures proper typing for direct storage access.

packages/editor/src/core/extensions/custom-image/components/uploader.tsx (3)

91-91: LGTM! Direct storage access aligns with new pattern.

The change from getExtensionStorage helper to direct editor.storage.utility access is consistent with the broader refactoring across the codebase.


61-66: LGTM! Improved position handling with proper null checks.

Adding the pos !== undefined check prevents potential runtime errors when getPos() returns undefined, making the cursor manipulation more robust.


113-113: LGTM! Consistent position handling improvements.

The changes to use getPos function and add proper undefined checks improve error handling and prevent potential runtime issues when position is not available.

Also applies to: 143-150

packages/editor/src/core/extensions/emoji/suggestion.ts (2)

13-13: LGTM! Direct storage access improves type safety.

The change from getExtensionStorage to direct editor.storage.emoji access aligns with the Tiptap v3 upgrade and provides better type safety.


57-57: LGTM! Consistent storage access pattern.

The direct access to editor.storage.utility.activeDropbarExtensions follows the same pattern as other storage access changes and maintains the existing functionality for tracking active dropdown extensions.

Also applies to: 101-104

packages/editor/src/core/extensions/custom-image/components/node-view.tsx (2)

1-1: LGTM! Improved import specificity.

The change to a type-only import of NodeViewProps is more specific and follows TypeScript best practices for type-only imports.


80-80: LGTM! Direct storage access with safe property chaining.

The direct access to editor.storage.imageComponent?.maxFileSize follows the refactoring pattern and uses optional chaining for safety.

packages/editor/src/core/extensions/custom-link/extension.tsx (2)

53-56: LGTM! Proper module augmentation for storage typing.

The module augmentation correctly extends the Tiptap Storage interface, providing type safety for direct storage access patterns used throughout the codebase.


59-59: Enhancement: Added bubble menu state tracking.

The addition of isBubbleMenuOpen property enhances the custom link extension's state management capabilities, allowing better control over bubble menu visibility.

packages/editor/src/core/hooks/use-editor.ts (4)

3-3: LGTM! Proper module augmentation for markdown storage.

The addition of module augmentation for MarkdownStorage ensures type safety for the markdown extension's storage access throughout the codebase.

Also applies to: 13-17


87-87: LGTM! Direct storage access with improved readability.

The change to editor.storage.utility provides direct access to the upload progress status, eliminating the helper function indirection.


90-95: LGTM! Updated setContent API for Tiptap v3.

The change to use an options object with emitUpdate and parseOptions aligns with the Tiptap v3 API and improves code clarity over positional arguments.


119-119: LGTM! Safe storage access with optional chaining.

The direct access to editor?.storage.utility?.assetsList uses appropriate optional chaining and nullish coalescing for safety.

packages/editor/src/core/helpers/editor-ref.ts (5)

41-43: LGTM! Direct storage access for document metrics.

The changes to access character count directly from editor?.storage.characterCount follow the refactoring pattern and maintain proper null safety with the nullish coalescing operator.


45-45: LGTM! Simplified headings access.

Direct access to editor.storage.headingsList?.headings eliminates the helper function indirection while maintaining safety with optional chaining.


47-48: LGTM! Safe markdown access with fallback.

The direct access to markdown storage with a proper empty string fallback ensures consistent behavior when markdown content is not available.


55-60: LGTM! Consistent setContent API usage.

The update to use the options object format for setContent matches the pattern used in use-editor.ts and aligns with the Tiptap v3 API.


111-111: LGTM! Consistent direct storage access pattern.

All the storage access changes follow the same pattern of replacing getExtensionStorage with direct editor.storage property access, maintaining functionality while simplifying the codebase.

Also applies to: 127-129, 145-145

packages/editor/src/core/hooks/use-file-upload.ts (4)

1-1: LGTM: Proper type-only import for Tiptap v3.

The import correctly uses type-only syntax for Editor and adds NodeViewProps to support the new position handling pattern.


93-93: Good refactoring: Dynamic position handling.

Changing from static pos: number to dynamic getPos: NodeViewProps["getPos"] ensures the position is current at the time of the drop event, which is more robust than storing a potentially stale position.


104-104: Smart optimization: Position caching.

Caching the position result prevents multiple calls to getPos() within the same operation while ensuring we have the most current position.


130-130: Essential guard: Undefined position check.

The guard against pos === undefined prevents invalid operations when the position cannot be determined, improving runtime safety.

packages/editor/src/core/components/menus/bubble-menu/root.tsx (3)

1-2: LGTM: Proper import structure for Tiptap v3.

The separation of type and component imports with appropriate type-only syntax is correct for the v3 upgrade.


53-58: Good refactoring: Simplified component structure.

The props destructuring and type definition improve code readability and maintainability.


78-92: LGTM: Improved useEditorState usage.

The simplified selector pattern with destructured editor parameter is cleaner and more consistent with v3 patterns.

packages/editor/src/core/extensions/table/table/table-view.tsx (5)

1-7: Excellent type safety: Proper type-only imports.

The consistent use of type-only imports for Editor, NodeViewProps, Node, Decoration, NodeView, and tippy types improves bundle size and type clarity.


259-261: Good type refinements: Readonly decorations and proper getPos typing.

Making decorations readonly prevents accidental mutations, and using NodeViewProps["getPos"] ensures type consistency with the Tiptap v3 API.


472-481: Robust position handling in selectColumn.

The position caching and undefined guard follow the established pattern from other files, preventing runtime errors when position cannot be determined.


484-493: Consistent pattern in selectRow.

The same robust position handling pattern is applied consistently, improving code reliability and maintainability.


149-149: Proper placeholder typing.

The placeholder actions are correctly typed with _args: unknown to indicate they're intentionally unused, which is good TypeScript practice.

Also applies to: 177-177

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2dbfb54 and c97d40f.

📒 Files selected for processing (1)
  • packages/editor/src/core/extensions/mentions/utils.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build and lint web apps
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Analyze (python)
🔇 Additional comments (5)
packages/editor/src/core/extensions/mentions/utils.ts (5)

1-2: LGTM: Clean migration to floating-ui.

The import changes properly replace tippy.js with @floating-ui/dom for positioning, and the addition of posToDOMRect from @tiptap/core provides the necessary integration with Tiptap's selection system.


49-52: LGTM: Proper DOM element setup and positioning.

The DOM manipulation correctly sets up absolute positioning, appends to document body, and calls the positioning function. This replaces the previous tippy.js approach effectively.


59-72: LGTM: Event handling maintains functionality.

The keyboard event handling preserves the original logic while properly handling component destruction and method calls through the optional chaining pattern.


48-52: Direct storage access verified
The editor.storage.utility structure is defined and consistently used across multiple extensions (mentions, emoji, placeholder, enter-key, custom-image, etc.), and there are no remaining calls to the removed getExtensionStorage helper. No further action is needed.


73-76: Improve cleanup robustness.

The cleanup in onExit should handle cases where the element might not exist or already be removed from the DOM.

 onExit: () => {
-  component?.element.remove();
+  if (component?.element && component.element.parentNode) {
+    component.element.remove();
+  }
   component?.destroy();
 },

Likely an incorrect or invalid review comment.

Comment on lines 13 to 28
const updatePosition = (editor: Editor, element: HTMLElement) => {
const virtualElement = {
getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
};

computePosition(virtualElement, element, {
placement: "bottom-start",
strategy: "absolute",
middleware: [shift(), flip()],
}).then(({ x, y, strategy }) => {
element.style.width = "max-content";
element.style.position = strategy;
element.style.left = `${x}px`;
element.style.top = `${y}px`;
});
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for async positioning.

The updatePosition function correctly uses floating-ui for dynamic positioning, but the async computePosition call lacks error handling. If positioning fails, it could leave the dropdown in an incorrect position.

-  computePosition(virtualElement, element, {
-    placement: "bottom-start",
-    strategy: "absolute",
-    middleware: [shift(), flip()],
-  }).then(({ x, y, strategy }) => {
-    element.style.width = "max-content";
-    element.style.position = strategy;
-    element.style.left = `${x}px`;
-    element.style.top = `${y}px`;
-  });
+  computePosition(virtualElement, element, {
+    placement: "bottom-start",
+    strategy: "absolute",
+    middleware: [shift(), flip()],
+  }).then(({ x, y, strategy }) => {
+    element.style.width = "max-content";
+    element.style.position = strategy;
+    element.style.left = `${x}px`;
+    element.style.top = `${y}px`;
+  }).catch((error) => {
+    console.warn("Failed to position mentions dropdown:", error);
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const updatePosition = (editor: Editor, element: HTMLElement) => {
const virtualElement = {
getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
};
computePosition(virtualElement, element, {
placement: "bottom-start",
strategy: "absolute",
middleware: [shift(), flip()],
}).then(({ x, y, strategy }) => {
element.style.width = "max-content";
element.style.position = strategy;
element.style.left = `${x}px`;
element.style.top = `${y}px`;
});
};
const updatePosition = (editor: Editor, element: HTMLElement) => {
const virtualElement = {
getBoundingClientRect: () =>
posToDOMRect(
editor.view,
editor.state.selection.from,
editor.state.selection.to,
),
};
computePosition(virtualElement, element, {
placement: "bottom-start",
strategy: "absolute",
middleware: [shift(), flip()],
})
.then(({ x, y, strategy }) => {
element.style.width = "max-content";
element.style.position = strategy;
element.style.left = `${x}px`;
element.style.top = `${y}px`;
})
.catch((error) => {
console.warn("Failed to position mentions dropdown:", error);
});
};
🤖 Prompt for AI Agents
In packages/editor/src/core/extensions/mentions/utils.ts around lines 13 to 28,
the async call to computePosition lacks error handling, which may cause the
dropdown to remain incorrectly positioned if the promise rejects. Add a catch
block to the computePosition promise chain to handle potential errors
gracefully, such as logging the error or applying a fallback positioning
strategy, ensuring the UI remains stable even if positioning fails.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (3)
packages/editor/src/core/extensions/table/table/table-view.tsx (1)

83-83: Double-check and update updateColumnsOnResize call to match Tiptap v3 signature

The upstream @tiptap/pm/tables export for updateColumnsOnResize in v3 has changed: it now expects

updateColumnsOnResize(
  tr: Transaction,
  table: Node,
  start: number,
  mapping?: Mapping,
): Transaction

— i.e. it takes a ProseMirror Transaction, the table Node, the table’s start offset, and an optional Mapping, returning a new Transaction.

On line 83 of packages/editor/src/core/extensions/table/table/table-view.tsx, the code still calls:

updateColumnsOnResize(this.node, this.colgroup, this.table, this.cellMinWidth);

These arguments don’t match the new signature. You’ll need to:

  • Obtain or create a Transaction (state.tr or similar)
  • Pass the table Node and its document offset, not the HTML elements
  • Provide the optional Mapping if required
  • Dispatch the returned Transaction

For example, you might refactor to something like:

const { state, dispatch } = view;  
let tr = state.tr;  
tr = updateColumnsOnResize(  
  tr,  
  this.node,           // ProseMirror Node  
  tableStartPos,       // number: offset of `this.node` in the doc  
  state.doc.map        // or other Mapping if needed  
);  
dispatch(tr);  

Please adjust this call to align with the v3 API so column resizing continues to work correctly.

packages/editor/src/core/components/menus/ai-menu.tsx (1)

53-61: Fix: use closest('#ai-handle') to support clicks on nested elements within the handle

Using target.matches('#ai-handle') fails when clicking a child of the handle. Switch to closest and anchor the popup to the handle if present.

-      const target = e.target as HTMLElement;
-      if (target.matches("#ai-handle") || menuRef.current?.contains(e.target as Node)) {
+      const target = e.target as HTMLElement;
+      const handle = target.closest("#ai-handle") as HTMLElement | null;
+      if (handle || menuRef.current?.contains(target)) {
         e.preventDefault();
 
         if (!isPopupVisible) {
           popup.current?.setProps({
-            getReferenceClientRect: () => target.getBoundingClientRect(),
+            getReferenceClientRect: () => (handle ?? target).getBoundingClientRect(),
           });
           popup.current?.show();
           setIsPopupVisible(true);
         }
         return;
       }
packages/editor/src/core/components/menus/block-menu.tsx (1)

144-178: Update Tiptap v3 command usage to match current signatures

The insertContentAt and focus calls with options objects do not align with Tiptap v3’s current TypeScript definitions. You must remove the unsupported third-argument options and adjust the chain accordingly:

• In packages/editor/src/core/components/menus/block-menu.tsx at the duplicated-content flow (lines 144–178):

  • Change
    editor
      .chain()
      .insertContentAt(insertPos, contentToInsert, { updateSelection: true })
      .focus(Math.min(insertPos + 1, docSize), { scrollIntoView: false })
      .run();
  • To something like
    editor
      .chain()
      .insertContentAt(insertPos, contentToInsert)
      .focus(Math.min(insertPos + 1, docSize))
      .run();

• If you need to prevent scrolling, call the dedicated scrollIntoView chain command (or omit it; focus no longer accepts options).
• If you intend to move the selection, omit updateSelection (v3’s insertContentAt has no such option) and rely on the default behavior or explicitly call .setSelection(...) afterward.

Making these changes will satisfy the following v3 signatures:

  1. insertContentAt(position, content): Chainable<any>
  2. focus(position?: number | 'start' | 'end'): Chainable<any>
♻️ Duplicate comments (1)
packages/editor/src/core/extensions/headings-list.ts (1)

52-56: Fix TipTap v3 "update" event payload; remove unsupported fields and the misleading transaction.

  • TipTap v3’s "update" event expects { editor } only; transaction and appendedTransactions are not part of that payload.
  • newState.tr creates a brand-new empty transaction; passing it as “the transaction” is incorrect and can mislead listeners.

Apply this diff:

-        this.editor.emit("update", {
-          editor: this.editor,
-          transaction: newState.tr,
-          appendedTransactions: [],
-        });
+        this.editor.emit("update", { editor: this.editor });

If you truly need transaction details, emit a separate "transaction" event from a place where you have the actual transaction, e.g., use the transactions parameter of appendTransaction or subscribe to TipTap’s "transaction" event.

🧹 Nitpick comments (14)
packages/editor/src/core/extensions/table/table/table-view.tsx (5)

10-10: Avoid cloning decorations on each update; also consider dropping unused state

You changed decorations to a readonly array (good), but you still clone on update. The clone is unnecessary and adds churn on frequent updates.

  • Minimal change: assign directly.
  • Optional: If decorations aren’t used in this view anymore (looks unused), consider removing the field entirely and the constructor/update assignments. The selection-outline plugin already supplies decorations to the view layer without TableView needing to store them (see packages/editor/src/core/extensions/table/plugins/selection-outline/plugin.ts Lines 54-56).

Apply minimal fix:

-    this.decorations = [...decorations];
+    this.decorations = decorations;

Also applies to: 32-32, 69-69


12-12: getPos typing matches NodeViewProps in v3

Using NodeViewProps["getPos"] is the right move for Tiptap v3 and ProseMirror compatibility. If this property isn’t used inside TableView anymore, you can drop it later to reduce surface area, but keeping it is harmless.

Also applies to: 34-34


79-80: Streamline col creation

Minor readability/perf nit: avoid creating an intermediate array of sentinel values.

-      const cols = Array.from({ length: this.map.width }, () => 1).map(() => h("col"));
+      const cols = Array.from({ length: this.map.width }, () => h("col"));

19-19: Remove unused fields

controls and hoveredCell aren’t referenced anymore after dropping toolboxes/hover logic. Safe to remove to reduce mental overhead.

-  hoveredCell: ResolvedPos | null = null;
...
-  controls?: HTMLElement;

Also applies to: 13-13


86-88: Be intentional about ignoreMutation always returning true

Always returning true can mask DOM mutations that ProseMirror might want to react to. It’s likely fine here because contentDOM is used and you control outer structure, but please validate IME/composition and selection behaviors in tables post-upgrade.

Suggested narrower guard (if needed later):

ignoreMutation(mutation: MutationRecord | { type: "selection"; target: Element }) {
  // Ignore selection/style mutations; let PM handle childList inside contentDOM
  if ((mutation as any).type === "selection") return true;
  if (mutation.type === "attributes" && (mutation.target as Element).closest("colgroup")) return true;
  return false;
}
packages/editor/src/core/extensions/headings-list.ts (1)

52-56: Use a scoped event instead of re-emitting the global “update”

We ran a search for any editor.on('update', …) listeners in the repo and found none—so right now no internal duplication occurs—but since TipTap itself emits a global "update" after every transaction, re-emitting it here risks doubling notifications for any consumer that does listen. Instead, emit a namespaced event just for headings if you need a post-transaction hook.

• File: packages/editor/src/core/extensions/headings-list.ts
Lines: ~52–56
• Replace the global update emission with a scoped event:

- this.editor.emit("update", {
-   editor: this.editor,
-   transaction: newState.tr,
-   appendedTransactions: [],
- });
+ this.editor.emit("headings:update", {
+   editor: this.editor,
+   headings,          // your computed headings payload
+ });

• If you still want to react to overall state updates, rely on TipTap’s built-in "update" event instead of re-emitting it here.

packages/editor/src/core/components/menus/ai-menu.tsx (4)

27-31: Harden appendTo to avoid null return from querySelector

document.querySelector('.frame-renderer') can return null, which breaks tippy’s expectation. Provide a safe fallback to document.body.

-      appendTo: () => document.querySelector(".frame-renderer"),
+      appendTo: () => document.querySelector(".frame-renderer") ?? document.body,

71-80: Only close on Escape; avoid collapsing the menu on any key press

Attaching hidePopup directly to keydown closes the AI menu on every keystroke, which is jarring when typing. Restrict to Escape.

-    document.addEventListener("keydown", hidePopup);
+    const onKeyDown = (e: KeyboardEvent) => {
+      if (e.key === "Escape") hidePopup();
+    };
+    document.addEventListener("keydown", onKeyDown);
...
-      document.removeEventListener("keydown", hidePopup);
+      document.removeEventListener("keydown", onKeyDown);

88-93: Make the popup container focusable and accessible

onShown: () => menuRef.current?.focus() won’t focus a div without a tabIndex. Add basic dialog semantics.

-      <div ref={menuRef} className="z-10">
+      <div ref={menuRef} className="z-10" tabIndex={-1} role="dialog" aria-modal="true" aria-label="AI menu">

2-2: Consider migrating this menu to Floating UI for consistency

Most menus in this PR moved to Floating UI; keeping Tippy here diverges in patterns and increases maintenance. Consider refactoring to the same virtual-reference + useFloating approach used in block-menu.tsx for consistency and easier positioning fixes.

packages/editor/src/core/components/menus/block-menu.tsx (3)

73-96: Reduce duplicated global listeners; lean more on Floating UI interactions

You’re already using useDismiss(context) which handles outside clicks/Escape. The additional document-level click/contextmenu/keydown listeners partly duplicate that behavior and add global event noise.

Consider:

  • Rely on useDismiss for outside/Escape.
  • Keep only the click/contextmenu handler that detects #drag-handle, but convert it to a delegated handler on the editor container instead of document.
  • Drop the extra keydown handler (Escape) in favor of useDismiss.

137-141: Guard selection.content() usage and avoid redundant checks

Accessing selection.content() repeatedly can be avoided, and for empty text selections it may not represent the user’s intent.

  • Cache const slice = editor.state.selection.content(); const first = slice.content.firstChild;
  • For collapsed text selections, consider duplicating the containing block node instead of a zero-width slice (optional, UX-dependent).

186-204: Add menu semantics for accessibility

Add role="menu" on the container and role="menuitem" on buttons for AT navigation. Also consider aria-labelledby if there’s a visible title.

-      <div
+      <div
         ref={(node) => {
           refs.setFloating(node);
           menuRef.current = node;
         }}
         style={{
           ...floatingStyles,
           animationFillMode: "forwards",
           transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)", // Expo ease out
         }}
-        className={cn(
+        className={cn(
           "z-20 max-h-60 min-w-[7rem] overflow-y-scroll rounded-lg border border-custom-border-200 bg-custom-background-100 p-1.5 shadow-custom-shadow-rg",
           "transition-all duration-300 transform origin-top-right",
           isAnimatedIn ? "opacity-100 scale-100" : "opacity-0 scale-75"
         )}
+        role="menu"
+        aria-orientation="vertical"
         {...getFloatingProps()}
       >

And for items:

-            <button
+            <button
               key={item.key}
               type="button"
               className="flex w-full items-center gap-1.5 truncate rounded px-1 py-1.5 text-xs text-custom-text-200 hover:bg-custom-background-90"
               onClick={item.onClick}
               disabled={item.isDisabled}
+              role="menuitem"
+              aria-disabled={item.isDisabled ? true : undefined}
             >
packages/editor/package.json (1)

48-65: Consolidate Tiptap imports to a single source

We’ve confirmed that the codebase currently mixes imports from the umbrella package (@tiptap/extensions) with individual extension packages (@tiptap/extension-*). This can lead to duplicate registrations and unnecessary bundle weight. Please pick one approach and update both your imports and dependencies in packages/editor/package.json accordingly:

• Files importing from the umbrella package:

  • packages/editor/src/core/extensions/placeholder.ts
  • packages/editor/src/core/extensions/extensions.ts (e.g. CharacterCount)

• Files importing individual extensions (partial list):

  • packages/editor/src/core/hooks/use-collaborative-editor.ts (extension-collaboration)
  • packages/editor/src/core/extensions/core-without-props.ts (extension-task-item, extension-task-list, extension-text-style, starter-kit)
  • packages/editor/src/core/extensions/emoji/*.ts (extension-emoji)
  • packages/editor/src/core/extensions/quote.ts (extension-blockquote)
  • packages/editor/src/core/extensions/mentions/extension-config.ts (extension-mention)
  • packages/editor/src/core/extensions/image/extension-config.tsx (extension-image)
  • (…)

Two safe paths—choose based on which import style you prefer:

Option A — keep umbrella, drop individuals:

-    "@tiptap/extension-blockquote": "^3.0.7",
-    "@tiptap/extension-collaboration": "^3.0.7",
-    "@tiptap/extension-emoji": "^3.0.7",
-    "@tiptap/extension-image": "^3.0.7",
-    "@tiptap/extension-list-item": "^3.0.7",
-    "@tiptap/extension-mention": "^3.0.7",
-    "@tiptap/extension-task-item": "^3.0.7",
-    "@tiptap/extension-task-list": "^3.0.7",
-    "@tiptap/extension-text-align": "^3.0.7",
-    "@tiptap/extension-text-style": "^3.0.7",
-    "@tiptap/starter-kit": "^3.0.7",

Option B — keep individuals, drop umbrella:

-    "@tiptap/extensions": "^3.0.7",

Once you decide, update all import sites and remove the unused deps from package.json. This will eliminate duplicate code and trim your final bundle.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5eac500 and 16e489a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • packages/editor/package.json (1 hunks)
  • packages/editor/src/core/components/menus/ai-menu.tsx (1 hunks)
  • packages/editor/src/core/components/menus/block-menu.tsx (5 hunks)
  • packages/editor/src/core/extensions/core-without-props.ts (1 hunks)
  • packages/editor/src/core/extensions/custom-image/components/block.tsx (2 hunks)
  • packages/editor/src/core/extensions/custom-image/components/uploader.tsx (5 hunks)
  • packages/editor/src/core/extensions/headings-list.ts (2 hunks)
  • packages/editor/src/core/extensions/slash-commands/root.tsx (3 hunks)
  • packages/editor/src/core/extensions/table/table/table-view.tsx (3 hunks)
  • packages/editor/src/core/helpers/editor-ref.ts (5 hunks)
  • packages/editor/src/styles/table.css (0 hunks)
💤 Files with no reviewable changes (1)
  • packages/editor/src/styles/table.css
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/editor/src/core/extensions/custom-image/components/block.tsx
  • packages/editor/src/core/extensions/core-without-props.ts
  • packages/editor/src/core/helpers/editor-ref.ts
  • packages/editor/src/core/extensions/custom-image/components/uploader.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
packages/editor/src/core/extensions/slash-commands/root.tsx (2)
packages/editor/src/core/helpers/tippy.ts (1)
  • CommandListInstance (1-3)
packages/editor/src/core/extensions/slash-commands/command-menu.tsx (2)
  • SlashCommandsMenuProps (10-14)
  • SlashCommandsMenu (16-151)
packages/editor/src/core/extensions/table/table/table-view.tsx (2)
packages/editor/src/core/extensions/code/lowlight-plugin.ts (1)
  • decorations (148-150)
packages/editor/src/core/extensions/table/plugins/selection-outline/plugin.ts (1)
  • decorations (55-57)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (7)
packages/editor/src/core/extensions/table/table/table-view.tsx (2)

1-4: Type-only imports: good v3 alignment and smaller bundles

Switching to type-only imports for Editor, NodeView, Decoration keeps runtime clean and matches Tiptap v3 packages. No issues spotted.


55-55: CSS class verification completed – all classes exist and scrolling/full-width behavior preserved

  • The .table-wrapper class (overflow-x: auto; padding-bottom: 26px) is defined in packages/editor/src/styles/table.css and applies horizontal scrolling.
  • The .editor-full-width-block modifier is in packages/editor/src/styles/variables.css, ensuring the table spans full width of the editor container.
  • The .horizontal-scrollbar utility (overflow-x: auto) and .scrollbar-sm sizing (12px height/width) are present in global styles under packages/ui/styles/globals.css (and mirrored in apps/web/styles/globals.css, etc.), providing the desired scrollbar appearance.

Given these definitions, the new wrapper classes maintain the previous horizontal-scroll and full-width functionality after removing the old toolbox DOM. Feel free to proceed.

packages/editor/src/core/components/menus/ai-menu.tsx (1)

23-25: Verify the invisible overlay doesn’t block interactions with the tippy element

You remove the menu node from the invisible full-screen overlay and append it elsewhere. With the overlay still active (pointer-events-auto when open), confirm it doesn’t intercept clicks meant for the menu if z-index stacking changes.

Would you sanity-check by toggling different themes/containers to ensure the tippy content remains above the overlay and interactions aren’t blocked?

packages/editor/src/core/components/menus/block-menu.tsx (1)

31-42: Nice migration to Floating UI with a virtual reference

Good use of useFloating + autoUpdate and a virtual reference. This keeps the menu anchored correctly and simplifies positioning.

packages/editor/src/core/extensions/slash-commands/root.tsx (1)

1-1: Overall: solid migration off Tippy; positioning logic is clean

Using a virtual caret rect with posToDOMRect and converting to Floating UI is the right call. With the autoUpdate addition, the UX should be robust.

packages/editor/package.json (2)

60-61: @tiptap/html is used at runtime – keep in dependencies

The calls to generateHTML and generateJSON occur in runtime helper modules, so this package must remain in dependencies:

  • packages/editor/src/core/helpers/yjs-utils.ts (HTML⇄JSON for rich-text and document editors)
  • apps/live/src/core/helpers/page.ts (HTML⇄JSON for live page content)

No change needed to move or remove @tiptap/html.

Likely an incorrect or invalid review comment.


60-65: Ensure ProseMirror dependency unification

I wasn’t able to detect multiple ProseMirror versions via the lockfile (pnpm-lock.yaml) or npm ls in the editor package, which is expected when using pnpm workspaces. However, absence of evidence isn’t evidence of absence—subtle mismatches between the ProseMirror core libs pulled in by @tiptap/pm (v3.0.7) and by @tiptap/y-tiptap (v3.0.0) can still occur.

Please manually verify that both extensions resolve to the same versions of:

  • prosemirror-model
  • prosemirror-state
  • prosemirror-view

You can run at the monorepo root:

# List all ProseMirror core versions across the workspace
pnpm list --pattern prosemirror-model prosemirror-state prosemirror-view

If you see more than one version (e.g. 1.25.x vs 1.40.x), consider adding a root-level override in package.json or pnpm.overrides to pin them to a single version, for example:

{
  "pnpm": {
    "overrides": {
      "prosemirror-model": "1.25.1",
      "prosemirror-state": "1.25.1",
      "prosemirror-view": "1.25.1"
    }
  }
}

This will ensure a consistent ProseMirror runtime and prevent subtle runtime or type conflicts.

Comment on lines +59 to +65
"@tiptap/extensions": "^3.0.7",
"@tiptap/html": "^3.0.7",
"@tiptap/pm": "^3.0.7",
"@tiptap/react": "^3.0.7",
"@tiptap/starter-kit": "^3.0.7",
"@tiptap/suggestion": "^3.0.7",
"@tiptap/y-tiptap": "^3.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Sanity check tiptap-markdown compatibility with v3.

Some tiptap-markdown builds on npm target v2 APIs. If you rely on it at runtime, confirm it officially supports TipTap v3 or you have a compatibility layer.

Run:

If it’s v2-only, we should either upgrade to a v3-compatible fork/package or keep it isolated to transform layers that don’t touch Editor instances.


🏁 Script executed:

#!/bin/bash
npm view tiptap-markdown version peerDependencies
# Also grep for its usage to scope impact
rg -nP --glob '!**/dist/**' --glob '!**/node_modules/**' "from ['\"]tiptap-markdown['\"]|require\\(['\"]tiptap-markdown['\"]\\)"

Length of output: 1597


Incompatible tiptap-markdown plugin detected – requires TipTap v2, not v3

Our investigation shows you’re pulling in [email protected], which declares a peer dependency on @tiptap/core ^2.0.3. Since the rest of the editor is now on TipTap v3, this plugin will not work at runtime without a compatibility layer.

Locations importing tiptap-markdown:

  • packages/editor/src/core/extensions/extensions.ts (line 6)
  • packages/editor/src/core/hooks/use-editor.ts (line 3)

You’ll need to either:

  • Swap in a TipTap v3–compatible Markdown extension (e.g. a community fork or the official v3 plugin if available),
  • Or isolate and transform Markdown outside of the running Editor instance, keeping it on a separate v2-compatible pipeline.

Please address this mismatch before merging.

🤖 Prompt for AI Agents
In packages/editor/package.json around lines 59-65, tiptap-markdown (v0.8.10) is
incompatible with the project’s TipTap v3; it declares a peer @tiptap/core v2
which will break runtime. Replace the dependency with a TipTap v3–compatible
Markdown extension (or a maintained community/official v3 fork), update the
imports in packages/editor/src/core/extensions/extensions.ts and
packages/editor/src/core/hooks/use-editor.ts to the new package, and remove the
old tiptap-markdown entry from package.json; alternatively, remove the plugin
usage from the editor code and perform Markdown parsing/serialization outside
the TipTap v3 instance with a separate v2-compatible pipeline. After changes,
update lockfile (install) and run editor tests to confirm runtime compatibility.

Comment on lines +61 to +65
"@tiptap/pm": "^3.0.7",
"@tiptap/react": "^3.0.7",
"@tiptap/starter-kit": "^3.0.7",
"@tiptap/suggestion": "^3.0.7",
"@tiptap/y-tiptap": "^3.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Collab stack: confirm @tiptap/y-tiptap@^3 + @tiptap/extension-collaboration + yjs@^13 versions are mutually compatible.

The versions listed look right, but a quick smoke check avoids runtime surprises in CRDT flows.

Run:

If peer ranges don’t align, we should pin compatible combos or add overrides.


🏁 Script executed:

#!/bin/bash
# Validate versions published and compatibility notes
npm view @tiptap/y-tiptap@^3 version peerDependencies
npm view y-prosemirror version peerDependencies
npm view yjs version

Length of output: 3937


🏁 Script executed:

#!/bin/bash
# Check peer dependencies for the collaboration extension to ensure compatibility with yjs@^13
npm view @tiptap/extension-collaboration@^3 version peerDependencies

Length of output: 4536


🏁 Script executed:

#!/usr/bin/env bash
# Locate @tiptap/extension-collaboration in the editor package.json
rg -n '"@tiptap/extension-collaboration"' -n packages/editor/package.json

Length of output: 127


🏁 Script executed:

#!/usr/bin/env bash
# Search for yjs and y-protocols entries in the editor package.json
rg -nP '\s*"yjs"\s*:' -n packages/editor/package.json
rg -nP '\s*"y-protocols"\s*:' -n packages/editor/package.json

Length of output: 176


🏁 Script executed:

#!/usr/bin/env bash
rg -n '"@tiptap/core"' packages/editor/package.json

Length of output: 86


Peer dependency mismatch: @tiptap/extension-collaboration@^3.0.7 requires prerelease @tiptap/y‐tiptap@^3.0.0-beta.3

Our smoke-check shows:

  • You’re using
    "@tiptap/y-tiptap": "^3.0.0",
    "@tiptap/extension-collaboration": "^3.0.7",
    "yjs": "^13.6.20"
  • But every 3.0.x of @tiptap/extension-collaboration (including 3.0.7) declares
    "peerDependencies": {
      "yjs": "^13",
      "@tiptap/pm": "^3.0.7",
      "@tiptap/core": "^3.0.7",
      "@tiptap/y-tiptap": "^3.0.0-beta.3"
    }
    which will not accept the stable 3.0.0 release of @tiptap/y-tiptap (semver prerelease ranges only match prereleases).

To resolve, choose one of:

  • Pin @tiptap/y-tiptap to the last prerelease that satisfies collaboration:
    - "@tiptap/y-tiptap": "^3.0.0",
    + "@tiptap/y-tiptap": "3.0.0-beta.3",
  • Downgrade @tiptap/extension-collaboration to 3.0.0 (its only non–beta-compatible version, which uses y-protocols instead of yjs):
    - "@tiptap/extension-collaboration": "^3.0.7",
    + "@tiptap/extension-collaboration": "3.0.0",
  • Or add an npm/Yarn override to force-map the peer dependency:
    // package.json
    "overrides": {
      "@tiptap/extension-collaboration": {
        "@tiptap/y-tiptap": "^3.0.0"
      }
    }

Without one of these changes, you’ll get unmet-peer warnings (and potential runtime breakage in CRDT flows).

🤖 Prompt for AI Agents
In packages/editor/package.json around lines 61-65 the installed
@tiptap/y-tiptap (3.0.0) conflicts with @tiptap/extension-collaboration@^3.0.7
which requires the prerelease @tiptap/y-tiptap@^3.0.0-beta.3; fix by doing one
of the following: (A) pin @tiptap/y-tiptap to the matching prerelease version
that satisfies ^3.0.0-beta.3 (update the dependency entry accordingly), (B)
downgrade @tiptap/extension-collaboration to the non-beta-compatible 3.0.0
release, or (C) add a package override mapping @tiptap/extension-collaboration
-> @tiptap/y-tiptap:^3.0.0 in package.json to force resolution; choose one
option and update package.json and lockfile, then reinstall and verify no peer
warnings.

Comment on lines +12 to +16
declare module "@tiptap/core" {
interface Storage {
[CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Ensure module augmentation key narrows to a string literal (not just string).

Using a computed key in an interface requires the expression’s type to be a literal (or a unique symbol). If CORE_EXTENSIONS.HEADINGS_LIST isn’t a literal type (e.g., missing as const at its definition), this augmentation won’t strongly type editor.storage[CORE_EXTENSIONS.HEADINGS_LIST] and may even be rejected by TS in stricter configs.

  • Verify that CORE_EXTENSIONS.HEADINGS_LIST is declared as a string literal (const-asserted), not string.
  • If it isn’t, either const-assert it at the source or consider a direct string-literal key for the augmentation to preserve types.

Run this to confirm the constant is a literal and const-asserted:


🏁 Script executed:

#!/bin/bash
# Find where CORE_EXTENSIONS is defined and inspect HEADINGS_LIST typing/value.
rg -nP 'export\s+(const|enum)\s+CORE_EXTENSIONS' -C3
rg -nP 'HEADINGS_LIST' -C2

Length of output: 1620


Ensure the module augmentation key is a true string literal

The computed key [CORE_EXTENSIONS.HEADINGS_LIST] is coming from a non-const enum, so its type is the enum CORE_EXTENSIONS rather than the literal "headingsList". In ambient contexts (like declare module), TypeScript only accepts literal types (string literals or unique symbol) for computed property names. As-is, this augmentation may be ignored by the compiler or fail under stricter TS configs.

Locations to update:

  • packages/editor/src/core/extensions/headings-list.ts (lines 12–16)
  • packages/editor/src/core/constants/extension.ts (enum declaration)

Two ways to fix:

  1. Use a direct string-literal key in the augmentation:
    declare module "@tiptap/core" {
      interface Storage {
    -   [CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage;
    +   headingsList: HeadingExtensionStorage;
      }
    }
  2. Convert the enum to a const enum so its members become literal types:
    // packages/editor/src/core/constants/extension.ts
    - export enum CORE_EXTENSIONS {
    + export const enum CORE_EXTENSIONS {
        BLOCKQUOTE      = "blockquote",
        BOLD            = "bold",
        BULLET_LIST     = "bulletList",
        /* … */
        HEADINGS_LIST   = "headingsList",
        /* … */
      }

Either approach will restore correct typing for editor.storage["headingsList"].

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
declare module "@tiptap/core" {
interface Storage {
[CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage;
}
}
// packages/editor/src/core/extensions/headings-list.ts
declare module "@tiptap/core" {
interface Storage {
// Use a true string literal here instead of a computed non-const enum member
headingsList: HeadingExtensionStorage;
}
}
Suggested change
declare module "@tiptap/core" {
interface Storage {
[CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage;
}
}
// packages/editor/src/core/constants/extension.ts
// Convert the enum to a const enum so its members are inlined as string literals
export const enum CORE_EXTENSIONS {
BLOCKQUOTE = "blockquote",
BOLD = "bold",
BULLET_LIST = "bulletList",
/* … */
HEADINGS_LIST = "headingsList",
/* … */
}
🤖 Prompt for AI Agents
In packages/editor/src/core/extensions/headings-list.ts around lines 12–16, the
module augmentation uses a computed key [CORE_EXTENSIONS.HEADINGS_LIST] which is
not a string literal; update this to use the actual string literal
"headingsList" (i.e. replace the computed property with "headingsList":
HeadingExtensionStorage) so the ambient declaration is accepted. Alternatively,
in packages/editor/src/core/constants/extension.ts change the enum declaration
to a const enum so CORE_EXTENSIONS.HEADINGS_LIST becomes a compile-time string
literal; ensure the enum member value is "headingsList" if not already. Choose
one approach and apply it consistently so editor.storage["headingsList"] is
correctly typed.

Comment on lines 72 to 95
let component: ReactRenderer<CommandListInstance, SlashCommandsMenuProps> | null = null;

return {
onStart: (props) => {
const MenuComponent = SlashCommandsMenu as unknown as FC<
SlashCommandsMenuProps & { ref: React.Ref<CommandListInstance> }
>;
component = new ReactRenderer(MenuComponent, {
props,
editor: props.editor,
});

if (!props.clientRect) {
return;
}

const element = component.element as HTMLElement;
element.style.position = "absolute";
element.style.zIndex = "100";
(props.editor.options.element || document.body).appendChild(element);

updatePosition(props.editor, element);
},

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Wire autoUpdate in render lifecycle and clean up

Register autoUpdate on start and dispose it on Escape/exit to track scroll/resize and selection-driven layout changes.

-        render: () => {
-          let component: ReactRenderer<CommandListInstance, SlashCommandsMenuProps> | null = null;
+        render: () => {
+          let component: ReactRenderer<CommandListInstance, SlashCommandsMenuProps> | null = null;
+          let cleanup: VoidFunction | null = null;
...
-            onStart: (props) => {
+            onStart: (props) => {
               const MenuComponent = SlashCommandsMenu as unknown as FC<
                 SlashCommandsMenuProps & { ref: React.Ref<CommandListInstance> }
               >;
               component = new ReactRenderer(MenuComponent, {
                 props,
                 editor: props.editor,
               });
 
               if (!props.clientRect) {
                 return;
               }
 
               const element = component.element as HTMLElement;
               element.style.position = "absolute";
               element.style.zIndex = "100";
               (props.editor.options.element || document.body).appendChild(element);
 
-              updatePosition(props.editor, element);
+              updatePosition(props.editor, element);
+              // Keep the menu anchored on scroll/resize/animation frames
+              const virtualRef = {
+                getBoundingClientRect: () =>
+                  posToDOMRect(props.editor.view, props.editor.state.selection.from, props.editor.state.selection.to),
+              } as any;
+              cleanup = autoUpdate(virtualRef, element, () => updatePosition(props.editor, element));
             },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let component: ReactRenderer<CommandListInstance, SlashCommandsMenuProps> | null = null;
return {
onStart: (props) => {
const MenuComponent = SlashCommandsMenu as unknown as FC<
SlashCommandsMenuProps & { ref: React.Ref<CommandListInstance> }
>;
component = new ReactRenderer(MenuComponent, {
props,
editor: props.editor,
});
if (!props.clientRect) {
return;
}
const element = component.element as HTMLElement;
element.style.position = "absolute";
element.style.zIndex = "100";
(props.editor.options.element || document.body).appendChild(element);
updatePosition(props.editor, element);
},
render: () => {
let component: ReactRenderer<CommandListInstance, SlashCommandsMenuProps> | null = null;
let cleanup: VoidFunction | null = null;
return {
onStart: (props) => {
const MenuComponent = SlashCommandsMenu as unknown as FC<
SlashCommandsMenuProps & { ref: React.Ref<CommandListInstance> }
>;
component = new ReactRenderer(MenuComponent, {
props,
editor: props.editor,
});
if (!props.clientRect) {
return;
}
const element = component.element as HTMLElement;
element.style.position = "absolute";
element.style.zIndex = "100";
(props.editor.options.element || document.body).appendChild(element);
updatePosition(props.editor, element);
// Keep the menu anchored on scroll/resize/animation frames
const virtualRef = {
getBoundingClientRect: () =>
posToDOMRect(
props.editor.view,
props.editor.state.selection.from,
props.editor.state.selection.to
),
} as any;
cleanup = autoUpdate(virtualRef, element, () =>
updatePosition(props.editor, element)
);
},
// …assumed onExit / onKeyDown handlers here…
};
},
🤖 Prompt for AI Agents
In packages/editor/src/core/extensions/slash-commands/root.tsx around lines 72
to 95, the render lifecycle creates a ReactRenderer but does not register
autoUpdate or clean it up; update onStart to call autoUpdate(props.editor.view,
element, updatePosition) (or the appropriate autoUpdate signature) and capture
the returned cleanup/disposer, store it on the component or a local variable,
and ensure that onEscape/onExit/onDestroy dispose that cleanup before removing
the element so the resize/scroll/selection listener is removed; also dispose the
ReactRenderer and remove the DOM element as part of that cleanup.

Comment on lines +109 to +117
onKeyDown: (props) => {
if (props.event.key === "Escape") {
component?.destroy();
component = null;
return true;
}

return component?.ref?.onKeyDown(props) ?? false;
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Dispose autoUpdate when closing via Escape

-              if (props.event.key === "Escape") {
-                component?.destroy();
-                component = null;
-                return true;
-              }
+              if (props.event.key === "Escape") {
+                cleanup?.();
+                cleanup = null;
+                component?.destroy();
+                component = null;
+                return true;
+              }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onKeyDown: (props) => {
if (props.event.key === "Escape") {
component?.destroy();
component = null;
return true;
}
return component?.ref?.onKeyDown(props) ?? false;
},
onKeyDown: (props) => {
if (props.event.key === "Escape") {
cleanup?.();
cleanup = null;
component?.destroy();
component = null;
return true;
}
return component?.ref?.onKeyDown(props) ?? false;
},
🤖 Prompt for AI Agents
In packages/editor/src/core/extensions/slash-commands/root.tsx around lines 109
to 117, when handling Escape you currently destroy the component but do not
dispose the autoUpdate handle; update the Escape branch to also clean up
autoUpdate by calling autoUpdate?.dispose() (or autoUpdate?.stop() /
autoUpdate?.cancel() to match the API) and then set autoUpdate = null before
returning, ensuring the auto-update subscription is released when the component
is closed.

Comment on lines +119 to +123
onExit: () => {
component?.destroy();
component = null;
},
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Dispose autoUpdate on exit too

-            onExit: () => {
-              component?.destroy();
-              component = null;
-            },
+            onExit: () => {
+              cleanup?.();
+              cleanup = null;
+              component?.destroy();
+              component = null;
+            },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onExit: () => {
component?.destroy();
component = null;
},
};
onExit: () => {
cleanup?.();
cleanup = null;
component?.destroy();
component = null;
},
🤖 Prompt for AI Agents
In packages/editor/src/core/extensions/slash-commands/root.tsx around lines 119
to 123, the onExit handler destroys the component but does not dispose the
autoUpdate handler; update the onExit to also stop and clean up autoUpdate
(e.g., call autoUpdate?.dispose() or clearInterval/clearTimeout depending on how
autoUpdate was created) and then set autoUpdate = null so the updater is fully
released when exiting.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
packages/editor/src/core/helpers/editor-ref.ts (2)

115-116: Unsafe access to editor.state. can still throw despite optional chaining.*

Patterns like editor?.state.selection.from and using editor.state inside ?? fall through. editor?.state yields undefined, then .selection is read off undefined. Also pos ?? editor.state.selection.from will evaluate the RHS when pos is nullish, even if editor is undefined.

Apply:

-    getCoordsFromPos: (pos) => editor?.view.coordsAtPos(pos ?? editor.state.selection.from),
-    getCurrentCursorPosition: () => editor?.state.selection.from,
+    getCoordsFromPos: (pos) =>
+      editor ? editor.view.coordsAtPos(pos ?? editor.state.selection.from) : undefined,
+    getCurrentCursorPosition: () => (editor ? editor.state.selection.from : undefined),
@@
-      const resolvedPos = pos ?? editor?.state.selection.from;
+      const resolvedPos = pos ?? (editor ? editor.state.selection.from : undefined);
@@
-      if (editor?.state.selection) {
+      if (editor?.state?.selection) {

This keeps API shape while eliminating potential TypeError when editor is not available.

Also applies to: 217-217, 222-222


1-251: Address unsafe optional-chaining patterns in EditorRef helpers

The ripgrep scan surfaced several locations where editor?.state.selection and editor?.storage.<…> are accessed without the second ?., which will throw if editor is nullish. Please update each of the following to use full optional-chain guards (editor?.state?.selection and editor?.storage?.…):

• packages/editor/src/core/helpers/editor-ref.ts
– Line 115:
diff - getCoordsFromPos: (pos) => editor?.view.coordsAtPos(pos ?? editor.state.selection.from), + getCoordsFromPos: (pos) => editor?.view.coordsAtPos(pos ?? editor?.state?.selection.from),
– Line 116:
diff - getCurrentCursorPosition: () => editor?.state.selection.from, + getCurrentCursorPosition: () => editor?.state?.selection.from,
– Line 217:
diff - const resolvedPos = pos ?? editor?.state.selection.from; + const resolvedPos = pos ?? editor?.state?.selection.from;
– Line 222:
diff - if (editor?.state.selection) { + if (editor?.state?.selection) {

• packages/editor/src/core/helpers/editor-ref.ts (storage access)
– Line 74:
diff - characters: editor?.storage.characterCount?.characters?.() ?? 0, + characters: editor?.storage?.characterCount?.characters?.() ?? 0,
– Line 76:
diff - words: editor?.storage.characterCount?.words?.() ?? 0, + words: editor?.storage?.characterCount?.words?.() ?? 0,
– Line 155:
diff - isEditorReadyToDiscard: () => editor?.storage.utility?.uploadInProgress === false, + isEditorReadyToDiscard: () => editor?.storage?.utility?.uploadInProgress === false,

• packages/editor/src/core/hooks/use-editor.ts
– Line 123:
diff - assets: editor?.storage.utility?.assetsList ?? [], + assets: editor?.storage?.utility?.assetsList ?? [],

These changes are required to prevent potential runtime TypeErrors when editor or its sub-objects are undefined.

🧹 Nitpick comments (2)
packages/editor/src/core/helpers/editor-ref.ts (2)

171-174: Update handler uses storage correctly; suggest mirroring the null-safe pattern.

Guard is present (if (!editor) return;), so this won’t crash. For consistency with getDocumentInfo(), you could also default values the same way:

-          characters: editor.storage.characterCount?.characters?.() ?? 0,
+          characters: editor.storage.characterCount?.characters?.() ?? 0,
           paragraphs: getParagraphCount(editor?.state),
-          words: editor.storage.characterCount?.words?.() ?? 0,
+          words: editor.storage.characterCount?.words?.() ?? 0,

No functional change; just aligning style.


189-193: Heading change callback: consider always emitting an array to clear stale UI.

Right now the callback is invoked only when headings is truthy. Emitting [] when headings are undefined helps consumers clear previously rendered items after structure edits.

-        const headings = editor.storage.headingsList?.headings;
-        if (headings) {
-          callback(headings);
-        }
+        const headings = editor.storage.headingsList?.headings ?? [];
+        callback(headings);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 16e489a and cbfc39b.

📒 Files selected for processing (1)
  • packages/editor/src/core/helpers/editor-ref.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/editor/src/core/helpers/editor-ref.ts (2)
packages/editor/src/core/helpers/common.ts (1)
  • getParagraphCount (104-111)
packages/editor/src/core/helpers/scroll-to-node.ts (1)
  • scrollSummary (51-56)
🔇 Additional comments (5)
packages/editor/src/core/helpers/editor-ref.ts (5)

1-2: Type-only imports: good call.

This avoids bundling those modules and prevents accidental runtime side effects; fits TS best practices for ambient types.


80-81: Direct markdown storage usage — LGTM.

Accessing editor.storage.markdown.getMarkdown() with a safe default is consistent with the v3 storage pattern.


28-29: Meta flags: double-check intended semantics when clearing vs setting content.

clearEditor() only sets SKIP_FILE_DELETION, while setEditorValue() sets both SKIP_FILE_DELETION and INTENTIONAL_DELETION. If consumers rely on both flags to differentiate user-initiated clears vs programmatic resets, confirm this divergence is intentional.

Do you want a quick pass to standardize meta usage across clear vs set operations?

Also applies to: 88-99


73-79: Events and counters: v3 storage/event usage looks correct.

  • Character/word counts are sourced from editor.storage.characterCount, which is the recommended v3 path.
  • Subscribing to "update" for these UI refreshes is appropriate.

Reference docs for counters and storage. (tiptap.dev)

Also applies to: 167-185


88-99: chain().setContent usage is correct for Tiptap v3

The chainable command editor.chain().setContent(...) uses Tiptap’s commands API, which in v3 defines setContent(content, options?)—i.e. a content argument followed by a single options object containing emitUpdate, parseOptions, etc. (tiptap.dev). The three-parameter signature (content, emitUpdate, parseOptions) belongs to the Editor#setContent instance method, not the chainable commands (v1.tiptap.dev). Your current code correctly passes an options object and does not need to be changed.

Likely an incorrect or invalid review comment.

@aaryan610 aaryan610 changed the title [WIKI-569] chore: upgrade to tiptap v3 [WIKI-569] chore: migrate to tiptap v3 Aug 23, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
packages/editor/src/core/extensions/mentions/utils.ts (3)

18-27: Handle computePosition errors to avoid unhandled rejections and stale UI.

If the promise rejects, the dropdown can remain mispositioned without feedback. Add a catch (and optionally ensure visibility toggles are consistent).

   computePosition(virtualElement, element, {
     placement: "bottom-start",
     strategy: "absolute",
     middleware: [shift(), flip()],
   }).then(({ x, y, strategy }) => {
     element.style.width = "max-content";
     element.style.position = strategy;
     element.style.left = `${x}px`;
     element.style.top = `${y}px`;
-  });
+  }).catch((error) => {
+    console.warn("Mentions dropdown: failed to compute position", error);
+    // Keep UI stable if positioning fails
+    element.style.visibility = "hidden";
+  }).finally(() => {
+    // Restore visibility when we have coordinates
+    if (element.style.left && element.style.top) {
+      element.style.visibility = "";
+    }
+  });

54-60: Null-guard on element during update — good fix.

This resolves the race highlighted earlier; safe to proceed when component?.element exists.


13-28: DRY: Extract updatePosition to a shared helper.

This positioning logic is used across multiple dropdowns (per earlier review). Centralizing it (and the error handling) will simplify maintenance.

Example new helper (suggested file: packages/editor/src/core/helpers/floating.ts):

import { computePosition, flip, shift } from "@floating-ui/dom";
import { type Editor, posToDOMRect } from "@tiptap/core";

export async function positionDropdown(
  editor: Editor,
  element: HTMLElement,
  getBoundingClientRect?: () => DOMRect
) {
  const virtual = {
    getBoundingClientRect:
      getBoundingClientRect ??
      (() => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to)),
  };

  try {
    const { x, y, strategy } = await computePosition(virtual as any, element, {
      placement: "bottom-start",
      strategy: "absolute",
      middleware: [shift(), flip()],
    });
    element.style.width = "max-content";
    element.style.position = strategy;
    element.style.left = `${x}px`;
    element.style.top = `${y}px`;
    element.style.visibility = "";
  } catch (e) {
    console.warn("Dropdown positioning failed:", e);
    element.style.visibility = "hidden";
  }
}

Then replace local updatePosition calls with positionDropdown(...).

🧹 Nitpick comments (2)
packages/editor/src/core/extensions/mentions/utils.ts (2)

49-52: Prevent flash-of-unpositioned content (FOUC) before first positioning.

Hide the element until the first coordinates are applied.

-        const element = component.element as HTMLElement;
-        element.style.position = "absolute";
+        const element = component.element as HTMLElement;
+        element.style.position = "absolute";
+        element.style.visibility = "hidden";
         document.body.appendChild(element);
         updatePosition(props.editor, element);

61-74: Key handling polish: stop propagation on Escape and return a strict boolean.

  • Stop propagation on Escape to avoid editor-level side effects.
  • Coalesce onKeyDown return to boolean to satisfy the contract even if ref is missing.
       onKeyDown: (props) => {
         if (props.event.key === "Escape") {
-          component?.destroy();
+          props.event?.stopPropagation();
+          component?.destroy();
           return true;
         }

         const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];

         if (navigationKeys.includes(props.event.key)) {
           props.event?.stopPropagation();
-          return component?.ref?.onKeyDown(props);
+          return component?.ref?.onKeyDown(props) ?? false;
         }
-        return component?.ref?.onKeyDown(props);
+        return component?.ref?.onKeyDown(props) ?? false;
       },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cbfc39b and cd48989.

📒 Files selected for processing (2)
  • packages/editor/src/core/extensions/mentions/utils.ts (2 hunks)
  • packages/editor/src/core/helpers/editor-ref.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/editor/src/core/helpers/editor-ref.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/editor/src/core/extensions/mentions/utils.ts (3)
packages/editor/src/core/types/mention.ts (1)
  • TMentionHandler (21-25)
packages/editor/src/core/helpers/tippy.ts (1)
  • CommandListInstance (1-3)
packages/editor/src/core/extensions/mentions/mentions-list-dropdown.tsx (1)
  • MentionsListDropdownProps (13-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
packages/editor/src/core/extensions/mentions/utils.ts (2)

1-2: Imports look correct for v3 migration.

Using posToDOMRect from @tiptap/core and Floating UI imports aligns with the v3 stack.


13-28: Prefer anchoring to TipTap’s clientRect when available.

posToDOMRect on the current selection usually works, but the Suggestion plugin already provides a precise anchor via props.clientRect. Accepting a rect getter improves accuracy during IME composition and complex ranges.
[Suggest_optional_refactor]

-const updatePosition = (editor: Editor, element: HTMLElement) => {
-  const virtualElement = {
-    getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
-  };
+const updatePosition = (
+  editor: Editor,
+  element: HTMLElement,
+  getBoundingClientRect: () => DOMRect = () =>
+    posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to)
+) => {
+  const virtualElement = { getBoundingClientRect };

And pass the getter where you already validate props.clientRect:

-        updatePosition(props.editor, element);
+        updatePosition(props.editor, element, props.clientRect!);
-          updatePosition(props.editor, component?.element as HTMLElement);
+          updatePosition(props.editor, component?.element as HTMLElement, props.clientRect!);

trigger: "manual",
placement: "bottom-start",
});
props.editor.storage.utility.activeDropbarExtensions.push(CORE_EXTENSIONS.MENTION);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Leaking active state in storage; ensure symmetric cleanup on exit.

You push CORE_EXTENSIONS.MENTION into editor.storage.utility.activeDropbarExtensions but never remove it. This can leave stale state and break feature gating/UX. Also, make element.remove() null-safe.

       onExit: () => {
-        component?.element.remove();
-        component?.destroy();
+        // Remove active state for this extension
+        const list = component?.editor?.storage?.utility?.activeDropbarExtensions;
+        if (Array.isArray(list)) {
+          const idx = list.lastIndexOf(CORE_EXTENSIONS.MENTION);
+          if (idx !== -1) list.splice(idx, 1);
+        }
+        component?.element?.remove();
+        component?.destroy();
       },

Also applies to: 75-78

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (5)
packages/editor/src/core/extensions/mentions/utils.ts (2)

21-36: Keep dropdown aligned on scroll/resize via autoUpdate (optional, shared helper available)

Position only updates on Start/Update; it won’t follow scroll/viewport changes. Either use Floating UI’s autoUpdate here or the new attachFloatingUIAutoUpdate helper to wire/cleanup automatically.

+import { attachFloatingUIAutoUpdate } from "@/helpers/floating-ui";
@@
   () => {
     const { searchCallback } = props;
     let component: ReactRenderer<CommandListInstance, MentionsListDropdownProps> | null = null;
+    let cleanup: VoidFunction | null = null;
@@
         const element = component.element as HTMLElement;
         element.style.position = "absolute";
-        document.body.appendChild(element);
-        updateFloatingUIFloaterPosition(props.editor, element);
+        document.body.appendChild(element);
+        cleanup = attachFloatingUIAutoUpdate(props.editor, element);
       },
@@
       onExit: () => {
-        component?.element.remove();
+        cleanup?.();
+        cleanup = null;
+        component?.element?.remove();
         component?.destroy();
       },

31-31: Symmetric cleanup for activeDropbar state and DOM element

You push CORE_EXTENSIONS.MENTION into editor.storage.utility.activeDropbarExtensions but never remove it; also Escape path doesn’t remove the appended element. This can leave stale state and stray DOM.

       onKeyDown: (props) => {
         if (props.event.key === "Escape") {
-          component?.destroy();
+          // remove active state
+          const list = props.editor.storage?.utility?.activeDropbarExtensions;
+          if (Array.isArray(list)) {
+            const idx = list.lastIndexOf(CORE_EXTENSIONS.MENTION);
+            if (idx !== -1) list.splice(idx, 1);
+          }
+          component?.element?.remove();
+          component?.destroy();
           return true;
         }
@@
       onExit: () => {
-        component?.element.remove();
-        component?.destroy();
+        // remove active state
+        const list = component?.editor?.storage?.utility?.activeDropbarExtensions;
+        if (Array.isArray(list)) {
+          const idx = list.lastIndexOf(CORE_EXTENSIONS.MENTION);
+          if (idx !== -1) list.splice(idx, 1);
+        }
+        component?.element?.remove();
+        component?.destroy();
       },

Also applies to: 58-60

packages/editor/src/core/extensions/slash-commands/root.tsx (3)

56-76: Create component only when usable and wire autoUpdate to track layout changes

  • Minor: Don’t construct ReactRenderer if clientRect is missing.
  • Optional: Use Floating UI’s autoUpdate so the menu stays glued to the caret on scroll/resize/line wraps. Clean it up on close.
-import { updateFloatingUIFloaterPosition } from "@/helpers/floating-ui";
+import { updateFloatingUIFloaterPosition, attachFloatingUIAutoUpdate } from "@/helpers/floating-ui";
@@
-          let component: ReactRenderer<CommandListInstance, SlashCommandsMenuProps> | null = null;
+          let component: ReactRenderer<CommandListInstance, SlashCommandsMenuProps> | null = null;
+          let cleanup: VoidFunction | null = null;
@@
-            onStart: (props) => {
-              const MenuComponent = SlashCommandsMenu as unknown as FC<
+            onStart: (props) => {
+              if (!props.clientRect) {
+                return;
+              }
+              const MenuComponent = SlashCommandsMenu as unknown as FC<
                 SlashCommandsMenuProps & { ref: React.Ref<CommandListInstance> }
               >;
               component = new ReactRenderer(MenuComponent, {
                 props,
                 editor: props.editor,
               });
-
-              if (!props.clientRect) {
-                return;
-              }
 
               const element = component.element as HTMLElement;
               element.style.position = "absolute";
               element.style.zIndex = "100";
               (props.editor.options.element || document.body).appendChild(element);
 
-              updateFloatingUIFloaterPosition(props.editor, element);
+              cleanup = attachFloatingUIAutoUpdate(props.editor, element);
             },

90-98: Dispose autoUpdate and remove element on Escape

Prevent leaks and ensure the DOM node is removed even if ReactRenderer fails to.

             onKeyDown: (props) => {
               if (props.event.key === "Escape") {
-                component?.destroy();
-                component = null;
+                cleanup?.();
+                cleanup = null;
+                component?.element?.remove();
+                component?.destroy();
+                component = null;
                 return true;
               }
 
               return component?.ref?.onKeyDown(props) ?? false;
             },

100-103: Dispose autoUpdate and remove element on exit as well

Mirror the Escape path for a clean teardown.

             onExit: () => {
-              component?.destroy();
-              component = null;
+              cleanup?.();
+              cleanup = null;
+              component?.element?.remove();
+              component?.destroy();
+              component = null;
             },
🧹 Nitpick comments (4)
packages/editor/src/core/helpers/floating-ui.ts (3)

18-31: Harden async positioning: add error handling and a small default offset

computePosition is async and can throw (e.g., selection/view teardown). Add a catch to avoid noisy console errors and optionally include a small offset so menus don’t overlap the caret.

-import { computePosition, flip, type Middleware, type Strategy, type Placement, shift } from "@floating-ui/dom";
+import { computePosition, flip, type Middleware, type Strategy, type Placement, shift, offset } from "@floating-ui/dom";
@@
-  computePosition(virtualElement, element, {
-    placement: options?.placement ?? "bottom-start",
-    strategy: options?.strategy ?? "absolute",
-    middleware: options?.middleware ?? [shift(), flip()],
-  }).then(({ x, y, strategy }) => {
+  computePosition(virtualElement, element, {
+    placement: options?.placement ?? "bottom-start",
+    strategy: options?.strategy ?? "absolute",
+    middleware: options?.middleware ?? [offset(4), shift(), flip()],
+  }).then(({ x, y, strategy }) => {
     Object.assign(element.style, {
       width: "max-content",
       position: strategy,
       left: `${x}px`,
       top: `${y}px`,
       ...options?.elementStyle,
     });
-  });
+  }).catch((err) => {
+    // Non-fatal: positioning can fail during view/selection teardown
+    if (process.env.NODE_ENV !== "production") console.warn("Floating UI positioning failed:", err);
+  });

14-16: Defensive virtual reference to avoid crashes during teardown

If editor.view is destroyed between scheduling and running, posToDOMRect can throw. Minor, but worth guarding.

-  const virtualElement = {
-    getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
-  };
+  const virtualElement = {
+    getBoundingClientRect: () => {
+      const view = editor?.view;
+      const state = editor?.state;
+      if (!view || !state) return new DOMRect(0, 0, 0, 0);
+      return posToDOMRect(view, state.selection.from, state.selection.to);
+    },
+  };

1-31: Optional: provide an autoUpdate helper so call sites don’t re-implement it

Multiple callers need scroll/resize tracking. Export a small helper that returns a disposer; low-risk addition that doesn’t break existing usage.

 import { computePosition, flip, type Middleware, type Strategy, type Placement, shift, offset } from "@floating-ui/dom";
+import { autoUpdate } from "@floating-ui/dom";
@@
 export const updateFloatingUIFloaterPosition = (
@@
 };
+
+export const attachFloatingUIAutoUpdate = (
+  editor: Editor,
+  element: HTMLElement,
+  options?: {
+    elementStyle?: Partial<CSSStyleDeclaration>;
+    middleware?: Middleware[];
+    placement?: Placement;
+    strategy?: Strategy;
+  }
+) => {
+  const virtualRef = {
+    getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
+  } as any;
+  const update = () => updateFloatingUIFloaterPosition(editor, element, options);
+  // Immediately position once and then subscribe
+  update();
+  const cleanup = autoUpdate(virtualRef, element, update);
+  return cleanup;
+};

Happy to wire this into mentions/slash-commands in a follow-up if you want.

packages/editor/src/core/extensions/mentions/utils.ts (1)

33-36: Append inside editor container and set stacking context

For consistency with Slash Commands and to avoid z-index layering issues, append to editor.options.element when available and set a safe z-index.

-        const element = component.element as HTMLElement;
-        element.style.position = "absolute";
-        document.body.appendChild(element);
+        const element = component.element as HTMLElement;
+        element.style.position = "absolute";
+        element.style.zIndex = "100";
+        (props.editor.options.element || document.body).appendChild(element);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cd48989 and b2c23d1.

📒 Files selected for processing (3)
  • packages/editor/src/core/extensions/mentions/utils.ts (3 hunks)
  • packages/editor/src/core/extensions/slash-commands/root.tsx (2 hunks)
  • packages/editor/src/core/helpers/floating-ui.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/editor/src/core/extensions/mentions/utils.ts (1)
packages/editor/src/core/helpers/floating-ui.ts (1)
  • updateFloatingUIFloaterPosition (4-31)
packages/editor/src/core/extensions/slash-commands/root.tsx (3)
packages/editor/src/core/helpers/tippy.ts (1)
  • CommandListInstance (1-3)
packages/editor/src/core/extensions/slash-commands/command-menu.tsx (2)
  • SlashCommandsMenuProps (10-14)
  • SlashCommandsMenu (16-151)
packages/editor/src/core/helpers/floating-ui.ts (1)
  • updateFloatingUIFloaterPosition (4-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
packages/editor/src/core/extensions/mentions/utils.ts (1)

37-43: Good fix: null-check before repositioning

The extra element guard in onUpdate avoids rare race conditions. Looks good.

packages/editor/src/core/extensions/slash-commands/root.tsx (1)

77-88: LGTM: update path is safe and idempotent

Guarding component existence, updating props, and repositioning is correct.

Comment on lines 50 to +56
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];

if (navigationKeys.includes(props.event.key)) {
props.event?.stopPropagation();
if (component?.ref?.onKeyDown(props)) {
return true;
}
return component?.ref?.onKeyDown(props);
}
return false;
return component?.ref?.onKeyDown(props);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent default navigation to stop caret moving while using the list

ArrowUp/ArrowDown/Enter should preventDefault, otherwise the editor selection may also move even though you stopPropagation.

         const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
 
         if (navigationKeys.includes(props.event.key)) {
-          props.event?.stopPropagation();
-          return component?.ref?.onKeyDown(props);
+          props.event.preventDefault();
+          props.event.stopPropagation();
+          return component?.ref?.onKeyDown(props) ?? true;
         }
-        return component?.ref?.onKeyDown(props);
+        return component?.ref?.onKeyDown(props) ?? false;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
if (navigationKeys.includes(props.event.key)) {
props.event?.stopPropagation();
if (component?.ref?.onKeyDown(props)) {
return true;
}
return component?.ref?.onKeyDown(props);
}
return false;
return component?.ref?.onKeyDown(props);
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
if (navigationKeys.includes(props.event.key)) {
props.event.preventDefault();
props.event.stopPropagation();
return component?.ref?.onKeyDown(props) ?? true;
}
return component?.ref?.onKeyDown(props) ?? false;
🤖 Prompt for AI Agents
In packages/editor/src/core/extensions/mentions/utils.ts around lines 50 to 56,
the handler checks ArrowUp/ArrowDown/Enter but only calls stopPropagation, which
still allows the editor caret/selection to move; call
props.event.preventDefault() before stopPropagation for those keys and then
invoke component?.ref?.onKeyDown(props); remove the duplicate return by always
returning component?.ref?.onKeyDown(props) (after preventDefault+stopPropagation
when the key is in navigationKeys) and ensure you null-check props.event before
calling preventDefault/stopPropagation.

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

Successfully merging this pull request may close these issues.

2 participants