Skip to content
D
Documentation

NodeToolbarComponent

reference
2 min readUpdated

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

NameTypeDefaultDescription
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

  • actionClicked
  • visibilityChanged
  • positionUpdated

Properties

NameTypeDefaultDescription
nodeNodeModel
engineDiagramEngine
canvasElement?HTMLElement
viewport{ x: number; y: number; width: number; height: number }
zoomnumber1.0
positionToolbarPosition'top'
alignmentToolbarAlignment'center'
offsetnumber8
actionsToolbarAction[][]
customTemplate?TemplateRef<any>
visiblebooleantrue
styleConfig?ToolbarStyleConfig
enableAnimationbooleantrue
autoHidebooleanfalse
config?NodeToolbarConfig
actionClicked
visibilityChanged
positionUpdated
toolbarRef?ElementRef<HTMLDivElement>
isVisiblefalse
transform''
focusedActionIndex0

Methods

  • constructor(private cdr: ChangeDetectorRef, private elementRef: ElementRef)
  • get effectiveConfig(): EffectiveToolbarConfig — Effective configuration merging individual inputs with config object
  • get visibleActions(): ToolbarAction[] — Get visible actions based on visibility conditions
  • get 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 layout
  • ngOnInit()
  • ngOnChanges(changes: SimpleChanges)
  • ngOnDestroy()
  • updatePosition() — Update toolbar position with error handling
  • handleActionClick(action: ToolbarAction) — Handle action click
  • handleKeyDown(event: KeyboardEvent) — Handle keyboard navigation
  • onActionFocus(groupIdx: number, actionIdx: number) — Handle action focus
  • calculateTabIndex(groupIdx: number, actionIdx: number): number — Calculate tabindex for group-aware navigation
  • show() — Show toolbar
  • hide() — Hide toolbar
  • toggle() — Toggle visibility

Example

html
<grafloria-node-toolbar
  [node]="selectedNode"
  [engine]="diagramEngine"
  [config]="toolbarConfig">
</grafloria-node-toolbar>

Was this page helpful?

NodeToolbarComponent — Grafloria