From efa4ece6c5dd12bc019b845ed9608a43c878ade8 Mon Sep 17 00:00:00 2001 From: igdmdimitrov Date: Wed, 7 Oct 2026 13:20:07 +0300 Subject: [PATCH 1/2] 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 62f1182c6f4..331b1f243c2 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. +## 21.2.21 + +### 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. + ## 21.2.0 ### New Features diff --git a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.html b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.html index e712098f9fc..a9d60a2499a 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 f2653fbaffa..c816a332d6d 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 739ef00374c1d94c2ac6f823bbc3c00db21dd760 Mon Sep 17 00:00:00 2001 From: igdmdimitrov Date: Wed, 11 Nov 2026 14:36:10 +0200 Subject: [PATCH 2/2] fix(grid): prevent implicit form submission from grid --- CHANGELOG.md | 13 + .../core/src/core/utils.spec.ts | 56 +++- .../igniteui-angular/core/src/core/utils.ts | 19 ++ .../column-actions.component.html | 3 +- .../column-actions.component.ts | 4 +- .../grids/grid/src/grid-base.directive.ts | 9 +- .../grid/src/grid-form-submission.spec.ts | 276 ++++++++++++++++++ .../src/pivot-data-selector.component.html | 1 + .../src/pivot-data-selector.component.ts | 4 +- .../src/pivot-data-selector.spec.ts | 21 ++ .../query-builder-tree.component.html | 4 +- .../query-builder.component.spec.ts | 50 ++++ .../query-builder/query-builder.component.ts | 10 +- 13 files changed, 461 insertions(+), 9 deletions(-) create mode 100644 projects/igniteui-angular/grids/grid/src/grid-form-submission.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fa685ef730e..9a24c7f85b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes for each version of this project will be documented in this file. +## 21.2.26 + +### 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), 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. Pressing Enter in the query builder inputs also no longer submits a surrounding `form`. +- `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, as well as selecting a date in their date pickers with Enter, 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. + ## 21.2.25 ### Bug Fixes diff --git a/projects/igniteui-angular/core/src/core/utils.spec.ts b/projects/igniteui-angular/core/src/core/utils.spec.ts index 2a6bb842080..0ae1e117953 100644 --- a/projects/igniteui-angular/core/src/core/utils.spec.ts +++ b/projects/igniteui-angular/core/src/core/utils.spec.ts @@ -1,5 +1,5 @@ import { SampleTestData } from 'igniteui-angular/test-utils/sample-test-data.spec'; -import { cloneValue, isObject, isDate } from './utils'; +import { cloneValue, isObject, isDate, preventImplicitSubmission } from './utils'; describe('Utils', () => { const complexObject = { @@ -253,4 +253,58 @@ describe('Utils', () => { expect(isDate(variable)).toBeFalsy(); }); }); + + describe('Utils - preventImplicitSubmission() unit tests', () => { + let host: HTMLDivElement; + + const dispatchKeydown = (target: Element, init: KeyboardEventInit = {}): boolean => { + const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true, composed: true, ...init }); + target.dispatchEvent(event); + return event.defaultPrevented; + }; + + beforeEach(() => { + host = document.createElement('div'); + host.addEventListener('keydown', preventImplicitSubmission); + document.body.appendChild(host); + }); + + afterEach(() => { + host.remove(); + }); + + it('should prevent Enter only on a form-owned input which would submit the form', () => { + host.innerHTML = '
'; + const [textInput, fileInput] = Array.from(host.querySelectorAll('form input')); + + expect(dispatchKeydown(textInput)).toBeTrue(); + expect(dispatchKeydown(fileInput)).toBeFalse(); + expect(dispatchKeydown(host.querySelector('#noForm'))).toBeFalse(); + }); + + it('should not prevent other keys than Enter', () => { + host.innerHTML = '
'; + const input = host.querySelector('input'); + + expect(dispatchKeydown(input, { key: 'a' })).toBeFalse(); + expect(dispatchKeydown(input, { key: ' ' })).toBeFalse(); + }); + + it('should not prevent Enter while an IME composition is in progress', () => { + host.innerHTML = '
'; + + expect(dispatchKeydown(host.querySelector('input'), { isComposing: true })).toBeFalse(); + }); + + it('should resolve the target of the event through its composed path', () => { + const shadowHost = document.createElement('div'); + host.appendChild(shadowHost); + const shadowRoot = shadowHost.attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '
'; + const input = shadowRoot.querySelector('input'); + + expect(input.form).toBe(shadowRoot.querySelector('form')); + expect(dispatchKeydown(input)).toBeTrue(); + }); + }); }); diff --git a/projects/igniteui-angular/core/src/core/utils.ts b/projects/igniteui-angular/core/src/core/utils.ts index 12cc7c76c89..d5406afd7a5 100644 --- a/projects/igniteui-angular/core/src/core/utils.ts +++ b/projects/igniteui-angular/core/src/core/utils.ts @@ -479,6 +479,25 @@ export const NAVIGATION_KEYS = new Set([ ' ' ]); +/** Input types on which Enter has its own activation behavior instead of submitting the form. */ +const ACTIVATABLE_INPUT_TYPES = new Set(['button', 'submit', 'reset', 'image', 'file', 'color']); + +/** + * @hidden + * @internal + * + * Cancels the implicit submission of the surrounding form, which the browser performs as the default action + * of an Enter keydown in an ``. + * Only inputs which have a form owner and no activation behavior of their own are affected. + */ +export function preventImplicitSubmission(event: KeyboardEvent): void { + const target = event.composedPath()[0]; + if (event.key === 'Enter' && !event.isComposing && target instanceof HTMLInputElement && + target.form && !ACTIVATABLE_INPUT_TYPES.has(target.type)) { + event.preventDefault(); + } +} + /** * @hidden * @internal 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 071b64a2ac8..f3c17cc979e 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 @@ -10,7 +10,8 @@

