Add slot allowedComponents UI support for parent-child component restrictions

Summary

Implements UI and backend support for slot component restrictions in Canvas, enabling parent components (Organisms) to define which child components (Molecules) can be placed in their slots.

Supports both direct component IDs and tag-based matching, aligning with Drupal core #3514072 / MR !13548.

Note

This implementation aligns with the proposed Drupal core slot schema from drupal!13548

How it works

In component.yml, slots can declare expected components using IDs or tags:

slots:
  items:
    title: Card Items
    expected:
      # Direct component ID (contains a dot)
      - sdc.my_theme.card-item
      # Tag-based match (no dot) — matches all components with this tag
      - card-content

Components can declare tags:

tags:
  - card-content
  - interactive

Resolution logic: entries with . are treated as component IDs (direct match), entries without . are tags (matched against metadata.tags of all components).

Slot Properties Support

Property Status Description
expected Supported Array of component IDs or tags
allowedComponents Supported Legacy property name (fallback)
minItems Future Min children count
maxItems Future Max children count

Changes

PHP (backend)

File Change
config/schema/canvas.schema.yml Add expected and allowedComponents sequences to canvas.slot_definition
src/Entity/Component.php Whitelist expected and allowedComponents in cleanSlotDefinition()

UI — New files

File Purpose
slot-utils.ts resolveExpectedToComponentIds() / getSlotAllowedComponentIds() — resolve IDs and tags to concrete component IDs
useComponentHierarchy.ts Build parent-child tree from slot expected/allowedComponents; filter children from flat list
useIsDropAllowed.ts useIsDropAllowed() — validate drag target against slot restrictions; useDraggedItemInfo() — detect restricted (Molecule) components
useSlotFilteredComponents.ts Filter component library panel to show only components allowed in the targeted slot
HierarchicalListItem.tsx Collapsible list item that nests child components under their parent Organism

UI — Modified files

File Change
ComponentList.tsx Integrate hierarchy hooks; render HierarchicalListItem; use slot-filtered components
List.module.css Hierarchical item styles (chevron trigger, spacer, indented children)
ComponentOverlay.tsx Pass parentComponentType prop through to ComponentDropZone
SlotOverlay.tsx Pass parentComponentType to ComponentOverlay
ComponentDropZone.tsx Use useIsDropAllowed for visual feedback; remove origin-based disabled (keep only self-drop check); pass parentComponentType in droppable data
SlotDropZone.tsx Gate isOver highlight on isDropAllowed; pass parentComponentType in droppable data
EmptySlotDropZone.tsx Remove disabled prop — always registered for collision detection; gate highlight on isDropAllowed
EmptyRegionDropZone.tsx Remove disabled prop; gate highlight on !hasSlotRestrictions (restricted Molecules cannot drop on regions)
RegionDropZone.tsx Remove disabled prop; gate highlight on !hasSlotRestrictions
DragEventsHandler.tsx Add checkDropAllowed() — validates slot restrictions for all origins (library, overlay, layers); applies not-allowed cursor via body class; defense-in-depth for region drops when over resolves to RegionDropZone instead of EmptySlotDropZone
DragOverlay.module.css Add .notAllowed cursor override and .notAllowedOverlay red background
PreviewOverlay.module.css Empty slot drop zone text handling for narrow containers; visual feedback for disallowed state
uiSlice.ts Add targetSlotInfo state (slotId + parentComponentType) and selectors for slot-aware library filtering

Why disabled was removed from drop zones

dnd-kit v6.3.1 measures droppable rects only for enabled containers when a drag starts. When disabled starts true and flips to false mid-drag (via useEffect), the container misses the initial measurement pass — its rect is not available for collision detection until the next frame. By then overId is already locked to another target (typically RegionDropZone). Keeping disabled: false from mount ensures all containers are measured in the first pass. Drop validation is handled by checkDropAllowed() in DragEventsHandler and useIsDropAllowed in each drop zone.

Addressing feedback from #3563163

Concern (Wim Leers) Resolution
Per-component enumeration insufficient Tag matching: expected: [card-content] matches all components with that tag
Must align with upstream core Uses expected as primary property, falls back to allowedComponents
Avoid enumerating all possible components Tags solve this — one tag matches many components

Demo

https://youtu.be/QT4cPzMBTr4

/closes #3563163

Edited by Andrii Podanenko

Merge request reports

Loading