Skip to content

feat(EditorDragHandle): proxy nested / nestedOptions props and emit hover event#5960

Merged
benjamincanac merged 12 commits intonuxt:v4from
solidprinciples:fix/editor-handles-props
Jan 29, 2026
Merged

feat(EditorDragHandle): proxy nested / nestedOptions props and emit hover event#5960
benjamincanac merged 12 commits intonuxt:v4from
solidprinciples:fix/editor-handles-props

Conversation

@solidprinciples
Copy link
Contributor

@solidprinciples solidprinciples commented Jan 27, 2026

❓ Type of change

  • 📖 Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

Nested drag handle options are documented but were not being forwarded in the implementation. This PR updates the editor drag handle to correctly pass through the nested and nestedOptions props exposed by TipTap’s DragHandleProps type.

It also omits these same keys from the button props, where they are not used.

Additionally, this PR introduces a new @hover event, emitted when the currently hovered drag handle node changes, allowing consumers to track the active node without relying on click events.

Together, these changes fix and enable proper support for nested drag handles and improve drag handle interaction handling.

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

…d 'nestedOptions', omit the same from button props
@github-actions github-actions bot added the v4 #4488 label Jan 27, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

Added a hover emit and handling in onNodeChange: pos is validated (non-null and >=0) before node lookup, and hover is emitted with { node, pos } when a node exists. Forwarded public props for dragHandleProps and buttonProps now include nested and nestedOptions. onClick now guards against invalid pos (null or negative) and relies on the slot-provided on-click binding; the root-level @click handler was removed. Slot/props wiring updated to match the new forward-props shape and hover emission flow.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the changes: fixing nested prop forwarding, omitting unused keys from button props, and adding a hover event for tracking active nodes without click reliance.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title accurately summarizes the main changes: proxying nested/nestedOptions props and emitting a hover event in EditorDragHandle component.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 27, 2026

npm i https://pkg.pr.new/@nuxt/ui@5960

commit: 09a4bb7

@solidprinciples solidprinciples changed the title fix(EditorDragHandle): update props forwarding to include 'nested' and 'nestedOptions', omit these from button props fix(editor): forward nested props and omit from button Jan 28, 2026
@solidprinciples solidprinciples changed the title fix(editor): forward nested props and omit from button fix(EditorDragHandle): pass nested option props, add custom middleware Jan 28, 2026
@solidprinciples solidprinciples changed the title fix(EditorDragHandle): pass nested option props, add custom middleware fix(EditorDragHandle): pass nested + nestedOptions, add custom middleware Jan 28, 2026
@solidprinciples solidprinciples changed the title fix(EditorDragHandle): pass nested + nestedOptions, add custom middleware feat(EditorDragHandle): pass nested props, custom middleware, fix onNodeChange Jan 28, 2026
@solidprinciples
Copy link
Contributor Author

solidprinciples commented Jan 28, 2026

@benjamincanac - noticed a few bugs/things that didn't quite align with documentation.

Here's a small concise PR for the fixes:

  • nested, nestedOptions weren't passed - omitted these from buttons - so it now supports nesting!
  • emit @node-change is documented as emitting on hover, but was only emitted onClick - fixed!
  • onClick is not impacted - so no breaking changes!

😄 first contribution, long time user of nuxt ui ❤️ - custom middleware seemed simple enough, and opens doors for flexibility. If its not robust enough, I can revert it.

@solidprinciples solidprinciples changed the title feat(EditorDragHandle): pass nested props, custom middleware, fix onNodeChange feat(EditorDragHandle): pass tiptap props, custom middleware, fix onNodeChange emit Jan 28, 2026
Copy link
Member

@benjamincanac benjamincanac left a comment

Choose a reason for hiding this comment

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

When building the Editor I did try to store the current node and send it inside the nodeChange even like you did but for a reason I never understood this breaks the drag handle completely, I even tried to pass it inside default slot props but didn't work either. My guess is the node.toJSON() object is just too big.

