Skip to content

Selected nested-slot component loses its canvas overlay after repeated drag-and-drop operations #1782

Description

@dkrasniy

Description

After repeatedly dragging a selected component that contains nested slot fields, the component can remain selected in Puck's global state while its canvas overlay disappears.

The properties panel and breadcrumb continue showing the correct selected component, but Puck no longer renders the selection border or action bar.

This occurs with the latest published Puck version, 0.22.4.

Video:

Screen.Recording.2026-08-05.at.1.22.56.PM.mov

Environment

  • Puck version: 0.22.4
  • Browser version: Chrome desktop (version to add)
  • Additional environment info:
    • React 19.2.4
    • macOS
    • Vite
    • Puck iframe enabled
    • Desktop device

Steps to reproduce

  1. Configure a component with multiple nested slot fields.
import { Puck } from "@puckeditor/core";

const config = {
  components: {
    Text: {
      fields: {
        text: {
          type: "text",
        },
      },
      defaultProps: {
        text: "Text",
      },
      render: ({ text }) => <p>{text}</p>,
    },

    Container: {
      fields: {
        columns: {
          type: "radio",
          options: [
            { label: "1", value: 1 },
            { label: "1 / 2", value: 2 },
            { label: "1 / 3", value: 3 },
          ],
        },
        column1: {
          type: "slot",
        },
        column2: {
          type: "slot",
        },
        column3: {
          type: "slot",
        },
      },

      defaultProps: {
        columns: 3,
        column1: [],
        column2: [],
        column3: [],
      },

      render: ({
        columns,
        column1: Column1,
        column2: Column2,
        column3: Column3,
      }) => {
        const slots = [Column1, Column2, Column3].slice(0, columns);

        return (
          <div
            style={{
              display: "grid",
              gap: 16,
              gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
              minHeight: 120,
            }}
          >
            {slots.map((Slot, index) => (
              <Slot key={`column-${index + 1}`} minEmptyHeight={96} />
            ))}
          </div>
        );
      },
    },
  },
};

const data = {
  root: {
    props: {},
  },
  content: [
    {
      type: "Text",
      props: {
        id: "text-before",
        text: "Before container",
      },
    },
    {
      type: "Container",
      props: {
        id: "container",
        columns: 3,
        column1: [
          {
            type: "Text",
            props: {
              id: "column-1-text",
              text: "Column one",
            },
          },
        ],
        column2: [
          {
            type: "Text",
            props: {
              id: "column-2-text",
              text: "Column two",
            },
          },
        ],
        column3: [],
      },
    },
    {
      type: "Text",
      props: {
        id: "text-after",
        text: "After container",
      },
    },
  ],
};
  1. Render Puck with its iframe enabled.
export function Editor() {
  return (
    <Puck
      config={config}
      data={data}
      iframe={{
        enabled: true,
        syncHostStyles: true,
      }}
    />
  );
}
  1. Select the Container.

  2. Drag the Container above and below the other root components repeatedly.

  3. After several drag-and-drop operations, click the Container again.

The issue is intermittent but becomes easier to reproduce after repeatedly moving the nested-slot component.

What happens

The Container remains selected in Puck's state:

selector: {"index":0,"zone":"root:default-zone"}
selectedId: Container-5424743b-8fb8-4d99-8c2a-d407602d0459
puckDragging: false
componentConnected: true
overlayCount: 0
selectedOverlayMounted: false
emptyAnimatingDropZones: 0
dndDraggingElements: 0

The following UI continues working:

  • The properties panel displays the Container fields.
  • The breadcrumb displays the Container as selected.
  • The selected component exists in the canvas DOM.

However:

  • No [data-puck-overlay] element is mounted.
  • The selection border is missing.
  • The duplicate/delete action bar is missing.
  • Clicking the Container again does not restore the overlay.

What I expect to happen

When Puck's global drag state is finished and the component remains selected:

  • The selection overlay should be mounted.
  • The selection border should be visible.
  • The action bar should be visible.
  • Global and local drag state should remain synchronized.

Suspected cause

DraggableComponent only mounts its overlay when both local conditions are true:

dragFinished && isVisible && createPortal(...)

After repeated nested-zone drag operations, the component's local dragFinished state can remain false, even though Puck's global state reports:

state.ui.isDragging === false

The properties panel reads the global selected item, which explains why it continues working while the local overlay remains unmounted.

Proposed fix

Reconcile the local dragFinished state when Puck's authoritative global drag state finishes:

const globalDragFinished = useAppStore(
  (state) => !state.state.ui.isDragging
);

useEffect(() => {
  if (globalDragFinished) {
    sync();
    setDragFinished(true);
  }
}, [globalDragFinished, sync]);

Alternatively, remove the duplicated local drag-finished state and derive overlay eligibility directly from Puck's global drag state, if compatible with the drop animation lifecycle.

A regression test should repeatedly move a selected nested-slot component and verify that:

expect(state.ui.isDragging).toBe(false);
expect(selectedItem.props.id).toBe("container");
expect(document.querySelector("[data-puck-overlay]")).not.toBeNull();

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions