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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 55 additions & 1 deletion projects/igniteui-angular/core/src/core/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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 = '<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)).toBeTrue();
expect(dispatchKeydown(fileInput)).toBeFalse();
expect(dispatchKeydown(host.querySelector('#noForm'))).toBeFalse();
});

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' })).toBeFalse();
expect(dispatchKeydown(input, { key: ' ' })).toBeFalse();
});

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

expect(input.form).toBe(shadowRoot.querySelector('form'));
expect(dispatchKeydown(input)).toBeTrue();
});
});
});
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 @@ -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 `<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];
if (event.key === 'Enter' && !event.isComposing && target instanceof HTMLInputElement &&
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 @@ -10,7 +10,8 @@ <h4 [attr.id]='titleID' class="igx-column-actions__header-title">{{ title }}</h4
[attr.aria-describedby]='titleID'
[(ngModel)]="filterCriteria"
[placeholder]="filterColumnsPrompt"
autocomplete="off" />
autocomplete="off"
(keydown)="preventImplicitSubmission($event)" />
</igx-input-group>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -3700,6 +3701,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