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
Related
- Drupal core slot schema: drupal!13548 / https://www.drupal.org/project/drupal/issues/3514072
- Demo project: https://github.com/ITCare-Company/ymca_ws_canvas_demo/pull/17
/closes #3563163