You can try in the playground and see it no longer displays when hovering elements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/runtime/components/EditorDragHandle.vue`:
- Around line 42-44: The emit was renamed from nodeChange to nodeChanged,
breaking consumers who used `@node-change`; update the API docs and changelog to
document this breaking change and provide a migration note (replace `@node-change`
with `@node-changed`), and ensure the type/interface EditorDragHandleEmits and any
usages of nodeChange in EditorDragHandle.vue and elsewhere are updated to
nodeChanged so templates and listeners reflect the new event name.

@benjamincanac
Copy link
Member

@solidprinciples We can't introduce a breaking change like this unfortunately, what are you trying to solve here? The @node-change event is used for this example: https://ui.nuxt.com/docs/components/editor-drag-handle#with-dropdown-menu so it's fine to trigger it on click in my opinion.

@solidprinciples
Copy link
Contributor Author

Looking to be able to track currently selected node by the handle upon hover.

@benjamincanac
Copy link
Member

What about adding a new hover event? 🤔

diff --git a/src/runtime/components/EditorDragHandle.vue b/src/runtime/components/EditorDragHandle.vue
index 2c1824881..862cc47b9 100644
--- a/src/runtime/components/EditorDragHandle.vue
+++ b/src/runtime/components/EditorDragHandle.vue
@@ -40,6 +40,10 @@ export interface EditorDragHandleSlots {
 
 export interface EditorDragHandleEmits {
   nodeChange: [{ node: JSONContent, pos: number }]
+  /**
+   * Emitted when the hovered node changes.
+   */
+  hover: [{ node: JSONContent, pos: number }]
 }
 </script>
 
@@ -114,6 +118,13 @@ const currentNodePos = ref<number | null>()
 
 function onNodeChange({ pos }: { pos: number }) {
   currentNodePos.value = pos
+
+  if (!props.editor || pos < 0) return
+
+  const node = props.editor.state.doc.nodeAt(pos)
+  if (node) {
+    emit('hover', { node: node.toJSON(), pos })
+  }
 }
 
 function onClick() {

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
src/runtime/components/EditorDragHandle.vue (1)

154-164: Default handle no longer triggers onClick.

After removing the root @click, the fallback <UButton> isn’t wired to onClick, so nodeChange will never fire for the default slot. Please bind the handler in the fallback content to avoid a regression.

🛠️ Suggested fix
       <UButton
         v-bind="{
           ...buttonProps,
           icon: props.icon || appConfig.ui.icons.drag,
           ...$attrs
         }"
         data-slot="handle"
         :class="ui.handle({ class: [props.ui?.handle, props.class] })"
         :ui="transformUI(ui, props.ui)"
+        `@click`="onClick"
       />

@solidprinciples
Copy link
Contributor Author

@benjamincanac hmm. same issue - I've at least diagnosed it down to this:

  • no external parent state modifications: renders fine (console.log, etc)
  • external parent state (setting ref, emit, etc): breaks rendering

Haven't seen anything on tiptap's repo :(

@solidprinciples
Copy link
Contributor Author

@benjamincanac - https://github.com/solidprinciples/tiptap-draghandle-state-update-bug/tree/bug/drag-handle

Tiptap bug - same thing happens in their extension demo. Investigating to see feasibility of fixing upstream.

@solidprinciples
Copy link
Contributor Author

solidprinciples commented Jan 28, 2026

@benjamincanac fixed in tip-tap - small few-liner in ueberdosis/tiptap#7472

👍 - seems like the DragHandle is re-rendering and re-applying visibility: hidden in these cases.

@benjamincanac
Copy link
Member

I already submitted such a fix back in November 😬 ueberdosis/tiptap#7252

But I don't see how it's relevant to this issue 🤔

@solidprinciples
Copy link
Contributor Author

solidprinciples commented Jan 28, 2026

I already submitted such a fix back in November 😬 ueberdosis/tiptap#7252

But I don't see how it's relevant to this issue 🤔

In testing for my use case outside of Nuxt - directly in Tiptap, which this feature depends on - I ran into an issue where the drag handles would disappear when consuming state extracted from @nodeChange externally (i.e. what @hover is now doing).

Persisting the value to a ref worked fine, but actually using that ref in the template or elsewhere caused rendering to break. This behavior reproduced consistently in Tiptap’s own extension demo, independent of Nuxt, which is what led me to dig further.

That surfaced an upstream Tiptap issue related to the DragHandle re-rendering and re-applying visibility: hidden during external state updates. I was able to isolate, diagnose, and fix it in a minimal repro, and the same fix can be picked up here if needed.

tl;dr: without the Tiptap fix, @hover hits the same underlying issue and causes to disappear.

@solidprinciples
Copy link
Contributor Author

solidprinciples commented Jan 28, 2026

From a relevancy standpoint, the changes in this PR are small, closely related, and all part of making the documented behavior function correctly. (@node-changed was documented what @hover is here).

Happy to remove middleware changes as that's more of a feature than a fix and very barebones at that. All in this would then just add/fix:

  • fix(editor): pass tiptap nested drag handle props
  • feat(editor): add @hover emit - triggered when the drag handle hovered node changes

@solidprinciples solidprinciples changed the title feat(EditorDragHandle): pass tiptap props, custom middleware, fix onNodeChange emit feat(EditorDragHandle): pass tiptap nested handle props, emit hover event Jan 28, 2026
@solidprinciples solidprinciples changed the title feat(EditorDragHandle): pass tiptap nested handle props, emit hover event feat(EditorDragHandle): emit hover event, fix passing tiptap props Jan 28, 2026
:on-node-change="onNodeChange"
data-slot="root"
:class="ui.root({ class: [props.ui?.root, props.class] })"
@click="onClick"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It didn't seem like this did anything - since we pass to the <UButton /> below, unless I am missing something. Otherwise nearing ready for review @benjamincanac

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/runtime/components/EditorDragHandle.vue`:
- Around line 41-43: The public API was changed by adding a new emit named
"hover" to EditorDragHandleEmits in EditorDragHandle.vue; update the public docs
and changelog to document this new event, including its signature (hover: [{
node: JSONContent, pos: number }]), when it is emitted, expected payload shape,
and any behavioral notes or migration guidance for consumers; ensure README/API
reference that lists component emits and the changelog entry for this release
mention the new hover event and its purpose so consumers are aware of the
behavior change.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
src/runtime/components/EditorDragHandle.vue (1)

