From a7c0b994942a32394bff770b59365baf2fdb3f27 Mon Sep 17 00:00:00 2001 From: igdmdimitrov Date: Wed, 7 Oct 2026 13:20:07 +0300 Subject: [PATCH 1/6] fix(snackbar): add type="button" --- CHANGELOG.md | 7 +++++++ .../snackbar/src/snackbar/snackbar.component.html | 2 +- .../snackbar/src/snackbar/snackbar.component.spec.ts | 12 ++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b0b87e9817..89766acbc16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes for each version of this project will be documented in this file. +## 23.0.0 + +### Bug Fixes + +- `IgxSnackbarComponent` + - The default action button now renders with `type="button"`. Previously, when the snackbar was inside a `form` (for example as part of a grid), pressing Enter in an input of the form submitted it. + ## 22.2.1 diff --git a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.html b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.html index 37903af9b6a..6947c30ccf9 100644 --- a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.html +++ b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.html @@ -6,7 +6,7 @@ @if (!customButton.children.length && actionText) { - } diff --git a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.spec.ts b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.spec.ts index 638dc113101..2b23bf161cf 100644 --- a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.spec.ts +++ b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.spec.ts @@ -94,6 +94,18 @@ describe('IgxSnackbar', () => { expect(snackbar.clicked.emit).toHaveBeenCalledWith(snackbar); })); + it('should render the action button with type="button" to prevent form submission', fakeAsync(() => { + snackbar.actionText = 'undo'; + snackbar.displayTime = 100; + + snackbar.open(); + tick(100); + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; + expect(button.getAttribute('type')).toBe('button'); + })); + it('should emit opening when snackbar is shown', fakeAsync(() => { spyOn(snackbar.opening, 'emit'); snackbar.open(); From 81e6176fe37652c7b1921a3770b81c6b1a4a8c73 Mon Sep 17 00:00:00 2001 From: igdmdimitrov Date: Wed, 14 Oct 2026 11:21:29 +0300 Subject: [PATCH 2/6] fix(*): prevent implicit form submission from a grid --- CHANGELOG.md | 6 +- .../grids/grid/src/grid-base.directive.ts | 10 ++ .../grid/src/grid-form-submission.spec.ts | 126 ++++++++++++++++++ .../query-builder-tree.component.html | 2 + .../query-builder.component.spec.ts | 14 ++ 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 projects/igniteui-angular/grids/grid/src/grid-form-submission.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 43854de1073..76385ac7e5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,11 @@ All notable changes for each version of this project will be documented in this ### Bug Fixes - `IgxSnackbarComponent` - - The default action button now renders with `type="button"`. Previously, when the snackbar was inside a `form` (for example as part of a grid), pressing Enter in an input of the form submitted it. + - The default action button now renders with `type="button"`. Previously, when the snackbar was inside a `form` (for example as part of a grid), it became the default button of the form, so pressing Enter in an input of the form triggered the snackbar action instead of the form's submit button. +- `IgxQueryBuilderComponent` + - The add expression and the group (AND/OR) buttons now render with `type="button"`. Previously, clicking them inside a `form` submitted it. +- `IgxGridComponent`, `IgxTreeGridComponent`, `IgxHierarchicalGridComponent`, `IgxPivotGridComponent` + - Pressing Enter in the grid UI, such as the filtering row, cell and row editors, the toolbar column hiding and pinning, Excel-style filtering and advanced filtering, no longer submits a surrounding `form`. ## Unreleased diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts index 8e30248b849..1de080b7aa8 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -3659,6 +3659,16 @@ export abstract class IgxGridBaseDirective implements GridType, */ public _setupListeners() { const destructor = takeUntil(this.destroy$); + // Enter inside the grid must not trigger the implicit submission of a surrounding form. + // Capture phase, since internal handlers (filtering row, editors, dialogs) stop the propagation of key events. + // Buttons, links, textareas and contenteditable elements keep their native Enter behavior. + fromEvent(this.nativeElement, 'keydown', { capture: true }).pipe(destructor).subscribe((event) => { + const target = event.target as HTMLElement; + if (event.key === 'Enter' && !event.isComposing && !(target instanceof HTMLTextAreaElement) && !target.isContentEditable && + !target.closest('button, a[href], input[type=button], input[type=submit], input[type=reset], input[type=image]')) { + event.preventDefault(); + } + }); fromEvent(this.nativeElement, 'focusout').pipe(filter(() => !!this.navigation.activeNode), destructor).subscribe((event) => { const activeNode = this.navigation.activeNode; if (!this.crudService.cell && !!activeNode && diff --git a/projects/igniteui-angular/grids/grid/src/grid-form-submission.spec.ts b/projects/igniteui-angular/grids/grid/src/grid-form-submission.spec.ts new file mode 100644 index 00000000000..ec97dc174d5 --- /dev/null +++ b/projects/igniteui-angular/grids/grid/src/grid-form-submission.spec.ts @@ -0,0 +1,126 @@ +import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { IgxGridComponent } from './grid.component'; +import { IgxColumnComponent, IgxGridToolbarActionsComponent, IgxGridToolbarComponent, IgxGridToolbarHidingComponent } from 'igniteui-angular/grids/core'; +import { IgxInputDirective } from 'igniteui-angular/input-group'; +import { GridFunctions } from '../../../test-utils/grid-functions.spec'; + +const FILTER_UI_ROW = 'igx-grid-filtering-row'; +const UNTYPED_BUTTONS = 'button:not([type]), button[type=submit]'; + +const DATA = [ + { ProductID: 1, ProductName: 'Chai', InStock: true }, + { ProductID: 2, ProductName: 'Aniseed Syrup', InStock: false }, + { ProductID: 3, ProductName: 'Chef Antons Cajun Seasoning', InStock: true } +]; + +/** Dispatches a cancelable Enter keydown, the event whose default action is the implicit form submission. */ +const pressEnter = (target: Element): KeyboardEvent => { + const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }); + target.dispatchEvent(event); + return event; +}; + +describe('IgxGrid - implicit form submission #grid', () => { + let fixture: ComponentFixture; + let grid: IgxGridComponent; + let form: HTMLFormElement; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, GridInFormComponent] + }).compileComponents(); + })); + + beforeEach(fakeAsync(() => { + fixture = TestBed.createComponent(GridInFormComponent); + fixture.detectChanges(); + // the toolbar actions are rendered once the grid has emitted `rendered` + tick(); + fixture.detectChanges(); + grid = fixture.componentInstance.grid; + form = fixture.nativeElement.querySelector('form'); + })); + + it('should not render buttons which can become the default button of the form', () => { + expect(grid.nativeElement.querySelectorAll(UNTYPED_BUTTONS).length).toBe(0); + // e.g. the add row snackbar action button must not take precedence over the form's own submit button + const defaultButton = form.querySelector('button:not([type]), button[type=submit], input[type=submit]'); + expect(defaultButton.id).toBe('submit'); + }); + + it('should prevent implicit submission on Enter in the filtering row input', fakeAsync(() => { + GridFunctions.clickFilterCellChipUI(fixture, 'ProductName'); + tick(); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css(FILTER_UI_ROW)).query(By.directive(IgxInputDirective)); + expect(input.nativeElement.form).toBe(form); + expect(pressEnter(input.nativeElement).defaultPrevented).toBeTrue(); + })); + + it('should prevent implicit submission on Enter in a cell editor', () => { + const cell = grid.getCellByColumn(0, 'ProductName'); + cell.editMode = true; + fixture.detectChanges(); + + const input: HTMLInputElement = grid.gridAPI.get_cell_by_index(0, 'ProductName').nativeElement.querySelector('input'); + expect(input.form).toBe(form); + expect(pressEnter(input).defaultPrevented).toBeTrue(); + }); + + it('should prevent implicit submission on Enter in the toolbar column hiding search input', fakeAsync(() => { + const hidingButton: HTMLButtonElement = fixture.nativeElement.querySelector('igx-grid-toolbar-hiding button'); + hidingButton.click(); + tick(); + fixture.detectChanges(); + + const input: HTMLInputElement = grid.nativeElement.querySelector('igx-column-actions input'); + expect(input.form).toBe(form); + expect(pressEnter(input).defaultPrevented).toBeTrue(); + })); + + it('should not prevent the native Enter activation of buttons inside the grid', () => { + const hidingButton: HTMLButtonElement = fixture.nativeElement.querySelector('igx-grid-toolbar-hiding button'); + expect(pressEnter(hidingButton).defaultPrevented).toBeFalse(); + }); + + it('should not affect implicit submission from inputs outside the grid', () => { + const outerInput: HTMLInputElement = fixture.nativeElement.querySelector('#outerInput'); + expect(pressEnter(outerInput).defaultPrevented).toBeFalse(); + }); +}); + +@Component({ + template: ` +
+ + + + + + + + + + + + +
`, + changeDetection: ChangeDetectionStrategy.Eager, + imports: [ + IgxGridComponent, + IgxColumnComponent, + IgxGridToolbarComponent, + IgxGridToolbarActionsComponent, + IgxGridToolbarHidingComponent + ] +}) +class GridInFormComponent { + @ViewChild('grid', { static: true }) + public grid: IgxGridComponent; + + public data = DATA; +} diff --git a/projects/igniteui-angular/query-builder/src/query-builder/query-builder-tree.component.html b/projects/igniteui-angular/query-builder/src/query-builder/query-builder-tree.component.html index c94e4787d24..54b88f2de15 100644 --- a/projects/igniteui-angular/query-builder/src/query-builder/query-builder-tree.component.html +++ b/projects/igniteui-angular/query-builder/src/query-builder/query-builder-tree.component.html @@ -322,6 +322,7 @@
+ `, + changeDetection: ChangeDetectionStrategy.Eager, + imports: [ + IgxGridComponent, + IgxColumnComponent, + IgxColumnActionsComponent, + IgxColumnHidingDirective, + IgxAdvancedFilteringDialogComponent + ] +}) +class GridComponentsInFormComponent { + @ViewChild('grid', { static: true }) + public grid!: IgxGridComponent; public data = DATA; } diff --git a/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.component.html b/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.component.html index 6323dd02a2e..0c61c47e3af 100644 --- a/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.component.html +++ b/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.component.html @@ -7,6 +7,7 @@ tabindex="0" placeholder="Search" autocomplete="off" + (keydown.enter)="$event.preventDefault()" /> diff --git a/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.spec.ts b/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.spec.ts index 85dce03a386..625a0451935 100644 --- a/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.spec.ts +++ b/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.spec.ts @@ -49,6 +49,16 @@ describe("Pivot data selector #pivotGrid", () => { expect(selector.dims).toEqual([]); expect(selector.values).toEqual([]); }); + + it("should prevent implicit form submission on Enter in its search input", () => { + const fixture = TestBed.createComponent(IgxPivotDataSelectorComponent); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.directive(IgxInputDirective)).nativeElement as HTMLInputElement; + const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }); + input.dispatchEvent(event); + expect(event.defaultPrevented).toBeTrue(); + }); }); describe("Pivot data selector integration #pivotGrid", () => { From 14b7ad58904425652450daaac413bf5637e142e1 Mon Sep 17 00:00:00 2001 From: igdmdimitrov Date: Wed, 21 Oct 2026 15:22:02 +0300 Subject: [PATCH 5/6] chore(*): apply PR review suggestions --- CHANGELOG.md | 2 +- .../grids/core/src/column-actions/column-actions.component.html | 2 +- projects/igniteui-angular/grids/grid/src/grid-base.directive.ts | 2 +- .../grids/pivot-grid/src/pivot-data-selector.component.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c991bc0ddcc..6bb1c3d8461 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes for each version of this project will be documented in this ### Bug Fixes - **Forms** - - `IgxSnackbarComponent` - The default action button now renders with `type="button"`. Previously, when the snackbar was inside a `form` (for example as part of a grid), it became the default button of the form, so pressing Enter in an input of the form triggered the snackbar action instead of the form's submit button. + - `IgxSnackbarComponent` - The default action button now renders with `type="button"`. Previously, when the snackbar was inside a `form` (for example as part of a grid), it became the default button of the form, so pressing Enter in any input of the form clicked it, which triggered the snackbar action and submitted the form. - `IgxQueryBuilderComponent` - The add expression and the group (AND/OR) buttons now render with `type="button"`. Previously, clicking them inside a `form` submitted it. - `IgxGridComponent`, `IgxTreeGridComponent`, `IgxHierarchicalGridComponent`, `IgxPivotGridComponent` - Pressing Enter in the grid UI, such as the filtering row, cell and row editors, the toolbar column hiding and pinning, Excel-style filtering and advanced filtering, no longer submits a surrounding `form`. - `IgxAdvancedFilteringDialogComponent`, `IgxColumnActionsComponent`, `IgxPivotDataSelectorComponent` - Pressing Enter in their inputs no longer submits a surrounding `form` when they are rendered outside of the grid, e.g. used standalone, or the advanced filtering dialog opened with custom overlay settings. diff --git a/projects/igniteui-angular/grids/core/src/column-actions/column-actions.component.html b/projects/igniteui-angular/grids/core/src/column-actions/column-actions.component.html index f6038fbcaa7..897ccd3d433 100644 --- a/projects/igniteui-angular/grids/core/src/column-actions/column-actions.component.html +++ b/projects/igniteui-angular/grids/core/src/column-actions/column-actions.component.html @@ -14,7 +14,7 @@

} diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts index 6560f5f1c0b..984ef92b936 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -3661,7 +3661,7 @@ export abstract class IgxGridBaseDirective implements GridType, const destructor = takeUntil(this.destroy$); fromEvent(this.nativeElement, 'keydown', { capture: true }).pipe(destructor).subscribe((event) => { const target = event.target as HTMLElement; - if (event.key === 'Enter' && !event.isComposing && !(target instanceof HTMLTextAreaElement) && !target.isContentEditable && + if (event.key === this.platform.KEYMAP.ENTER && !event.isComposing && !(target instanceof HTMLTextAreaElement) && !target.isContentEditable && !target.closest('button, a[href], input[type=button], input[type=submit], input[type=reset], input[type=image]')) { event.preventDefault(); } diff --git a/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.component.html b/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.component.html index 0c61c47e3af..30bccbcc492 100644 --- a/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.component.html +++ b/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.component.html @@ -7,7 +7,7 @@ tabindex="0" placeholder="Search" autocomplete="off" - (keydown.enter)="$event.preventDefault()" + (keydown.enter)="!$event.isComposing && $event.preventDefault()" /> From e8959c041e90096aa6bf8c2da2f7481198f80cd6 Mon Sep 17 00:00:00 2001 From: igdmdimitrov Date: Thu, 22 Oct 2026 15:05:23 +0300 Subject: [PATCH 6/6] fix(grid): resolve Enter target through shadow DOM composed path --- .../grids/grid/src/grid-base.directive.ts | 8 +++-- .../grid/src/grid-form-submission.spec.ts | 34 ++++++++++++++++--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts index 8a82364896d..ec2091ddc93 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -3660,8 +3660,12 @@ export abstract class IgxGridBaseDirective implements GridType, public _setupListeners() { const destructor = takeUntil(this.destroy$); fromEvent(this.nativeElement, 'keydown', { capture: true }).pipe(destructor).subscribe((event) => { - const target = event.target as HTMLElement; - if (event.key === this.platform.KEYMAP.ENTER && !event.isComposing && !(target instanceof HTMLTextAreaElement) && !target.isContentEditable && + const target = event.composedPath()[0]; + if (!(target instanceof Element)) { + return; + } + if (event.key === this.platform.KEYMAP.ENTER && !event.isComposing && !(target instanceof HTMLTextAreaElement) && + !(target instanceof HTMLElement && target.isContentEditable) && !target.closest('button, a[href], input[type=button], input[type=submit], input[type=reset], input[type=image]')) { event.preventDefault(); } diff --git a/projects/igniteui-angular/grids/grid/src/grid-form-submission.spec.ts b/projects/igniteui-angular/grids/grid/src/grid-form-submission.spec.ts index dde16024888..6e6fc1f66e5 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-form-submission.spec.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-form-submission.spec.ts @@ -1,9 +1,9 @@ -import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { IgxGridComponent } from './grid.component'; -import { FilterMode, IgxAdvancedFilteringDialogComponent, IgxColumnActionsComponent, IgxColumnComponent, IgxColumnHidingDirective, IgxGridToolbarActionsComponent, IgxGridToolbarComponent, IgxGridToolbarHidingComponent } from 'igniteui-angular/grids/core'; +import { FilterMode, IgxAdvancedFilteringDialogComponent, IgxCellTemplateDirective, IgxColumnActionsComponent, IgxColumnComponent, IgxColumnHidingDirective, IgxGridToolbarActionsComponent, IgxGridToolbarComponent, IgxGridToolbarHidingComponent } from 'igniteui-angular/grids/core'; import { AbsoluteScrollStrategy, GlobalPositionStrategy } from 'igniteui-angular/core'; import { IgxInputDirective } from 'igniteui-angular/input-group'; import { GridFunctions } from '../../../test-utils/grid-functions.spec'; @@ -20,11 +20,22 @@ const DATA = [ ]; const pressEnter = (target: Element): KeyboardEvent => { - const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }); + const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true, composed: true }); target.dispatchEvent(event); return event; }; +const SHADOW_CONTROLS_TAG = 'igx-test-shadow-controls'; +if (!customElements.get(SHADOW_CONTROLS_TAG)) { + customElements.define(SHADOW_CONTROLS_TAG, class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }).innerHTML = + 'Link'; + } + }); +} + const addAdvancedFilteringCondition = async (fixture: ComponentFixture): Promise => { QueryBuilderFunctions.clickQueryBuilderInitialAddConditionBtn(fixture, 0); await fixture.whenStable(); @@ -144,6 +155,15 @@ describe('IgxGrid - implicit form submission #grid', () => { expect(pressEnter(hidingButton).defaultPrevented).toBe(false); }); + it('should not prevent the native Enter behavior of controls inside a shadow root in the grid', () => { + const shadowRoot = fixture.nativeElement.querySelector(SHADOW_CONTROLS_TAG).shadowRoot as ShadowRoot; + expect(grid.nativeElement.contains(shadowRoot.host)).toBe(true); + + expect(pressEnter(shadowRoot.querySelector('button span')!).defaultPrevented).toBe(false); + expect(pressEnter(shadowRoot.querySelector('textarea')!).defaultPrevented).toBe(false); + expect(pressEnter(shadowRoot.querySelector('a')!).defaultPrevented).toBe(false); + }); + it('should not affect implicit submission from inputs outside the grid', () => { const outerInput: HTMLInputElement = fixture.nativeElement.querySelector('#outerInput'); expect(pressEnter(outerInput).defaultPrevented).toBe(false); @@ -195,14 +215,20 @@ describe('IgxGrid - implicit form submission from grid components outside of the - + + + + + `, changeDetection: ChangeDetectionStrategy.Eager, + schemas: [CUSTOM_ELEMENTS_SCHEMA], imports: [ IgxGridComponent, IgxColumnComponent, + IgxCellTemplateDirective, IgxGridToolbarComponent, IgxGridToolbarActionsComponent, IgxGridToolbarHidingComponent