From d5ab50a3e3a68a1e8847da9d36614cf06edfd9d7 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 16 Jun 2025 11:42:00 +0300 Subject: [PATCH 1/8] fix(action-strip): Assign parent to not destroy on DOM move. --- .../igniteui-angular-elements/src/app/custom-strategy.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.ts index 1907cba1fe7..48b84a4d538 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.ts @@ -114,6 +114,13 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { // ngElementStrategy getter is protected and also has initialization logic, though that should be safe at this point if (parent?.ngElementStrategy) { this.angularParent = parent.ngElementStrategy.angularParent; + + // action strip is reused in row island child grid + // assign parent so it's not destroyed on detach/attach. + if (element.tagName.toLocaleLowerCase() === 'igc-action-strip') { + this.angularParent = (parent.ngElementStrategy as any).componentRef; + } + this.parentElement = new WeakRef(parent); let parentComponentRef = await parent?.ngElementStrategy[ComponentRefKey]; parentInjector = parentComponentRef?.injector; From 60f3c029addb1335274007ff201f9522873f7e85 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 16 Jun 2025 12:05:54 +0300 Subject: [PATCH 2/8] chore(*): Move strip handling after parentComponentRef is populated. --- .../src/app/custom-strategy.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.ts index 48b84a4d538..25aea19dd78 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.ts @@ -115,12 +115,6 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { if (parent?.ngElementStrategy) { this.angularParent = parent.ngElementStrategy.angularParent; - // action strip is reused in row island child grid - // assign parent so it's not destroyed on detach/attach. - if (element.tagName.toLocaleLowerCase() === 'igc-action-strip') { - this.angularParent = (parent.ngElementStrategy as any).componentRef; - } - this.parentElement = new WeakRef(parent); let parentComponentRef = await parent?.ngElementStrategy[ComponentRefKey]; parentInjector = parentComponentRef?.injector; @@ -133,6 +127,12 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { parentComponentRef = await parent?.ngElementStrategy[ComponentRefKey]; parentAnchor = parentComponentRef?.instance.anchor; } + + // action strip is reused for all rows + // assign parent so it's not destroyed on detach/attach. + if (element.tagName.toLocaleLowerCase() === 'igc-action-strip' || configParent.selector === 'igc-action-strip') { + this.angularParent = parentComponentRef; + } } else if ((parent as any)?.__componentRef) { this.angularParent = (parent as any).__componentRef; parentInjector = this.angularParent.injector; From 434a502659de902546ebc5fada76be0e205bfdb7 Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 20 Jun 2025 15:02:24 +0300 Subject: [PATCH 3/8] fix(action-strip): Fix timing issues. --- .../igniteui-angular-elements/src/app/custom-strategy.ts | 6 ------ .../src/lib/action-strip/action-strip.component.ts | 3 +++ .../grid-actions/grid-editing-actions.component.ts | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.ts index 25aea19dd78..bfef980e8a9 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.ts @@ -114,7 +114,6 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { // ngElementStrategy getter is protected and also has initialization logic, though that should be safe at this point if (parent?.ngElementStrategy) { this.angularParent = parent.ngElementStrategy.angularParent; - this.parentElement = new WeakRef(parent); let parentComponentRef = await parent?.ngElementStrategy[ComponentRefKey]; parentInjector = parentComponentRef?.injector; @@ -128,11 +127,6 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { parentAnchor = parentComponentRef?.instance.anchor; } - // action strip is reused for all rows - // assign parent so it's not destroyed on detach/attach. - if (element.tagName.toLocaleLowerCase() === 'igc-action-strip' || configParent.selector === 'igc-action-strip') { - this.angularParent = parentComponentRef; - } } else if ((parent as any)?.__componentRef) { this.angularParent = (parent as any).__componentRef; parentInjector = this.angularParent.injector; diff --git a/projects/igniteui-angular/src/lib/action-strip/action-strip.component.ts b/projects/igniteui-angular/src/lib/action-strip/action-strip.component.ts index 56bfdf2c6f4..5fbff8c4973 100644 --- a/projects/igniteui-angular/src/lib/action-strip/action-strip.component.ts +++ b/projects/igniteui-angular/src/lib/action-strip/action-strip.component.ts @@ -279,6 +279,9 @@ export class IgxActionStripComponent implements IgxActionStripToken, AfterConten * ``` */ public show(context?: any): void { + if(!this._originalParent) { + this._originalParent = this._viewContainer.element.nativeElement?.parentElement; + } this.hidden = false; if (!context) { return; diff --git a/projects/igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component.ts b/projects/igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component.ts index e48ec8745f2..0f35205cb54 100644 --- a/projects/igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component.ts +++ b/projects/igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component.ts @@ -147,7 +147,7 @@ export class IgxGridEditingActionsComponent extends IgxGridActionsBaseDirective const context = this.strip.context; const grid = context.grid; grid.deleteRow(context.key); - + this.grid.cdr.detectChanges(); this.strip.hide(); } From f16962157db55206e884aa0b60dafb8256916d7c Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 9 Oct 2025 16:00:38 +0300 Subject: [PATCH 4/8] fix(igxGridRow): On destroy check for still shown action strip. --- .../src/lib/action-strip/action-strip.component.ts | 3 --- .../grid-actions/grid-editing-actions.component.ts | 1 - projects/igniteui-angular/src/lib/grids/row.directive.ts | 4 ++++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/igniteui-angular/src/lib/action-strip/action-strip.component.ts b/projects/igniteui-angular/src/lib/action-strip/action-strip.component.ts index 5fbff8c4973..56bfdf2c6f4 100644 --- a/projects/igniteui-angular/src/lib/action-strip/action-strip.component.ts +++ b/projects/igniteui-angular/src/lib/action-strip/action-strip.component.ts @@ -279,9 +279,6 @@ export class IgxActionStripComponent implements IgxActionStripToken, AfterConten * ``` */ public show(context?: any): void { - if(!this._originalParent) { - this._originalParent = this._viewContainer.element.nativeElement?.parentElement; - } this.hidden = false; if (!context) { return; diff --git a/projects/igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component.ts b/projects/igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component.ts index 0f35205cb54..4207450cb45 100644 --- a/projects/igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component.ts +++ b/projects/igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component.ts @@ -147,7 +147,6 @@ export class IgxGridEditingActionsComponent extends IgxGridActionsBaseDirective const context = this.strip.context; const grid = context.grid; grid.deleteRow(context.key); - this.grid.cdr.detectChanges(); this.strip.hide(); } diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index b2118b1f15e..18faef7484b 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -458,6 +458,10 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { * @internal */ public ngOnDestroy() { + // if action strip is shown here but row is about to be destroyed, hide it. + if (this.grid.actionStrip && this.grid.actionStrip.context === this) { + this.grid.actionStrip.hide(); + } this.destroy$.next(true); this.destroy$.complete(); } From e9c4b6885e96512fe6941e274c9dd60d5e96c510 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 13 Oct 2025 17:57:18 +0300 Subject: [PATCH 5/8] fix(igxGrid): On detach also check for still shown action strip. --- .../igniteui-angular/src/lib/grids/grid-base.directive.ts | 6 ++++++ .../igniteui-angular/src/lib/grids/grid/grid.component.html | 1 + .../hierarchical-grid/hierarchical-grid.component.html | 2 +- .../src/lib/grids/tree-grid/tree-grid.component.html | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts index 8bd23768c0b..8e9b2020575 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -6205,6 +6205,12 @@ export abstract class IgxGridBaseDirective implements GridType, } } + protected viewDetachHandler(args) { + if (this.actionStrip && args.view.rootNodes.find(x => x === this.actionStrip.context.element.nativeElement)) { + this.actionStrip.hide(); + } + } + /** * @hidden @internal */ diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html index f20ed0502ab..ed959819058 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -90,6 +90,7 @@ [igxTemplateOutletContext]="getContext(rowData, rowIndex)" (cachedViewLoaded)="cachedViewLoaded($event)" (viewCreated)="viewCreatedHandler($event)" + (beforeViewDetach)="viewDetachHandler($event)" (viewMoved)="viewMovedHandler($event)"> diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html index 8cd4c1eea35..458067138e9 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html @@ -68,7 +68,7 @@ + (viewMoved)="viewMovedHandler($event)" (cachedViewLoaded)="cachedViewLoaded($event)" (beforeViewDetach)="viewDetachHandler($event)"> diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html index 17bc02196b6..b82306c2bd3 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html @@ -71,6 +71,7 @@ (dataChanging)="dataRebinding($event)" (dataChanged)="dataRebound($event)"> From c83b89ac60b825872d1b1c1316e15e5b2a724f1a Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 13 Oct 2025 18:07:30 +0300 Subject: [PATCH 6/8] chore(*): Add null check. --- projects/igniteui-angular/src/lib/grids/grid-base.directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts index 8e9b2020575..42e25b657bc 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -6206,7 +6206,7 @@ export abstract class IgxGridBaseDirective implements GridType, } protected viewDetachHandler(args) { - if (this.actionStrip && args.view.rootNodes.find(x => x === this.actionStrip.context.element.nativeElement)) { + if (this.actionStrip && args.view.rootNodes.find(x => x === this.actionStrip.context?.element.nativeElement)) { this.actionStrip.hide(); } } From f38979e8739b77824a1c2a01ec3a65bdb141fa2c Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 23 Oct 2025 11:49:40 +0300 Subject: [PATCH 7/8] chore(*): Add angular tests. --- .../grid-editing-actions.component.spec.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/projects/igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component.spec.ts b/projects/igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component.spec.ts index 4a86a58fdf4..34750256cd2 100644 --- a/projects/igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component.spec.ts +++ b/projects/igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component.spec.ts @@ -15,6 +15,7 @@ import { IgxGridPinningActionsComponent } from './grid-pinning-actions.component import { IgxActionStripComponent } from '../action-strip.component'; import { IRowDataCancelableEventArgs, IgxColumnComponent } from '../../grids/public_api'; import { SampleTestData } from '../../test-utils/sample-test-data.spec'; +import { SortingDirection } from '../../data-operations/sorting-strategy'; describe('igxGridEditingActions #grid ', () => { let fixture; @@ -274,6 +275,59 @@ describe('igxGridEditingActions #grid ', () => { expect(actionStrip.hidden).toBeTrue(); }); + + it('should auto-hide on delete action click.', () => { + const row = grid.rowList.toArray()[0]; + actionStrip.show(row); + fixture.detectChanges(); + + expect(actionStrip.hidden).toBeFalse(); + + const deleteIcon = fixture.debugElement.queryAll(By.css(`igx-grid-editing-actions igx-icon`))[1]; + expect(deleteIcon.nativeElement.innerText).toBe('delete'); + deleteIcon.parent.triggerEventHandler('click', new Event('click')); + fixture.detectChanges(); + + expect(actionStrip.hidden).toBeTrue(); + + }); + + it('should auto-hide if context row is destroyed.', () => { + const row = grid.rowList.toArray()[0]; + actionStrip.show(row); + fixture.detectChanges(); + + expect(actionStrip.hidden).toBeFalse(); + + // bind to no data, which removes all rows. + grid.data = []; + grid.cdr.detectChanges(); + + expect((row.cdr as any).destroyed).toBeTrue(); + expect(actionStrip.hidden).toBeTrue(); + }); + + it('should auto-hide if context row is cached.', () => { + // create group rows + grid.groupBy({ fieldName: 'ContactTitle', dir: SortingDirection.Desc, ignoreCase: false }); + fixture.detectChanges(); + + // show for first data row + const row = grid.dataRowList.toArray()[0]; + actionStrip.show(row); + fixture.detectChanges(); + + // collapse all groups to cache data rows + grid.toggleAllGroupRows(); + fixture.detectChanges(); + + // not destroyed, but not in DOM anymore + expect((row.cdr as any).destroyed).toBeFalse(); + expect(row.element.nativeElement.isConnected).toBe(false); + + // action strip should be hidden + expect(actionStrip.hidden).toBeTrue(); + }); }); describe('auto show/hide in HierarchicalGrid', () => { From 59df19be8bb22ac8048dd4e0bc50f3cb596efdd6 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 23 Oct 2025 12:17:29 +0300 Subject: [PATCH 8/8] chore(*): Add elements test for actionstrip. --- .../src/app/custom-strategy.spec.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts index 2a6bd5dc461..b269289efe7 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts @@ -11,6 +11,9 @@ import { IgcColumnComponent, IgcPaginatorComponent, IgcGridStateComponent, + IgcColumnLayoutComponent, + IgcActionStripComponent, + IgcGridEditingActionsComponent, } from './components'; import { defineComponents } from '../utils/register'; @@ -25,6 +28,8 @@ describe('Elements: ', () => { IgcColumnComponent, IgcPaginatorComponent, IgcGridStateComponent, + IgcActionStripComponent, + IgcGridEditingActionsComponent ); }); @@ -181,5 +186,66 @@ describe('Elements: ', () => { await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); expect(() => stateComponent.getStateAsString()).not.toThrow(); }); + + it('should not destroy action strip when row it is shown in is destroyed or cached.', async() => { + const innerHtml = ` + + + + + `; + testContainer.innerHTML = innerHtml; + + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + + const grid = document.querySelector>('#testGrid'); + const actionStrip = document.querySelector>('#testStrip'); + grid.data = SampleTestData.foodProductData(); + + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + + let row = grid.dataRowList.toArray()[0]; + actionStrip.show(row); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + + expect(actionStrip.hidden).toBeFalse(); + + grid.data = []; + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + + // row destroyed + expect((row.cdr as any).destroyed).toBeTrue(); + // action strip still in DOM, only hidden. + expect(actionStrip.hidden).toBeTrue(); + expect(actionStrip.isConnected).toBeTrue(); + + grid.data = SampleTestData.foodProductData(); + grid.groupBy({ fieldName: 'InStock', dir: 1, ignoreCase: false }); + + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + + row = grid.dataRowList.toArray()[0]; + actionStrip.show(row); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + + expect(actionStrip.hidden).toBeFalse(); + + // collapse all data rows, leave only groups + grid.toggleAllGroupRows(); + + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + + // row not destroyed, but also not in dom anymore + expect((row.cdr as any).destroyed).toBeFalse(); + expect(row.element.nativeElement.isConnected).toBe(false); + + // action strip still in DOM, only hidden. + expect(actionStrip.hidden).toBeTrue(); + expect(actionStrip.isConnected).toBeTrue(); + }); }); });