Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a7c0b99
fix(snackbar): add type="button"
igdmdimitrov Oct 7, 2026
d64650a
Merge branch 'master' into dmdimitrov/fix-17638
kdinev Oct 9, 2026
81e6176
fix(*): prevent implicit form submission from a grid
igdmdimitrov Oct 14, 2026
15a0e47
chore(*): remove comments
igdmdimitrov Oct 14, 2026
5ccbeec
Merge branch 'master' into dmdimitrov/fix-17638
igdmdimitrov Oct 14, 2026
3208f9b
Merge branch 'master' into dmdimitrov/fix-17638
igdmdimitrov Oct 20, 2026
8153548
fix(*): prevent implicit form submission on standalone grid-related c…
igdmdimitrov Oct 20, 2026
14b7ad5
chore(*): apply PR review suggestions
igdmdimitrov Oct 21, 2026
04ab4fd
Merge branch 'master' into dmdimitrov/fix-17638
igdmdimitrov Oct 21, 2026
8947c72
Merge branch 'master' into dmdimitrov/fix-17638
viktorkombov Oct 22, 2026
e8959c0
fix(grid): resolve Enter target through shadow DOM composed path
igdmdimitrov Oct 22, 2026
39257e1
fix(*): add single internal preventImplicitSubmission helper
igdmdimitrov Nov 10, 2026
2247eda
Merge branch 'master' into dmdimitrov/fix-17638
igdmdimitrov Nov 10, 2026
ebd36ad
chore(*): update documentation comment
igdmdimitrov Nov 10, 2026
14f8d3f
Merge branch 'master' into dmdimitrov/fix-17638
viktorkombov Nov 10, 2026
02f501f
chore(*): address PR comments
igdmdimitrov Nov 10, 2026
4cd6975
chore(*): test improvements
igdmdimitrov Nov 10, 2026
db0e375
chore(*): moved import where it belongs
igdmdimitrov Nov 10, 2026
d37b7cb
Merge branch 'master' into dmdimitrov/fix-17638
viktorkombov Nov 10, 2026
f3ecdae
Merge branch 'master' into dmdimitrov/fix-17638
kdinev Nov 11, 2026
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
58 changes: 56 additions & 2 deletions projects/igniteui-angular/core/src/core/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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 = '<form><input type="text" /><input type="file" /></form><input id="noForm" type="text" />';
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 = '<form><input type="text" /></form>';
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 = '<form><input type="text" /></form>';

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 = '<form><input type="text" /></form>';
const input = shadowRoot.querySelector('input')!;

expect(input.form).toBe(shadowRoot.querySelector('form'));
expect(dispatchKeydown(input)).toBe(true);
});
});
});
19 changes: 19 additions & 0 deletions projects/igniteui-angular/core/src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<input>`.
* 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];
Comment thread
igdmdimitrov marked this conversation as resolved.
Resolved
if (event.key === 'Enter' && !event.isComposing && target instanceof HTMLInputElement &&
Comment thread
igdmdimitrov marked this conversation as resolved.
Resolved
target.form && !ACTIVATABLE_INPUT_TYPES.has(target.type)) {
event.preventDefault();
}
}

/**
* @hidden
* @internal
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)="preventImplicitSubmission($event)"
/>
</igx-input-group>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ import {
runAfterRenderOnce,
GridStyleCSSProperty,
I18N_FORMATTER,
IgxStylesRegistrar
IgxStylesRegistrar,
preventImplicitSubmission
} from 'igniteui-angular/core';
import { IgcTrialWatermark } from 'igniteui-trial-watermark';
import {
Expand Down Expand Up @@ -3827,6 +3828,12 @@ export abstract class IgxGridBaseDirective implements GridType,
*/
public _setupListeners() {
const destructor = takeUntil<any>(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<KeyboardEvent>(this.nativeElement, 'keydown', { capture: true }),
fromEvent<KeyboardEvent>(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 &&
Expand Down
Loading
Loading