{{ title }}

+ autocomplete="off" + (keydown)="preventImplicitSubmission($event)" /> } diff --git a/projects/igniteui-angular/grids/core/src/column-actions/column-actions.component.ts b/projects/igniteui-angular/grids/core/src/column-actions/column-actions.component.ts index 0c474bc8585..ce8a36af154 100644 --- a/projects/igniteui-angular/grids/core/src/column-actions/column-actions.component.ts +++ b/projects/igniteui-angular/grids/core/src/column-actions/column-actions.component.ts @@ -7,7 +7,7 @@ import { FormsModule } from '@angular/forms'; import { IgxInputDirective, IgxInputGroupComponent } from 'igniteui-angular/input-group'; import { IgxCheckboxComponent } from 'igniteui-angular/checkbox'; import { IgxButtonDirective, IgxRippleDirective } from 'igniteui-angular/directives'; -import { ColumnType } from 'igniteui-angular/core'; +import { ColumnType, preventImplicitSubmission } from 'igniteui-angular/core'; let NEXT_ID = 0; /** @@ -24,6 +24,8 @@ let NEXT_ID = 0; export class IgxColumnActionsComponent implements DoCheck { private differs = inject(IterableDiffers); + protected readonly preventImplicitSubmission = preventImplicitSubmission; + /** * Gets/Sets the grid to provide column actions for. 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 71eeba662cb..803c7b2f55e 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -94,7 +94,8 @@ import { IgxOverlayOutletDirective, DEFAULT_LOCALE, onResourceChangeHandle, - runAfterRenderOnce + runAfterRenderOnce, + preventImplicitSubmission } from 'igniteui-angular/core'; import { IgcTrialWatermark } from 'igniteui-trial-watermark'; import { Subject, pipe, fromEvent, animationFrameScheduler, merge, BehaviorSubject, timer } from 'rxjs'; @@ -3700,6 +3701,12 @@ export abstract class IgxGridBaseDirective implements GridType, */ public _setupListeners() { const destructor = takeUntil(this.destroy$); + // Capture phase, since internal handlers (filtering row, editors, dialogs) stop the propagation of key events. + // The keypress is also handled, as a picker selecting on Enter may move the focus to its input before it. + merge( + fromEvent(this.nativeElement, 'keydown', { capture: true }), + fromEvent(this.nativeElement, 'keypress', { capture: true }) + ).pipe(destructor).subscribe(preventImplicitSubmission); 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..14c3930b7dc --- /dev/null +++ b/projects/igniteui-angular/grids/grid/src/grid-form-submission.spec.ts @@ -0,0 +1,276 @@ +import { 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 { FilterMode, INPUT_DEBOUNCE_TIME_DEFAULT, 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 { IgxDatePickerComponent } from 'igniteui-angular/date-picker'; +import { GridFunctions } from '../../../test-utils/grid-functions.spec'; +import { QueryBuilderFunctions } from '../../../query-builder/src/query-builder/query-builder-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, Released: new Date(2024, 0, 10) }, + { ProductID: 2, ProductName: 'Aniseed Syrup', InStock: false, Released: new Date(2024, 1, 11) }, + { ProductID: 3, ProductName: 'Chef Antons Cajun Seasoning', InStock: true, Released: new Date(2024, 2, 12) } +]; + +/** + * Dispatches a keyboard event on the target and returns it. + * The browser performs the implicit submission as the default action of the Enter key, so a cancelled event does not submit the form. + */ +const pressKey = (target: Element, key = 'Enter', type = 'keydown'): KeyboardEvent => { + const event = new KeyboardEvent(type, { key, bubbles: true, cancelable: true, composed: true }); + target.dispatchEvent(event); + return event; +}; + +const addAdvancedFilteringCondition = (fixture: ComponentFixture): HTMLInputElement => { + QueryBuilderFunctions.clickQueryBuilderInitialAddConditionBtn(fixture, 0); + tick(100); + fixture.detectChanges(); + QueryBuilderFunctions.selectColumnInEditModeExpression(fixture, 1); // 'ProductName' + QueryBuilderFunctions.selectOperatorInEditModeExpression(fixture, 2); // 'Starts With' + return QueryBuilderFunctions.getQueryBuilderValueInput(fixture).querySelector('input'); +}; + +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(); + tick(100); + 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', () => { + // The add row snackbar is always rendered and was the default button of the form before the fix. + expect(grid.nativeElement.querySelector('igx-snackbar button').getAttribute('type')).toBe('button'); + expect(grid.nativeElement.querySelectorAll(UNTYPED_BUTTONS).length).toBe(0); + 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(100); + fixture.detectChanges(); + + const input: HTMLInputElement = fixture.debugElement.query(By.css(FILTER_UI_ROW)).query(By.directive(IgxInputDirective)).nativeElement; + expect(input.form).toBe(form); + // Typing must keep working: only Enter is cancelled. + expect(pressKey(input, 'a').defaultPrevented).toBeFalse(); + + GridFunctions.typeValueInFilterRowInput('Chai', fixture); + tick(INPUT_DEBOUNCE_TIME_DEFAULT); + fixture.detectChanges(); + + expect(pressKey(input).defaultPrevented).toBeTrue(); + tick(100); + fixture.detectChanges(); + + expect(grid.rowList.map(row => row.data.ProductName)).toEqual(['Chai']); + })); + + 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.nativeElement.querySelector('.igx-grid__td--editing input'); + expect(input.form).toBe(form); + expect(pressKey(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(100); + fixture.detectChanges(); + + const input: HTMLInputElement = grid.nativeElement.querySelector('igx-column-actions input'); + expect(input.form).toBe(form); + expect(pressKey(input).defaultPrevented).toBeTrue(); + })); + + it('should prevent implicit submission on Enter in the excel style filtering custom dialog input', fakeAsync(() => { + grid.filterMode = FilterMode.excelStyleFilter; + fixture.detectChanges(); + + GridFunctions.clickExcelFilterIconFromCode(fixture, grid, 'ProductName'); + GridFunctions.clickExcelFilterCascadeButton(fixture); + tick(100); + fixture.detectChanges(); + GridFunctions.clickOperatorFromCascadeMenu(fixture, 0); + tick(200); + fixture.detectChanges(); + + const input = GridFunctions.getExcelFilteringInput(fixture, 0); + expect(input.form).toBe(form); + expect(pressKey(input).defaultPrevented).toBeTrue(); + })); + + it('should prevent implicit submission on Enter in the advanced filtering dialog opened with custom overlay settings', fakeAsync(() => { + grid.openAdvancedFilteringDialog({ + closeOnOutsideClick: false, + modal: false, + positionStrategy: new GlobalPositionStrategy(), + scrollStrategy: new AbsoluteScrollStrategy() + }); + tick(100); + fixture.detectChanges(); + + const input = addAdvancedFilteringCondition(fixture); + expect(grid.nativeElement.contains(input)).toBeFalse(); + expect(input.form).toBe(form); + expect(pressKey(input).defaultPrevented).toBeTrue(); + + grid.closeAdvancedFilteringDialog(false); + tick(100); + })); + + it('should prevent implicit submission on Enter in a filtering row date picker calendar, which moves the focus to its input', fakeAsync(() => { + GridFunctions.clickFilterCellChipUI(fixture, 'Released'); + tick(100); + fixture.detectChanges(); + + const filteringRow = fixture.debugElement.query(By.css(FILTER_UI_ROW)); + const datePicker = filteringRow.query(By.directive(IgxDatePickerComponent)).componentInstance as IgxDatePickerComponent; + (datePicker.getEditElement() as HTMLElement).click(); + tick(100); + fixture.detectChanges(); + expect(datePicker.collapsed).toBeFalse(); + expect(document.activeElement.closest('igx-calendar')).not.toBeNull(); + + // Selecting a date with Enter closes the picker and focuses its input during the keydown, so the browser + // performs the implicit submission on the following keypress, which targets that input. + pressKey(document.activeElement); + tick(100); + fixture.detectChanges(); + + const pickerInput = document.activeElement as HTMLInputElement; + expect(datePicker.collapsed).toBeTrue(); + expect(pickerInput.form).toBe(form); + expect(pressKey(pickerInput, 'Enter', 'keypress').defaultPrevented).toBeTrue(); + })); + + it('should not prevent the native Enter behavior of other elements inside the grid', () => { + const fileInput = grid.nativeElement.querySelector('input[type=file]') as HTMLInputElement; + expect(fileInput.form).toBe(form); + + expect(pressKey(grid.nativeElement.querySelector('summary')).defaultPrevented).toBeFalse(); + expect(pressKey(fileInput).defaultPrevented).toBeFalse(); + expect(pressKey(grid.nativeElement.querySelector('input[type=color]')).defaultPrevented).toBeFalse(); + }); + + it('should not affect implicit submission from inputs outside the grid', () => { + const outerInput: HTMLInputElement = fixture.nativeElement.querySelector('#outerInput'); + expect(pressKey(outerInput).defaultPrevented).toBeFalse(); + expect(pressKey(outerInput, 'Enter', 'keypress').defaultPrevented).toBeFalse(); + }); +}); + +describe('IgxGrid - implicit form submission from grid components outside of the grid #grid', () => { + let fixture: ComponentFixture; + let form: HTMLFormElement; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, GridComponentsInFormComponent] + }).compileComponents(); + })); + + beforeEach(fakeAsync(() => { + fixture = TestBed.createComponent(GridComponentsInFormComponent); + fixture.detectChanges(); + tick(100); + fixture.detectChanges(); + form = fixture.nativeElement.querySelector('form'); + })); + + it('should prevent implicit submission on Enter in the standalone column actions filter input', () => { + const input: HTMLInputElement = fixture.nativeElement.querySelector('#standaloneActions input'); + expect(fixture.componentInstance.grid.nativeElement.contains(input)).toBeFalse(); + expect(input.form).toBe(form); + expect(pressKey(input).defaultPrevented).toBeTrue(); + }); +}); + +@Component({ + template: ` +
+ + + + + + + + + + + +
DetailsContent
+ + +
+
+ +
+ +
`, + imports: [ + IgxGridComponent, + IgxColumnComponent, + IgxCellTemplateDirective, + IgxGridToolbarComponent, + IgxGridToolbarActionsComponent, + IgxGridToolbarHidingComponent + ] +}) +class GridInFormComponent { + @ViewChild('grid', { static: true }) + public grid: IgxGridComponent; + + public data = DATA; +} + +@Component({ + template: ` +
+ + + + + + + +
`, + imports: [ + IgxGridComponent, + IgxColumnComponent, + IgxColumnActionsComponent, + IgxColumnHidingDirective + ] +}) +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 5a41d48b3bb..2272a7af02f 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)="preventImplicitSubmission($event)" /> diff --git a/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.component.ts b/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.component.ts index a8a6307ea64..07172ff9511 100644 --- a/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.component.ts +++ b/projects/igniteui-angular/grids/pivot-grid/src/pivot-data-selector.component.ts @@ -12,7 +12,7 @@ import { IgxExpansionPanelBodyComponent, IgxExpansionPanelComponent, IgxExpansio import { IDragBaseEventArgs, IDragGhostBaseEventArgs, IDragMoveEventArgs, IDropBaseEventArgs, IDropDroppedEventArgs, IgxDragDirective, IgxDragHandleDirective, IgxDropDirective } from 'igniteui-angular/directives'; import { IgxChipComponent } from 'igniteui-angular/chips'; import { IgxDropDownComponent, IgxDropDownItemComponent, IgxDropDownItemNavigationDirective, ISelectionEventArgs } from 'igniteui-angular/drop-down'; -import { AbsoluteScrollStrategy, AutoPositionStrategy, ColumnType, OverlaySettings, PositionSettings, ɵSize, SortingDirection, VerticalAlignment } from 'igniteui-angular/core'; +import { AbsoluteScrollStrategy, AutoPositionStrategy, ColumnType, OverlaySettings, PositionSettings, preventImplicitSubmission, ɵSize, SortingDirection, VerticalAlignment } from 'igniteui-angular/core'; import { IPivotAggregator, IPivotDimension, IPivotValue, PivotDimensionType, PivotGridType, PivotUtil } from 'igniteui-angular/grids/core'; interface IDataSelectorPanel { @@ -57,6 +57,8 @@ export class IgxPivotDataSelectorComponent { private renderer = inject(Renderer2); private cdr = inject(ChangeDetectorRef); + protected readonly preventImplicitSubmission = preventImplicitSubmission; + /** * Gets/sets whether the columns panel is expanded 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 e5d1ac8f61b..b7a64a0f7d0 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 @@ -34,6 +34,27 @@ describe("Pivot data selector", () => { fixture.detectChanges(); expect(fixture.componentInstance).toBeDefined(); }); + + 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 pressEnter = () => { + const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }); + input.dispatchEvent(event); + return event.defaultPrevented; + }; + + expect(pressEnter()).toBeFalse(); + + const form = document.createElement('form'); + fixture.nativeElement.parentElement.appendChild(form); + form.appendChild(fixture.nativeElement); + expect(input.form).toBe(form); + expect(pressEnter()).toBeTrue(); + form.remove(); + }); }); describe("Pivot data selector integration", () => { 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 12f6069238c..99599118e00 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 @@ -257,7 +257,7 @@ @if (expressionItem.focused || expressionItem.hovered) {
- + + `, + standalone: true, + imports: [ + IgxQueryBuilderComponent + ] +}) +export class IgxQueryBuilderInFormSampleTestComponent { + @ViewChild(IgxQueryBuilderComponent) public queryBuilder: IgxQueryBuilderComponent; + public entities: Array = SampleEntities.map(a => ({ ...a })); +} + @Component({ template: ` diff --git a/projects/igniteui-angular/query-builder/src/query-builder/query-builder.component.ts b/projects/igniteui-angular/query-builder/src/query-builder/query-builder.component.ts index c012b1b2fa9..925261a4f19 100644 --- a/projects/igniteui-angular/query-builder/src/query-builder/query-builder.component.ts +++ b/projects/igniteui-angular/query-builder/src/query-builder/query-builder.component.ts @@ -11,7 +11,8 @@ import { QueryBuilderResourceStringsEN, recreateTree, getCurrentResourceStrings, - onResourceChangeHandle + onResourceChangeHandle, + preventImplicitSubmission } from 'igniteui-angular/core'; import { IgxQueryBuilderTreeComponent } from './query-builder-tree.component'; import { IgxIconService } from 'igniteui-angular/icon'; @@ -36,11 +37,16 @@ import { IgxQueryBuilderHeaderComponent } from './query-builder-header.component @Component({ selector: 'igx-query-builder', templateUrl: './query-builder.component.html', - imports: [IgxQueryBuilderTreeComponent] + imports: [IgxQueryBuilderTreeComponent], + host: { + '(keydown)': 'preventImplicitSubmission($event)' + } }) export class IgxQueryBuilderComponent implements OnDestroy { protected iconService = inject(IgxIconService); + protected readonly preventImplicitSubmission = preventImplicitSubmission; + /** * @hidden @internal */