Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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.
- `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.
- `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`
Expand Down Expand Up @@ -76,6 +81,7 @@ All notable changes for each version of this project will be documented in this
- `IgxOverlayService`
- Destroying a view while an overlay component attached through a `ViewContainerRef` is open (e.g. an open date picker calendar or grid filtering dialog) no longer leaves the parent components undestroyed.


## 22.2.1

### New Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ <h4 [attr.id]="titleID" class="igx-column-actions__header-title" data-no-typogra
[(ngModel)]="filterCriteria"
[placeholder]="filterColumnsPrompt"
autocomplete="off"
(keydown.enter)="!$event.isComposing && $event.preventDefault()"
/>
</igx-input-group>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ export class IgxAdvancedFilteringDialogComponent implements OnDestroy {
const key = eventArgs.key;
if (key === this.platform.KEYMAP.ESCAPE) {
this.closeDialog();
} else if (key === this.platform.KEYMAP.ENTER && !eventArgs.isComposing && eventArgs.target instanceof HTMLInputElement) {
eventArgs.preventDefault();
}
}

Expand Down
11 changes: 11 additions & 0 deletions projects/igniteui-angular/grids/grid/src/grid-base.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3659,6 +3659,17 @@ export abstract class IgxGridBaseDirective implements GridType,
*/
public _setupListeners() {
const destructor = takeUntil<any>(this.destroy$);
fromEvent<KeyboardEvent>(this.nativeElement, 'keydown', { capture: true }).pipe(destructor).subscribe((event) => {
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();
}
});
fromEvent(this.nativeElement, 'focusout').pipe(filter(() => !!this.navigation.activeNode), destructor).subscribe((event) => {
const activeNode = this.navigation.activeNode;
if (!this.crudService.cell && !!activeNode &&
Expand Down
270 changes: 270 additions & 0 deletions projects/igniteui-angular/grids/grid/src/grid-form-submission.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
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, 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';
import { QueryBuilderFunctions } from '../../../query-builder/src/query-builder/query-builder-functions.spec';
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 },
{ ProductID: 2, ProductName: 'Aniseed Syrup', InStock: false },
{ ProductID: 3, ProductName: 'Chef Antons Cajun Seasoning', InStock: true }
];

const pressEnter = (target: Element): KeyboardEvent => {
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 =
'<button type="button"><span>Click</span></button><textarea></textarea><a href="#">Link</a>';
}
});
}

const addAdvancedFilteringCondition = async (fixture: ComponentFixture<any>): Promise<HTMLInputElement> => {
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<GridInFormComponent>;
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', () => {
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 = fixture.debugElement.query(By.css(FILTER_UI_ROW)).query(By.directive(IgxInputDirective));
expect(input.nativeElement.form).toBe(form);
expect(pressEnter(input.nativeElement).defaultPrevented).toBe(true);
});

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 input', async () => {
grid.openAdvancedFilteringDialog();
fixture.detectChanges();

const input = await addAdvancedFilteringCondition(fixture);
expect(input.form).toBe(form);
expect(pressEnter(input).defaultPrevented).toBe(true);

grid.closeAdvancedFilteringDialog(false);
await fixture.whenStable();
});

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 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).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);
});
});

describe('IgxGrid - implicit form submission from grid components outside of the grid #grid', () => {
let fixture: ComponentFixture<GridComponentsInFormComponent>;
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);
});

it('should prevent implicit submission on Enter in the standalone advanced filtering dialog input', async () => {
const input = await addAdvancedFilteringCondition(fixture);
expect(fixture.componentInstance.grid.nativeElement.contains(input)).toBe(false);
expect(input.form).toBe(form);
expect(pressEnter(input).defaultPrevented).toBe(true);
});
});

@Component({
template: `
<form (submit)="$event.preventDefault()">
<input id="outerInput" name="outerInput" type="text" />
<igx-grid #grid [data]="data" [allowFiltering]="true" [primaryKey]="'ProductID'" height="500px">
<igx-grid-toolbar>
<igx-grid-toolbar-actions>
<igx-grid-toolbar-hiding></igx-grid-toolbar-hiding>
</igx-grid-toolbar-actions>
</igx-grid-toolbar>
<igx-column field="ProductID" [dataType]="'number'"></igx-column>
<igx-column field="ProductName" [editable]="true"></igx-column>
<igx-column field="InStock" [dataType]="'boolean'">
<ng-template igxCell>
<igx-test-shadow-controls></igx-test-shadow-controls>
</ng-template>
</igx-column>
</igx-grid>
<button id="submit" type="submit">Submit</button>
</form>`,
changeDetection: ChangeDetectionStrategy.Eager,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [
IgxGridComponent,
IgxColumnComponent,
IgxCellTemplateDirective,
IgxGridToolbarComponent,
IgxGridToolbarActionsComponent,
IgxGridToolbarHidingComponent
]
})
class GridInFormComponent {
@ViewChild('grid', { static: true })
public grid!: IgxGridComponent;

public data = DATA;
}

@Component({
template: `
<form (submit)="$event.preventDefault()">
<igx-grid #grid [data]="data" height="300px">
<igx-column field="ProductID" [dataType]="'number'"></igx-column>
<igx-column field="ProductName"></igx-column>
<igx-column field="InStock" [dataType]="'boolean'"></igx-column>
</igx-grid>
<igx-column-actions id="standaloneActions" igxColumnHiding [grid]="grid"></igx-column-actions>
<igx-advanced-filtering-dialog [grid]="grid"></igx-advanced-filtering-dialog>
<button id="submit" type="submit">Submit</button>
</form>`,
changeDetection: ChangeDetectionStrategy.Eager,
imports: [
IgxGridComponent,
IgxColumnComponent,
IgxColumnActionsComponent,
IgxColumnHidingDirective,
IgxAdvancedFilteringDialogComponent
]
})
class GridComponentsInFormComponent {
@ViewChild('grid', { static: true })
public grid!: IgxGridComponent;

public data = DATA;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
tabindex="0"
placeholder="Search"
autocomplete="off"
(keydown.enter)="!$event.isComposing && $event.preventDefault()"
/>
</igx-input-group>
<igx-list>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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).toBe(true);
});
});

describe("Pivot data selector integration #pivotGrid", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@
<div igxDragIgnore class="igx-filter-tree__expression-actions">
<button
#addExpressionButton
type="button"
igxDragIgnore
igxIconButton="outlined"
[igxDropDownItemNavigation]="addOptionsDropDown"
Expand Down Expand Up @@ -624,6 +625,7 @@
>
<button
#changeGroupButton
type="button"
[ngClass]="{
'igx-filter-tree__button--and': getOperator(expressionItem) === 0,
'igx-filter-tree__button--or': getOperator(expressionItem) === 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1121,6 +1121,20 @@ describe('IgxQueryBuilder', () => {
expect(QueryBuilderFunctions.getQueryBuilderTreeExpressionActionsContainer(fix, [0]), 'actions container is visible').toBeNull();
});

it('Should render the add expression and group buttons with type="button" to prevent form submission.', async () => {
queryBuilder.expressionTree = QueryBuilderFunctions.generateExpressionTree();
fix.detectChanges();

UIInteractions.hoverElement(QueryBuilderFunctions.getQueryBuilderTreeItem(fix, [0]) as HTMLElement);
await fix.whenStable();
fix.detectChanges();

const addButton = QueryBuilderFunctions.getQueryBuilderTreeExpressionActionsContainer(fix, [0]).querySelector('button');
const groupButton = fix.nativeElement.querySelector('button.igx-filter-tree__button--and, button.igx-filter-tree__button--or');
expect(addButton.getAttribute('type')).toBe('button');
expect(groupButton.getAttribute('type')).toBe('button');
});

it('Should have disabled adding buttons when an expression is in edit mode.', async () => {
queryBuilder.expressionTree = QueryBuilderFunctions.generateExpressionTree();
fix.detectChanges();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<ng-content select="[igxButton]"></ng-content>
</div>
@if (!customButton.children.length && actionText) {
<button igxButton (click)="triggerAction()">
<button type="button" igxButton (click)="triggerAction()">
{{ actionText }}
</button>
}
Loading
Loading