diff --git a/CHANGELOG.md b/CHANGELOG.md index 05a3c13bb20..bf83fc33a69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,11 @@ 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 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. - `IgxAccordionComponent` - When `singleBranchExpand` is set initially and the panels are rendered with `@for`, the panels collapsed on init no longer keep `aria-expanded="true"` and no `ExpressionChangedAfterItHasBeenCheckedError` is thrown in development mode. - `IgxCarouselComponent` diff --git a/projects/igniteui-angular/core/src/core/utils.spec.ts b/projects/igniteui-angular/core/src/core/utils.spec.ts index 131b514e7cc..b8214ba2ad0 100644 --- a/projects/igniteui-angular/core/src/core/utils.spec.ts +++ b/projects/igniteui-angular/core/src/core/utils.spec.ts @@ -1,7 +1,7 @@ import { SampleTestData } from 'igniteui-angular/test-utils/sample-test-data.spec'; import { cloneValue, cloneValueCached, cloneHierarchicalArray, compareMaps, getComponentCssSizeVar, - intoChunks, isDate, isLeftToRight, isObject, showMessage, uniqueDates } from './utils'; -import { describe, it, expect } from 'vitest'; + intoChunks, isDate, isLeftToRight, isObject, preventImplicitSubmission, showMessage, uniqueDates } from './utils'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; describe('Utils', () => { const complexObject = { @@ -430,4 +430,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)).toBe(true); + expect(dispatchKeydown(fileInput)).toBe(false); + expect(dispatchKeydown(host.querySelector('#noForm')!)).toBe(false); + }); + + it('should not prevent other keys than Enter', () => { + host.innerHTML = '
'; + const input = host.querySelector('input')!; + + expect(dispatchKeydown(input, { key: 'a' })).toBe(false); + expect(dispatchKeydown(input, { key: ' ' })).toBe(false); + }); + + it('should not prevent Enter while an IME composition is in progress', () => { + host.innerHTML = '
'; + + expect(dispatchKeydown(host.querySelector('input')!, { isComposing: true })).toBe(false); + }); + + 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)).toBe(true); + }); + }); }); diff --git a/projects/igniteui-angular/core/src/core/utils.ts b/projects/igniteui-angular/core/src/core/utils.ts index f7f4a9e4185..cc494fb9532 100644 --- a/projects/igniteui-angular/core/src/core/utils.ts +++ b/projects/igniteui-angular/core/src/core/utils.ts @@ -481,6 +481,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 f111f4e0953..04c697d9d45 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,6 +14,7 @@

} 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 61de8da3ee9..510a1d059c8 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 @@ -25,7 +25,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; /** @@ -45,6 +45,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 18098e8e9c9..40c06f19ee8 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -103,7 +103,8 @@ import { runAfterRenderOnce, GridStyleCSSProperty, I18N_FORMATTER, - IgxStylesRegistrar + IgxStylesRegistrar, + preventImplicitSubmission } from 'igniteui-angular/core'; import { IgcTrialWatermark } from 'igniteui-trial-watermark'; import { @@ -3827,6 +3828,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..36de95a2b50 --- /dev/null +++ b/projects/igniteui-angular/grids/grid/src/grid-form-submission.spec.ts @@ -0,0 +1,283 @@ +import { ChangeDetectionStrategy, Component, 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, 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'; +import { firstValueFrom } from 'rxjs'; +import { userEvent } from 'vitest/browser'; +import { describe, it, expect, beforeEach } from 'vitest'; + +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) } +]; + +const pressEnter = (target: Element): KeyboardEvent => { + const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true, composed: true }); + target.dispatchEvent(event); + return event; +}; + +const addAdvancedFilteringCondition = async (fixture: ComponentFixture): Promise => { + QueryBuilderFunctions.clickQueryBuilderInitialAddConditionBtn(fixture, 0); + await fixture.whenStable(); + fixture.detectChanges(); + await QueryBuilderFunctions.selectColumnInEditModeExpression(fixture, 1); // 'ProductName' + await 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(async () => { + await TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, GridInFormComponent] + }).compileComponents(); + }); + + beforeEach(async () => { + fixture = TestBed.createComponent(GridInFormComponent); + fixture.detectChanges(); + await fixture.whenStable(); + 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', async () => { + GridFunctions.clickFilterCellChipUI(fixture, 'ProductName'); + await fixture.whenStable(); + fixture.detectChanges(); + + const input: HTMLInputElement = fixture.debugElement.query(By.css(FILTER_UI_ROW)).query(By.directive(IgxInputDirective)).nativeElement; + expect(input.form).toBe(form); + expect(pressEnter(input).defaultPrevented).toBe(true); + + await userEvent.click(input); + for (const key of 'Chai') { + await userEvent.keyboard(key); + await fixture.whenStable(); + await new Promise(resolve => requestAnimationFrame(resolve)); + fixture.detectChanges(); + } + expect(input.value).toBe('Chai'); + + await userEvent.keyboard('{Enter}'); + await fixture.whenStable(); + await new Promise(resolve => requestAnimationFrame(resolve)); + fixture.detectChanges(); + + expect(grid.filteredData?.map(r => r.ProductName)).toEqual(['Chai']); + expect(fixture.componentInstance.submits).toBe(0); + }); + + 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(pressEnter(input).defaultPrevented).toBe(true); + }); + + it('should prevent implicit submission on Enter in the toolbar column hiding search input', async () => { + const hidingButton: HTMLButtonElement = fixture.nativeElement.querySelector('igx-grid-toolbar-hiding button'); + hidingButton.click(); + await fixture.whenStable(); + fixture.detectChanges(); + + const input: HTMLInputElement = grid.nativeElement.querySelector('igx-column-actions input')!; + expect(input.form).toBe(form); + expect(pressEnter(input).defaultPrevented).toBe(true); + }); + + it('should prevent implicit submission on Enter in the excel style filtering custom dialog input', async () => { + grid.filterMode = FilterMode.excelStyleFilter; + fixture.detectChanges(); + + await GridFunctions.clickExcelFilterIconFromCode(fixture, grid, 'ProductName'); + GridFunctions.clickExcelFilterCascadeButton(fixture); + await fixture.whenStable(); + fixture.detectChanges(); + GridFunctions.clickOperatorFromCascadeMenu(fixture, 0); + await fixture.whenStable(); + fixture.detectChanges(); + + const input = GridFunctions.getExcelFilteringInput(fixture, 0); + expect(input.form).toBe(form); + expect(pressEnter(input).defaultPrevented).toBe(true); + }); + + it('should prevent implicit submission on Enter in the advanced filtering dialog opened with custom overlay settings', async () => { + grid.openAdvancedFilteringDialog({ + closeOnOutsideClick: false, + modal: false, + positionStrategy: new GlobalPositionStrategy(), + scrollStrategy: new AbsoluteScrollStrategy() + }); + await fixture.whenStable(); + fixture.detectChanges(); + + const input = await addAdvancedFilteringCondition(fixture); + expect(grid.nativeElement.contains(input)).toBe(false); + expect(input.form).toBe(form); + expect(pressEnter(input).defaultPrevented).toBe(true); + + grid.closeAdvancedFilteringDialog(false); + await fixture.whenStable(); + }); + + it('should not submit the form on Enter in a filtering row date picker calendar, which moves the focus to its input', async () => { + GridFunctions.clickFilterCellChipUI(fixture, 'Released'); + await fixture.whenStable(); + fixture.detectChanges(); + + const filteringRow = fixture.debugElement.query(By.css(FILTER_UI_ROW)); + const datePicker = filteringRow.query(By.directive(IgxDatePickerComponent)).componentInstance as IgxDatePickerComponent; + const opened = firstValueFrom(datePicker.opened); + (datePicker.getEditElement() as HTMLElement).click(); + fixture.detectChanges(); + await opened; + await fixture.whenStable(); + fixture.detectChanges(); + expect(document.activeElement!.closest('igx-calendar')).not.toBeNull(); + + await userEvent.keyboard('{Enter}'); + await fixture.whenStable(); + fixture.detectChanges(); + + expect(datePicker.collapsed).toBe(true); + expect((document.activeElement as HTMLInputElement).form).toBe(form); + expect(fixture.componentInstance.submits).toBe(0); + }); + + it('should submit the form on Enter in an input outside the grid', async () => { + await userEvent.click(fixture.nativeElement.querySelector('#outerInput')); + await userEvent.keyboard('{Enter}'); + expect(fixture.componentInstance.submits).toBe(1); + }); + + 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(pressEnter(grid.nativeElement.querySelector('summary')!).defaultPrevented).toBe(false); + expect(pressEnter(fileInput).defaultPrevented).toBe(false); + expect(pressEnter(grid.nativeElement.querySelector('input[type=color]')!).defaultPrevented).toBe(false); + }); +}); + +describe('IgxGrid - implicit form submission from grid components outside of the grid #grid', () => { + let fixture: ComponentFixture; + let form: HTMLFormElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, GridComponentsInFormComponent] + }).compileComponents(); + }); + + beforeEach(async () => { + fixture = TestBed.createComponent(GridComponentsInFormComponent); + fixture.detectChanges(); + await fixture.whenStable(); + 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)).toBe(false); + expect(input.form).toBe(form); + expect(pressEnter(input).defaultPrevented).toBe(true); + }); +}); + +@Component({ + template: ` +
+ + + + + + + + + + + +
DetailsContent
+ + +
+
+ +
+ +
`, + changeDetection: ChangeDetectionStrategy.Eager, + imports: [ + IgxGridComponent, + IgxColumnComponent, + IgxCellTemplateDirective, + IgxGridToolbarComponent, + IgxGridToolbarActionsComponent, + IgxGridToolbarHidingComponent + ] +}) +class GridInFormComponent { + @ViewChild('grid', { static: true }) + public grid!: IgxGridComponent; + + public data = DATA; + public submits = 0; +} + +@Component({ + template: ` +
+ + + + + + + +
`, + changeDetection: ChangeDetectionStrategy.Eager, + 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 6323dd02a2e..af95f530c3a 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 901c7a05377..10fbd441074 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 @@ -25,7 +25,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'; import { Subscription } from 'rxjs'; @@ -77,6 +77,8 @@ export class IgxPivotDataSelectorComponent implements OnDestroy { private pivotConfigChangeSub?: Subscription; protected pipeRetrigger = 0; + 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 81d929cb1e0..bbd55e819d8 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 @@ -44,6 +44,27 @@ 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 pressEnter = () => { + const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }); + input.dispatchEvent(event); + return event.defaultPrevented; + }; + + expect(pressEnter()).toBe(false); + + const form = document.createElement('form'); + fixture.nativeElement.parentElement.appendChild(form); + form.appendChild(fixture.nativeElement); + expect(input.form).toBe(form); + expect(pressEnter()).toBe(true); + form.remove(); + }); }); describe("Pivot data selector integration #pivotGrid", () => { 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 9c3b216eec2..1c28f5f9948 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 @@ -333,6 +333,7 @@
+ + `, + changeDetection: ChangeDetectionStrategy.Eager, + 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 c9fff3429e3..f8f1ce6acee 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 @@ -22,7 +22,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'; @@ -50,11 +51,16 @@ import { IgxQueryBuilderHeaderComponent } from './query-builder-header.component styleUrls: ['./query-builder.component.css'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.Eager, - imports: [IgxQueryBuilderTreeComponent] + imports: [IgxQueryBuilderTreeComponent], + host: { + '(keydown)': 'preventImplicitSubmission($event)' + } }) export class IgxQueryBuilderComponent implements OnDestroy { protected iconService = inject(IgxIconService); + protected readonly preventImplicitSubmission = preventImplicitSubmission; + /** * @hidden @internal */ 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 fd2c469a0d6..334015f57c6 100644 --- a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.spec.ts +++ b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.spec.ts @@ -98,6 +98,17 @@ describe('IgxSnackbar', () => { expect(snackbar.clicked.emit).toHaveBeenCalledWith(snackbar); }); + it('should render the action button with type="button" to prevent form submission', async () => { + snackbar.actionText = 'undo'; + snackbar.displayTime = 100; + + snackbar.open(); + 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', async () => { vi.spyOn(snackbar.opening, 'emit'); snackbar.open();