feat: add optional drag handle to component action bar - #1770
Conversation
Add an opt-in drag handle to the component action bar, so components can be picked up by a small grip icon instead of only by dragging the component itself. Helps with slots that wrap tightly around nested content and are hard to grab. Enabled via dnd.enableDragHandle, off by default. The handle is passed to custom action bars as a dragHandle prop on the actionBar override, the same way parentAction is, and respects permissions.drag. Dragging the component body still works. Registering a handle would otherwise make dnd-kit only start drags from that handle, so useSensors accepts an activatorElements option and DragDropContext registers both the body and the handle. Closes puckeditor#1477
|
@dkrasniy is attempting to deploy a commit to the Puck Team on Vercel. A member of the Team first needs to authorize it. |
…sniy/puck into feat/action-bar-drag-handle
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Hey @dkrasniy! Thank you very much for the contribution 🙏 Played with this and it looks great! I think we want to merge it, and want to do a proper code review, but before that we're keen to merge #1735 since it introduces some new dnd behavior that might play nicely with this. In the meantime, if you could rebase from main it would be great as it would pick up the fix for the build failure you're facing. |
📝 WalkthroughWalkthroughAdds ChangesAction bar drag handle
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant DraggableComponent
participant DefaultActionBar
participant ActionBarDragHandle
participant DragDropContextClient
participant useSensors
participant PointerSensor
DragDropContextClient->>useSensors: Pass activatorElements
useSensors->>PointerSensor: Configure activatorElements
DraggableComponent->>DefaultActionBar: Pass dragHandle
DefaultActionBar->>ActionBarDragHandle: Render handle
ActionBarDragHandle->>PointerSensor: Initiate dragging
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
apps/docs/pages/docs/api-reference/components/_meta.jsOops! Something went wrong! :( ESLint: 10.8.0 TypeError: scopeManager.addGlobals is not a function packages/core/components/ActionBar/__tests__/index.spec.tsxOops! Something went wrong! :( ESLint: 9.39.4 YAMLException: Cannot read config file: /packages/eslint-config-custom/index.mjs 7 | ... packages/core/components/ActionBar/index.tsxOops! Something went wrong! :( ESLint: 9.39.4 YAMLException: Cannot read config file: /packages/eslint-config-custom/index.mjs 7 | ...
Comment Warning |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/core/components/DraggableComponent/index.tsx (1)
57-65: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd integration coverage for drag-handle activation.
Test
dnd.enableDragHandlewith a pointer drag fromActionBar.DragHandle, verify that the component moves, and verify that a user withoutpermissions.dragcannot start a drag.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/components/DraggableComponent/index.tsx` around lines 57 - 65, Add integration coverage for the draggable component using dnd.enableDragHandle: perform a pointer drag beginning at ActionBar.DragHandle and assert that the component moves, then verify a user lacking permissions.drag cannot initiate the drag.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/docs/pages/docs/api-reference/components/action-bar-drag-handle.mdx`:
- Around line 14-18: Update the copied example around ActionBar.Group so
dragHandle is declared through the actionBar override callback parameter, using
the actionBar: ({ dragHandle }) => ... context; preserve the existing rendered
structure.
In `@apps/docs/pages/docs/api-reference/overrides/action-bar.mdx`:
- Line 26: Update the dragHandle table link to use the lowercase `#draghandle`
fragment so it matches Nextra’s generated heading anchor.
---
Nitpick comments:
In `@packages/core/components/DraggableComponent/index.tsx`:
- Around line 57-65: Add integration coverage for the draggable component using
dnd.enableDragHandle: perform a pointer drag beginning at ActionBar.DragHandle
and assert that the component moves, then verify a user lacking permissions.drag
cannot initiate the drag.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a6d6bbf3-c8ba-43da-b63f-5167ebf476ad
📒 Files selected for processing (15)
apps/docs/pages/docs/api-reference/components.mdxapps/docs/pages/docs/api-reference/components/_meta.jsapps/docs/pages/docs/api-reference/components/action-bar-drag-handle.mdxapps/docs/pages/docs/api-reference/components/puck.mdxapps/docs/pages/docs/api-reference/dictionary.mdxapps/docs/pages/docs/api-reference/overrides/action-bar.mdxpackages/core/components/ActionBar/__tests__/index.spec.tsxpackages/core/components/ActionBar/index.tsxpackages/core/components/ActionBar/styles.module.csspackages/core/components/DragDropContext/index.tsxpackages/core/components/DraggableComponent/index.tsxpackages/core/lib/dictionary.tspackages/core/lib/dnd/use-sensors.tspackages/core/types/API/Overrides.tspackages/core/types/API/index.ts
| ```tsx showLineNumbers {2} copy | ||
| <ActionBar> | ||
| <ActionBar.Group>{dragHandle}</ActionBar.Group> | ||
| </ActionBar> | ||
| ``` |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Declare dragHandle in the copied example.
The snippet uses dragHandle without a parameter or declaration. If a reader copies it into an override, the snippet fails with dragHandle is not defined. Show the actionBar: ({ dragHandle }) => ... context, or remove the copy option.
Proposed documentation fix
-<ActionBar>
- <ActionBar.Group>{dragHandle}</ActionBar.Group>
-</ActionBar>
+const overrides = {
+ actionBar: ({ dragHandle }) => (
+ <ActionBar>
+ <ActionBar.Group>{dragHandle}</ActionBar.Group>
+ </ActionBar>
+ ),
+};📝 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.
| ```tsx showLineNumbers {2} copy | |
| <ActionBar> | |
| <ActionBar.Group>{dragHandle}</ActionBar.Group> | |
| </ActionBar> | |
| ``` | |
| const overrides = { | |
| actionBar: ({ dragHandle }) => ( | |
| <ActionBar> | |
| <ActionBar.Group>{dragHandle}</ActionBar.Group> | |
| </ActionBar> | |
| ), | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/docs/pages/docs/api-reference/components/action-bar-drag-handle.mdx`
around lines 14 - 18, Update the copied example around ActionBar.Group so
dragHandle is declared through the actionBar override callback parameter, using
the actionBar: ({ dragHandle }) => ... context; preserve the existing rendered
structure.
| | Prop | Example | Type | | ||
| | ------------------------------- | ---------------- | --------- | | ||
| | [`children`](#children) | `<div />` | ReactNode | | ||
| | [`dragHandle`](#dragHandle) | `<div />` | ReactNode | |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n '`#dragHandle`|`#draghandle`|### `dragHandle`' \
apps/docs/pages/docs/api-reference/overrides/action-bar.mdx \
apps/docs/pages/docs/api-reference/components/action-bar-drag-handle.mdxRepository: puckeditor/puck
Length of output: 721
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- documentation references ---'
cat -n apps/docs/pages/docs/api-reference/overrides/action-bar.mdx | sed -n '20,38p'
cat -n apps/docs/pages/docs/api-reference/components/action-bar-drag-handle.mdx | sed -n '1,16p'
printf '%s\n' '--- Nextra and slugger declarations ---'
rg -n -i '"nextra"|"github-slugger"|slug' \
package.json package-lock.json pnpm-lock.yaml yarn.lock apps/docs 2>/dev/null | head -120Repository: puckeditor/puck
Length of output: 2542
🌐 Web query:
Nextra 3.3.1 heading slug github-slugger default maintainCase heading IDs
💡 Result:
In Nextra 3.3.1, heading IDs are generated to ensure compatibility with how GitHub handles markdown headings, primarily using the github-slugger package [1][2][3]. The github-slugger utility defaults to converting all characters to lowercase when creating slugs [4]. The maintainCase option exists within the github-slugger API (where it defaults to false) to allow preserving the original casing of the heading text [4]. However, Nextra generally follows the default behavior of github-slugger, which aligns with GitHub's convention of using lowercase slugs [1][2][5]. Key points regarding heading slug generation in Nextra: 1. Consistency: Nextra uses github-slugger to ensure that heading IDs are generated consistently and are unique to prevent collisions [1][2]. 2. Custom IDs: Nextra supports custom header IDs via remark-header-ids (allowing syntax like {#custom-id}), which overrides the default slug generation for those specific headings [6][7]. 3. Case Sensitivity: Because the underlying github-slugger defaults to false for maintainCase, headings in Nextra are typically transformed to lowercase [4]. While the option technically exists in the library, Nextra's implementation does not enable maintainCase by default, prioritizing lowercase URLs that match standard GitHub anchor behavior [2][5][4].
Citations:
- 1: use github-slugger for custom heading ids to prevent duplicated headings shuding/nextra#2151
- 2: https://github.com/Flet/github-slugger/
- 3: https://registry.npmjs.org/github-slugger
- 4: https://tessl.io/registry/tessl/npm-github-slugger
- 5: https://github.com/flet/github-slugger
- 6: Support for Custom Header Ids - replaces #553 shuding/nextra#1645
- 7: Fix: Use custom header as slug in search result shuding/nextra#2142
Use the lowercase #draghandle fragment.
Nextra generates lowercase heading fragments, so #dragHandle does not match ### \dragHandle``.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/docs/pages/docs/api-reference/overrides/action-bar.mdx` at line 26,
Update the dragHandle table link to use the lowercase `#draghandle` fragment so it
matches Nextra’s generated heading anchor.
Closes #1477
Description
This PR adds an opt-in drag handle to a component's action bar. This is especially useful for slots that wrap tightly around nested content and are awkward to grab directly (#1477).
The handle is off by default and enabled via the existing
dndprop:<Puck dnd={{ enableDragHandle: true }} />.Following the consideration in the issue, the handle is exposed to custom action bars as an additional
dragHandleprop on theactionBaroverride — mirroring howparentAction(select parent) is passed — so consumers who override the action bar can place or omit it.Non-breaking: whole-component-body dragging still works exactly as before.
Changes made
dnd.enableDragHandle— new opt-in flag, defaultfalse.ActionBar.DragHandle— new export. A grip button that forwards its ref.overrides.actionBar— now gets an optionaldragHandleprop. Existing overrides are unaffected.DraggableComponent— renders the handle at the start of the action bar whenenableDragHandle && permissions.drag.useSensors/DragDropContext— register both the body and the handle as drag activators.action-dragdictionary key ("Drag to reorder"), docs, and tests.Known behavior
The action bar (and therefore the handle) is unmounted while the component it belongs to is being dragged. This is existing behavior from the
dragFinishedgate inDraggableComponent, not something this PR changes. The document-levelgrabbingcursor persists through the drag and the bar reappears at the dropped position. Making the bar track the drag would mean positioning it against dnd-kit's clone rather than the source element, which felt out of scope here.How to test
In
apps/demo, enable the handle:Select a component — a grip handle appears at the left of the action bar. Grab it and drag to reorder the component. It should start dragging immediately, with no press-and-hold delay.
Confirm existing behavior is intact: dragging the component body still reorders it (handle is additive, not a replacement),
and still requires the short press-and-hold.
Set
enableDragHandle: false(or omitdnd) — no handle renders, behavior is unchanged frommain.Set
permissions={{ drag: false }}— the handle does not render.Override the action bar and place
dragHandleyourself to confirm the prop is exposed:Summary by CodeRabbit
New Features
Documentation
Tests