154-164: Default UButton missing @click binding — nodeChange won't emit on click.

The slot exposes onClick via :on-click, but the default UButton doesn't bind a click handler. Since the root-level @click was removed (per AI summary), clicking the default drag handle button won't call onClick, breaking the nodeChange emission.

🐛 Proposed fix to bind the click handler on the default button
       <UButton
         v-bind="{
           ...buttonProps,
           icon: props.icon || appConfig.ui.icons.drag,
           ...$attrs
         }"
+        `@click`="onClick"
         data-slot="handle"
         :class="ui.handle({ class: [props.ui?.handle, props.class] })"
         :ui="transformUI(ui, props.ui)"
       />

Copy link
Member

@benjamincanac benjamincanac left a comment

Choose a reason for hiding this comment

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

@solidprinciples Let me know if you still have work to do on this, otherwise looks good!

@solidprinciples
Copy link
Contributor Author

@solidprinciples Let me know if you still have work to do on this, otherwise looks good!

@benjamincanac - tested, all good from my end. do I can wake up and commit the changelog part using the prompt from coderabbit.

🤖 Fix all issues with AI agents
In `@src/runtime/components/EditorDragHandle.vue`:
- Around line 41-43: The public API was changed by adding a new emit named
"hover" to EditorDragHandleEmits in EditorDragHandle.vue; update the public docs
and changelog to document this new event, including its signature (hover: [{
node: JSONContent, pos: number }]), when it is emitted, expected payload shape,
and any behavioral notes or migration guidance for consumers; ensure README/API
reference that lists component emits and the changelog entry for this release
mention the new hover event and its purpose so consumers are aware of the
behavior change.

@benjamincanac benjamincanac changed the title feat(EditorDragHandle): emit hover event, fix passing tiptap props feat(EditorDragHandle): proxy nested / nestedOptions props and emit hover event Jan 29, 2026
Copy link
Member

It isn't necessary as the docs and changelog are automatic 😊

@benjamincanac benjamincanac merged commit ed60193 into nuxt:v4 Jan 29, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v4 #4488

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants