-
Notifications
You must be signed in to change notification settings - Fork 2.7k
[WIKI-569] chore: migrate to tiptap v3 #7526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: preview
Are you sure you want to change the base?
Conversation
WalkthroughTiptap packages upgraded v2 → v3 and the Changes
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Pull Request Linked with Plane Work Items
Comment Automatically Generated by Plane |
There was a problem hiding this 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 outdatedtiptap-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
⛔ 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 appWe’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
, andgenerateJSON
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()
returnsundefined
, 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
toundoRedo: 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
todiv
element maintains the same functionality with thewhitespace-pre-wrap
class while potentially improving rendering behavior. The semantic meaning is preserved since it's already wrapped in apre
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 thegetExtensionStorage
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 thegetExtensionStorage
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 directeditor.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 directeditor.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 consolidatedNo remaining imports of
@tiptap/extension-character-count
or@tiptap/extension-placeholder
were detected. BothCharacterCount
andPlaceholder
are now imported from@tiptap/extensions
and used in:• packages/editor/src/core/extensions/placeholder.ts
• packages/editor/src/core/extensions/extensions.tsNo 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 directeditor.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 whengetPos()
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 directeditor.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
andparseOptions
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 inuse-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 directeditor.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 addsNodeViewProps
to support the new position handling pattern.
93-93
: Good refactoring: Dynamic position handling.Changing from static
pos: number
to dynamicgetPos: 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
, andtippy
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
There was a problem hiding this 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
📒 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
Theeditor.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 removedgetExtensionStorage
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.
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`; | ||
}); | ||
}; |
There was a problem hiding this comment.
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.
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.
There was a problem hiding this 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 updateupdateColumnsOnResize
call to match Tiptap v3 signatureThe upstream
@tiptap/pm/tables
export forupdateColumnsOnResize
in v3 has changed: it now expectsupdateColumnsOnResize( tr: Transaction, table: Node, start: number, mapping?: Mapping, ): Transaction— i.e. it takes a ProseMirror
Transaction
, the tableNode
, the table’s start offset, and an optionalMapping
, returning a newTransaction
.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: useclosest('#ai-handle')
to support clicks on nested elements within the handleUsing
target.matches('#ai-handle')
fails when clicking a child of the handle. Switch toclosest
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 signaturesThe
insertContentAt
andfocus
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, omitupdateSelection
(v3’sinsertContentAt
has no such option) and rely on the default behavior or explicitly call.setSelection(...)
afterward.Making these changes will satisfy the following v3 signatures:
insertContentAt(position, content): Chainable<any>
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
andappendedTransactions
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 thetransactions
parameter ofappendTransaction
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 stateYou 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 v3Using 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 creationMinor 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 fieldscontrols 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 trueAlways 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
: HardenappendTo
to avoid null return fromquerySelector
document.querySelector('.frame-renderer')
can return null, which breaks tippy’s expectation. Provide a safe fallback todocument.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 pressAttaching
hidePopup
directly tokeydown
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 atabIndex
. 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 consistencyMost 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 inblock-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 interactionsYou’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 ofdocument
.- Drop the extra
keydown
handler (Escape) in favor ofuseDismiss
.
137-141
: Guardselection.content()
usage and avoid redundant checksAccessing
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 accessibilityAdd
role="menu"
on the container androle="menuitem"
on buttons for AT navigation. Also consideraria-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 sourceWe’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 inpackages/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.
⛔ 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 bundlesSwitching 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 inpackages/editor/src/styles/table.css
and applies horizontal scrolling.- The
.editor-full-width-block
modifier is inpackages/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 underpackages/ui/styles/globals.css
(and mirrored inapps/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 elementYou 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 referenceGood 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 cleanUsing a virtual caret rect with
posToDOMRect
and converting to Floating UI is the right call. With theautoUpdate
addition, the UX should be robust.packages/editor/package.json (2)
60-61
: @tiptap/html is used at runtime – keep in dependenciesThe calls to
generateHTML
andgenerateJSON
occur in runtime helper modules, so this package must remain independencies
:
- 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 unificationI 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
orpnpm.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.
"@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", |
There was a problem hiding this comment.
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.
"@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", |
There was a problem hiding this comment.
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
(including3.0.7
) declareswhich will not accept the stable"peerDependencies": { "yjs": "^13", "@tiptap/pm": "^3.0.7", "@tiptap/core": "^3.0.7", "@tiptap/y-tiptap": "^3.0.0-beta.3" }
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
to3.0.0
(its only non–beta-compatible version, which usesy-protocols
instead ofyjs
):- "@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.
declare module "@tiptap/core" { | ||
interface Storage { | ||
[CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage; | ||
} | ||
} |
There was a problem hiding this comment.
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), notstring
. - 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:
- Use a direct string-literal key in the augmentation:
declare module "@tiptap/core" { interface Storage { - [CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage; + headingsList: HeadingExtensionStorage; } }
- 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.
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; | |
} | |
} |
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.
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); | ||
}, | ||
|
There was a problem hiding this comment.
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.
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.
onKeyDown: (props) => { | ||
if (props.event.key === "Escape") { | ||
component?.destroy(); | ||
component = null; | ||
return true; | ||
} | ||
|
||
return component?.ref?.onKeyDown(props) ?? false; | ||
}, |
There was a problem hiding this comment.
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.
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.
onExit: () => { | ||
component?.destroy(); | ||
component = null; | ||
}, | ||
}; |
There was a problem hiding this comment.
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.
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.
There was a problem hiding this 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 usingeditor.state
inside??
fall through.editor?.state
yieldsundefined
, then.selection
is read offundefined
. Alsopos ?? editor.state.selection.from
will evaluate the RHS whenpos
isnullish
, even ifeditor
isundefined
.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
wheneditor
is not available.Also applies to: 217-217, 222-222
1-251
: Address unsafe optional-chaining patterns in EditorRef helpersThe ripgrep scan surfaced several locations where
editor?.state.selection
andeditor?.storage.<…>
are accessed without the second?.
, which will throw ifeditor
is nullish. Please update each of the following to use full optional-chain guards (editor?.state?.selection
andeditor?.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 withgetDocumentInfo()
, 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.
📒 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 setsSKIP_FILE_DELETION
, whilesetEditorValue()
sets bothSKIP_FILE_DELETION
andINTENTIONAL_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 v3The chainable command
editor.chain().setContent(...)
uses Tiptap’s commands API, which in v3 definessetContent(content, options?)
—i.e. a content argument followed by a single options object containingemitUpdate
,parseOptions
, etc. (tiptap.dev). The three-parameter signature(content, emitUpdate, parseOptions)
belongs to theEditor#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.
There was a problem hiding this 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
: HandlecomputePosition
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: ExtractupdatePosition
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 withpositionDropdown(...)
.
🧹 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 ifref
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.
📒 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’sclientRect
when available.
posToDOMRect
on the current selection usually works, but the Suggestion plugin already provides a precise anchor viaprops.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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this 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 elementYou 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 EscapePrevent 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 wellMirror 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 offsetcomputePosition 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 teardownIf 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 itMultiple 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 contextFor 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.
📒 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 repositioningThe 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 idempotentGuarding component existence, updating props, and repositioning is correct.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
Description
This PR upgrades Tiptap to
v3
Type of Change
Summary by CodeRabbit
New Features
Bug Fixes
Refactor
Chores