The second kind of group: when a new container class breaks selection
TLDR
Hardcoding DOM selection walks to look for a single container class (.domain-symbol) breaks when new container types are introduced. When our team added user-created selection groups (<g id="group_...">), single clicks on grouped elements selected individual child paths instead of the parent group container. Resolving this required creating a unified resolveSelectionTarget() helper that traverses ancestor trees up to the outermost valid container.
| Selection Walk Strategy | Handling for Palette Symbols (.domain-symbol) | Handling for User Groups (<g id="group_...">) | Click Result |
|---|---|---|---|
| Hardcoded Inlined Selector | Selected group container | Ignored (Selected individual child) | Broken for user groups |
| Unified Target Resolver | Selected outermost container | Selected outermost container | 100% Consistent Target Selection |
Problem statement: the inlined selection walk defect
Our editor's palette instantiated multi-element symbols inside container groups with class .domain-symbol.
Selection logic across mouse handlers used an inlined closest() DOM walk to ensure clicking any sub-element selected the parent symbol:
// INLINED IMPLEMENTATION: Hardcoded to palette symbols
let selectionTarget = clickedElement.closest('.domain-symbol');
if (selectionTarget) clickedElement = selectionTarget;
Later, we implemented a user grouping feature (Ctrl+G) that wrapped selected canvas elements in standard SVG groups: <g id="group_1719823490">.
Because the selection walk checked strictly for .domain-symbol, clicking a user-grouped element ignored the group container and selected the individual clicked child element.
Technical failure mode: fragmented container definitions
The defect escaped initial testing because Ctrl+G worked as expected while elements remained selected.
Only after deselecting the elements and attempting to re-select the group via single click did the broken selection walk manifest:
clickedElement.closest('.domain-symbol')evaluated tonull.- The selection engine treated the click as targeting the raw child path.
- The group container was bypassed completely.
The fix & architecture: unified target resolution engine
We extracted inlined closest() calls into a central resolveSelectionTarget() helper that supports all container types and walks up to the outermost group:
// REFACTORED: Unified Selection Target Resolver
export function resolveSelectionTarget(clickedElement, cameraGroup) {
if (!clickedElement) return null;
// 1. Find nearest valid container match let container = clickedElement.closest('.domain-symbol, g[id^="group_"]'); if (!container) return clickedElement;
// 2. Walk up parent hierarchy to select outermost group wrapper let current = container; let parent = current.parentElement;
while (parent && parent !== cameraGroup && parent.tagName === 'g') { if (parent.classList.contains('domain-symbol') || parent.id?.startsWith('group_')) { current = parent; } parent = parent.parentElement; }
return current; }
Double-Click child piercing
To allow users to select individual elements inside a group (following standard Figma/Draw.io conventions), we reserved double-click for container piercing:// Single-click selects group unit; Double-click pierces to child element
canvas.addEventListener('dblclick', (e) => {
const rawChild = e.target;
selectElement(rawChild, { pierceGroup: true });
});
Rule of thumb: Never inline container DOM selector strings across click handlers. Route all element selection queries through a centralized target resolver.