Import it from @grafloria/angular.
NodeToolbar Component
A floating toolbar that attaches to nodes and provides contextual actions. The toolbar automatically positions itself relative to the node and updates its position when the node moves, the canvas zooms/pans, or the window resizes.
Features:
- Smart positioning with boundary detection
- Keyboard navigation and accessibility (WCAG 2.1 Level AA)
- Custom styling with CSS variables
- Error boundaries for robust operation
- Configuration object pattern
ts@Component({
selector: 'grafloria-node-toolbar',
imports: [CommonModule],
template: `
<div
#toolbar
class="grafloria-node-toolbar"
role="toolbar"
[attr.aria-label]="effectiveConfig.ariaLabel || 'Node actions'"
[attr.aria-hidden]="!isVisible"
[class.visible]="isVisible"
[class.animated]="effectiveConfig.animation?.enabled !== false"
[attr.data-animation-preset]="effectiveConfig.animation?.preset || 'fade'"
[style.transform]="transform"
[style.opacity]="isVisible ? 1 : 0"
[style.--toolbar-bg]="effectiveConfig.style?.backgroundColor"
[style.--toolbar-border]="effectiveConfig.style?.borderColor"
[style.--toolbar-radius]="effectiveConfig.style?.borderRadius"
[style.--toolbar-shadow]="effectiveConfig.style?.boxShadow"
[style.--toolbar-padding]="effectiveConfig.style?.padding"
[style.--toolbar-z]="effectiveConfig.style?.zIndex"
[style.--toolbar-transition]="effectiveConfig.animation?.duration || '0.2s'"
[attr.data-position]="effectiveConfig.position"
(keydown)="handleKeyDown($event)"
>
<!-- Default toolbar content (Phase 2: Supports groups) -->
@if (!effectiveConfig.template) {
<div class="toolbar-content" role="group">
@for (group of effectiveGroups; track group.id; let groupIdx = $index) {
<!-- Group label (optional) -->
@if (group.label && useGroupedLayout) {
<div class="toolbar-group-label">{{ group.label }}</div>
}
<!-- Before separator -->
@if (groupIdx > 0 && (group.separator === 'before' || group.separator === 'both')) {
<div class="toolbar-separator" role="separator"></div>
}
<!-- Group actions -->
@for (action of group.actions; track action.id; let idx = $index) {
<button
#actionButton
type="button"
role="button"
class="toolbar-button"
[attr.aria-label]="action.tooltip || action.label"
[attr.aria-disabled]="action.disabled"
[attr.data-action-id]="action.id"
[attr.data-group-id]="group.id"
[attr.tabindex]="calculateTabIndex(groupIdx, idx)"
[disabled]="action.disabled"
[title]="action.tooltip || action.label"
(click)="handleActionClick(action)"
(focus)="onActionFocus(groupIdx, idx)"
>
@if (action.icon) {
<i [class]="action.icon" aria-hidden="true"></i>
}
<span>{{ action.label }}</span>
</button>
}
<!-- After separator -->
@if (groupIdx < effectiveGroups.length - 1 && (group.separator === 'after' || group.separator === 'both')) {
<div class="toolbar-separator" role="separator"></div>
}
}
</div>
}
<!-- Custom template content -->
@if (effectiveConfig.template) {
<ng-container *ngTemplateOutlet="effectiveConfig.template; context: { $implicit: node, actions: visibleActions, config: effectiveConfig }">
</ng-container>
}
</div>
`,
styles: [`
.grafloria-node-toolbar {
position: absolute;
background: var(--toolbar-bg, var(--grafloria-toolbar-bg, white));
border: 1px solid var(--toolbar-border, var(--grafloria-toolbar-border, #e2e8f0));
border-radius: var(--toolbar-radius, var(--grafloria-toolbar-radius, 8px));
box-shadow: var(--toolbar-shadow, var(--grafloria-toolbar-shadow, 0 4px 6px rgba(0, 0, 0, 0.1)));
padding: var(--toolbar-padding, var(--grafloria-toolbar-padding, 4px));
display: flex;
gap: 4px;
z-index: var(--toolbar-z, var(--grafloria-toolbar-z, 1000));
transition: opacity var(--toolbar-transition, 0.2s) ease;
pointer-events: all;
…
Use it as <grafloria-node-toolbar> in a template.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
node? | NodeModel | ||
engine? | DiagramEngine | ||
canvasElement? | HTMLElement | ||
viewport? | { x: number; y: number; width: number; height: number } | ||
zoom? | number | ||
position? | ToolbarPosition | ||
alignment? | ToolbarAlignment | ||
offset? | number | ||
actions? | ToolbarAction[] | ||
customTemplate? | TemplateRef<any> | ||
visible? | boolean | ||
styleConfig? | ToolbarStyleConfig | ||
enableAnimation? | boolean | ||
autoHide? | boolean | ||
config? | NodeToolbarConfig |
Outputs
actionClickedvisibilityChangedpositionUpdated
Properties
| Name | Type | Default | Description |
|---|---|---|---|
node | NodeModel | ||
engine | DiagramEngine | ||
canvasElement? | HTMLElement | ||
viewport | { x: number; y: number; width: number; height: number } | ||
zoom | number | 1.0 | |
position | ToolbarPosition | 'top' | |
alignment | ToolbarAlignment | 'center' | |
offset | number | 8 | |
actions | ToolbarAction[] | [] | |
customTemplate? | TemplateRef<any> | ||
visible | boolean | true | |
styleConfig? | ToolbarStyleConfig | ||
enableAnimation | boolean | true | |
autoHide | boolean | false | |
config? | NodeToolbarConfig | ||
actionClicked | |||
visibilityChanged | |||
positionUpdated | |||
toolbarRef? | ElementRef<HTMLDivElement> | ||
isVisible | false | ||
transform | '' | ||
focusedActionIndex | 0 |
Methods
constructor(private cdr: ChangeDetectorRef, private elementRef: ElementRef)get effectiveConfig(): EffectiveToolbarConfig— Effective configuration merging individual inputs with config objectget visibleActions(): ToolbarAction[]— Get visible actions based on visibility conditionsget effectiveGroups(): ToolbarActionGroup[]— Get effective action groups If actionGroups are provided, use them. Otherwise, convert flat actions into a single group.get useGroupedLayout(): boolean— Check if using grouped layoutngOnInit()ngOnChanges(changes: SimpleChanges)ngOnDestroy()updatePosition()— Update toolbar position with error handlinghandleActionClick(action: ToolbarAction)— Handle action clickhandleKeyDown(event: KeyboardEvent)— Handle keyboard navigationonActionFocus(groupIdx: number, actionIdx: number)— Handle action focuscalculateTabIndex(groupIdx: number, actionIdx: number): number— Calculate tabindex for group-aware navigationshow()— Show toolbarhide()— Hide toolbartoggle()— Toggle visibility
Example
html<grafloria-node-toolbar
[node]="selectedNode"
[engine]="diagramEngine"
[config]="toolbarConfig">
</grafloria-node-toolbar>
Was this page helpful?