Reordering TabStrip Tabs with Drag and Drop
Environment
| Product | Progress® Kendo UI for Angular TabStrip |
Description
How can I enable drag and drop functionality to reorder tabs in the Kendo Angular TabStrip? I want users to move tabs within the TabStrip by dragging them with a handle icon.
This Knowledge Base article also answers the following questions:
- How to implement tab reordering in the Kendo UI for Angular TabStrip?
- Is it possible to drag and drop tabs in the Kendo UI for Angular TabStrip?
- How can I use the Kendo Drag and Drop directives with the Kendo UI for Angular TabStrip?
Solution
The Kendo UI for Angular TabStrip does not have built-in support for tab reordering via drag and drop. However, you can achieve this functionality by integrating the Kendo Drag and Drop directives with the TabStrip component.
The following example demonstrates tab reordering using the Drag and Drop utility package.
To implement tab reordering, follow these steps:
-
Wrap the TabStrip in a container element and apply the
kendoDragTargetContainerandkendoDropTargetContainerdirectives.html<div #wrapper kendoDragTargetContainer kendoDropTargetContainer dragTargetFilter=".k-tabstrip-item" dropTargetFilter=".k-tabstrip-item" [dragData]="dragData" [hint]="{ hintClass: 'tabHint' }" (onDrop)="onDrop($event)" (onDragOver)="onDragOver($event)" (onDragLeave)="clearDropIndicators()" dragHandle=".reorder-icon" > <kendo-tabstrip> <!-- tabs --> </kendo-tabstrip> </div> -
Add a custom drag handle icon to each tab by implementing a custom template using the
kendoTabTitledirective. Include a reorder icon from the SVG icons package that will serve as the drag handle.html@for (tab of tabs; track $index) { <kendo-tabstrip-tab [title]="tab.title" [selected]="tab.selected"> <ng-template kendoTabTitle> <kendo-svgicon class="reorder-icon" [icon]="reorderSVG"></kendo-svgicon> <span class="tab-title">{{ tab.title }}</span> </ng-template> <ng-template kendoTabContent> <p>{{ tab.content }}</p> </ng-template> </kendo-tabstrip-tab> } -
Track tab positions by adding a custom data attribute to each tab element using the
AfterViewInitlifecycle hook. These attributes help identify the source and destination tabs during drag and drop operations.typescriptprivate readonly TAB_INDEX_ATTR = 'data-kendo-tab-index'; @ViewChild('wrapper', { read: ElementRef }) wrapper!: ElementRef; ngAfterViewInit(): void { this.updateTabIndices(); } private updateTabIndices(): void { setTimeout(() => { const tabItems = this.wrapper.nativeElement.querySelectorAll('.k-tabstrip-item'); tabItems.forEach((item: HTMLElement, index: number) => { item.setAttribute(this.TAB_INDEX_ATTR, index.toString()); }); }); } -
Define a
dragDatafunction to provide custom data for the events of theDropTargetContainerDirective. In this function, retrieve thedata-kendo-tab-indexattribute from the drag target and return an object with the source tab index.typescriptpublic dragData = ({ dragTarget }: { dragTarget: HTMLElement }) => { if (dragTarget instanceof HTMLLIElement) { const index = dragTarget.getAttribute(this.TAB_INDEX_ATTR); if (index !== null) { return { fromIndex: +index }; } } return null; }; -
Provide visual feedback during drag operations by handling the
onDragOverevent. In this example, we add CSS classes to indicate whether the tab will be dropped before or after the target tab based on the cursor position.typescriptprivate readonly DROP_INDICATOR_BEFORE = 'drop-indicator-before'; private readonly DROP_INDICATOR_AFTER = 'drop-indicator-after'; public onDragOver(e: DropTargetEvent): void { if (!e.dragData || !(e.dropTarget instanceof HTMLLIElement)) { return; } this.clearDropIndicators(); const isDropAfter = this.isDropAfterMiddle(e.dropTarget, e.dragEvent.clientX); e.dropTarget.classList.add( isDropAfter ? this.DROP_INDICATOR_AFTER : this.DROP_INDICATOR_BEFORE ); } private isDropAfterMiddle(element: HTMLElement, clientX: number): boolean { const rect = element.getBoundingClientRect(); const middle = rect.left + rect.width / 2; return clientX >= middle; } -
Handle the
onDropevent to perform the actual tab reordering. Calculate the correct destination index based on the drag direction and cursor position.typescriptpublic onDrop(e: DropTargetEvent): void { this.clearDropIndicators(); if (!e.dragData || !(e.dragTarget instanceof HTMLLIElement) || !(e.dropTarget instanceof HTMLLIElement)) { return; } const { fromIndex } = e.dragData; const toIndexAttr = e.dropTarget.getAttribute(this.TAB_INDEX_ATTR); if (toIndexAttr === null) { return; } const toIndex = +toIndexAttr; const destinationIndex = this.calculateDestinationIndex( fromIndex, toIndex, e.dropTarget, e.dragEvent.clientX ); if (destinationIndex === null) { return; } this.reorderTabs(fromIndex, destinationIndex); } -
Update the tabs array by removing the dragged tab from its original position and inserting it at the calculated destination index. Then, update the DOM indices of the tabs and notify the Drag and Drop directives to synchronize their internal state with the updated DOM.
typescript@ViewChild('wrapper', { read: DragTargetContainerDirective }) dragTargetContainer!: DragTargetContainerDirective; @ViewChild('wrapper', { read: DropTargetContainerDirective }) dropTargetContainer!: DropTargetContainerDirective; private reorderTabs(fromIndex: number, destinationIndex: number): void { const [movedTab] = this.tabs.splice(fromIndex, 1); this.tabs.splice(destinationIndex, 0, movedTab); this.tabs = [...this.tabs]; this.updateTabIndices(); this.dragTargetContainer.notify(); this.dropTargetContainer.notify(); }