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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ All notable changes for each version of this project will be documented in this

### New Features

- `IgxCombo`, `IgxSimpleCombo`
- Introduced the ability for Combo and Simple Combo to close the dropdown list and move the focus to the next focusable element on "Tab" press and clear the selection if the combo is collapsed on "Escape".

- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid`
- Introduced a new cell merging feature that allows you to configure and merge cells in a column based on same data or other custom condition, into a single cell.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
/**
* @hidden @internal
*/
public override onItemActionKey(key: DropDownActionKey) {
public override onItemActionKey(key: DropDownActionKey, event?: KeyboardEvent) {
switch (key) {
case DropDownActionKey.ENTER:
this.handleEnter();
Expand All @@ -177,8 +177,10 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
this.handleSpace();
break;
case DropDownActionKey.ESCAPE:
case DropDownActionKey.TAB:
this.close();
break;
case DropDownActionKey.TAB:
this.close(event);
}
}

Expand Down
12 changes: 4 additions & 8 deletions projects/igniteui-angular/src/lib/combo/combo.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1208,7 +1208,8 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
return;
}
this.searchValue = '';
if (!e.event) {
const isTab = (e.event as KeyboardEvent)?.key === 'Tab';
if (!e.event || isTab) {
this.comboInput?.nativeElement.focus();
} else {
this._onTouchedCallback();
Expand All @@ -1228,13 +1229,8 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
event.stopPropagation();
this.close();
}
}

/** @hidden @internal */
public handleToggleKeyDown(eventArgs: KeyboardEvent) {
if (eventArgs.key === 'Enter' || eventArgs.key === ' ') {
eventArgs.preventDefault();
this.toggle();
if (event.key === "Tab") {
this.close();
}
}

Expand Down
4 changes: 2 additions & 2 deletions projects/igniteui-angular/src/lib/combo/combo.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</ng-container>
@if (displayValue) {
<igx-suffix [attr.aria-label]="resourceStrings.igx_combo_clearItems_placeholder" class="igx-combo__clear-button"
(click)="handleClearItems($event)" (keydown)="handleClearKeyDown($event)" [tabindex]="disabled ? -1 : 0" role="button">
(click)="handleClearItems($event)">
@if (clearIconTemplate) {
<ng-container *ngTemplateOutlet="clearIconTemplate"></ng-container>
}
Expand All @@ -29,7 +29,7 @@
}
</igx-suffix>
}
<igx-suffix class="igx-combo__toggle-button" (keydown)="handleToggleKeyDown($event)" [tabindex]="disabled ? -1 : 0" role="button">
<igx-suffix class="igx-combo__toggle-button">
@if (toggleIconTemplate) {
<ng-container *ngTemplateOutlet="toggleIconTemplate; context: {$implicit: collapsed}"></ng-container>
}
Expand Down
111 changes: 79 additions & 32 deletions projects/igniteui-angular/src/lib/combo/combo.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1944,15 +1944,93 @@ describe('igxCombo', () => {
fixture.detectChanges();
expect(firstVisibleItem.classList.contains(CSS_CLASS_FOCUSED)).toBeTruthy();
}));
it('should close the dropdown list on pressing Tab key', fakeAsync(() => {
it('should close the dropdown list on pressing Tab key and focus the next focusable element', fakeAsync(() => {
combo.toggle();
fixture.detectChanges();

const dropdownContent = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTENT}`));
const dropdownList = fixture.debugElement.query(By.css(`.${CSS_CLASS_DROPDOWNLIST_SCROLL}`)).nativeElement;
UIInteractions.triggerEventHandlerKeyDown('Tab', dropdownContent);
tick();
fixture.detectChanges();
expect(combo.collapsed).toBeTruthy();

combo.toggle();
fixture.detectChanges();
expect(combo.collapsed).toBeFalsy();

let focusedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_FOCUSED}`);
let selectedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_SELECTED}`);
expect(focusedItems.length).toEqual(0);
expect(selectedItems.length).toEqual(0);

UIInteractions.triggerEventHandlerKeyDown('ArrowDown', dropdownContent);
fixture.detectChanges();
focusedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_FOCUSED}`);
expect(focusedItems.length).toEqual(1);

UIInteractions.triggerEventHandlerKeyDown('Tab', dropdownContent);
tick();
fixture.detectChanges();
expect(combo.collapsed).toBeTruthy();
expect(document.activeElement).not.toEqual(combo.comboInput.nativeElement);

combo.toggle();
fixture.detectChanges();
expect(combo.collapsed).toBeFalsy();

UIInteractions.triggerEventHandlerKeyDown('ArrowDown', dropdownContent);
fixture.detectChanges();
focusedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_FOCUSED}`);
expect(focusedItems.length).toEqual(1);

UIInteractions.triggerEventHandlerKeyDown('Space', dropdownContent);
fixture.detectChanges();
selectedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_SELECTED}`);
expect(selectedItems.length).toEqual(1);

UIInteractions.triggerEventHandlerKeyDown('Tab', dropdownContent);
tick();
fixture.detectChanges();
expect(combo.collapsed).toBeTruthy();
expect(document.activeElement).not.toEqual(combo.comboInput.nativeElement);
}));
it('should clear the selection and preserve the focus when the combo is collapsed and Escape key is pressed', fakeAsync(() => {
combo.comboInput.nativeElement.focus();
fixture.detectChanges();
expect(document.activeElement).toEqual(combo.comboInput.nativeElement);

combo.select([combo.data[0][combo.valueKey]]);
expect(combo.selection.length).toEqual(1);
fixture.detectChanges();

combo.onEscape(UIInteractions.getKeyboardEvent('keydown', 'Escape'));
tick();
fixture.detectChanges();
expect(document.activeElement).toEqual(combo.comboInput.nativeElement);
expect(combo.selection.length).toEqual(0);
}));
it('should close the combo and preserve the focus when Escape key is pressed', fakeAsync(() => {
combo.comboInput.nativeElement.focus();
fixture.detectChanges();
expect(document.activeElement).toEqual(combo.comboInput.nativeElement);

combo.toggle();
fixture.detectChanges();
expect(combo.collapsed).toBeFalsy();

const dropdownContent = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTENT}`));

UIInteractions.triggerEventHandlerKeyDown('ArrowDown', dropdownContent);
fixture.detectChanges();

UIInteractions.triggerEventHandlerKeyDown('Escape', dropdownContent);
fixture.detectChanges();
expect(document.activeElement).toEqual(combo.comboInput.nativeElement);

tick();
fixture.detectChanges();
expect(combo.collapsed).toBeTruthy();
}));
});
describe('primitive data dropdown: ', () => {
Expand Down Expand Up @@ -2283,37 +2361,6 @@ describe('igxCombo', () => {
cancel: false
});
});
it('should toggle combo dropdown on Enter of the focused toggle icon', fakeAsync(() => {
spyOn(combo, 'toggle').and.callThrough();
const toggleBtn = fixture.debugElement.query(By.css(`.${CSS_CLASS_TOGGLEBUTTON}`));

UIInteractions.triggerEventHandlerKeyDown('Enter', toggleBtn);
tick();
fixture.detectChanges();
expect(combo.toggle).toHaveBeenCalledTimes(1);
expect(combo.collapsed).toEqual(false);

UIInteractions.triggerEventHandlerKeyDown('Enter', toggleBtn);
tick();
fixture.detectChanges();
expect(combo.toggle).toHaveBeenCalledTimes(2);
expect(combo.collapsed).toEqual(true);
}));
it('should clear the selection on Enter of the focused clear icon', () => {
const selectedItem_1 = combo.dropdown.items[1];
combo.toggle();
fixture.detectChanges();
simulateComboItemClick(1);
expect(combo.selection[0]).toEqual(selectedItem_1.value);
expect(combo.value[0]).toEqual(selectedItem_1.value[combo.valueKey]);

const clearBtn = fixture.debugElement.query(By.css(`.${CSS_CLASS_CLEARBUTTON}`));
UIInteractions.triggerEventHandlerKeyDown('Enter', clearBtn);
fixture.detectChanges();
expect(input.nativeElement.value).toEqual('');
expect(combo.selection.length).toEqual(0);
expect(combo.value.length).toEqual(0);
});
it('should not be able to select group header', () => {
spyOn(combo.selectionChanging, 'emit').and.callThrough();
combo.toggle();
Expand Down
32 changes: 11 additions & 21 deletions projects/igniteui-angular/src/lib/combo/combo.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
this.open();
}

@HostListener('keydown.Escape', ['$event'])
public onEscape(event: Event) {
if (this.collapsed) {
this.deselectAllItems(true, event);
}
}

/** @hidden @internal */
public get displaySearchInput(): boolean {
return !this.disableFiltering || this.allowCustomValues;
Expand Down Expand Up @@ -267,7 +274,10 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
/**
* @hidden @internal
*/
public clearInput(event: Event): void {
public handleClearItems(event: Event): void {
if (this.disabled) {
return;
}
this.deselectAllItems(true, event);
if (this.collapsed) {
this.getEditElement().focus();
Expand All @@ -277,26 +287,6 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
event.stopPropagation();
}

/**
* @hidden @internal
*/
public handleClearItems(event: Event): void {
if (this.disabled) {
return;
}
this.clearInput(event);
}

/**
* @hidden @internal
*/
public handleClearKeyDown(eventArgs: KeyboardEvent) {
if (eventArgs.key === 'Enter' || eventArgs.key === ' ') {
eventArgs.preventDefault();
this.clearInput(eventArgs);
}
}

/**
* Select defined items
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ export class IgxDropDownItemNavigationDirective implements IDropDownNavigationDi
if (navKeys.indexOf(key) === -1) { // If key has appropriate function in DD
return;
}
event.preventDefault();
event.stopPropagation();
if (key !== 'tab') { // Prevent default behavior for all keys except Tab
event.preventDefault();
event.stopPropagation();
}
} else { // If dropdown is closed, do nothing
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -626,21 +626,20 @@ export class QueryBuilderFunctions {
switch (i) {
case 0: expect(element).toHaveClass('igx-input-group__input'); break;
case 1: expect(element).toHaveClass('igx-input-group__input'); break;
case 2: expect(element).toHaveClass('igx-combo__toggle-button'); break;
case 3: expect(element).toHaveClass('igx-button');
case 2: expect(element).toHaveClass('igx-button');
expect(element.innerText).toContain('and'); break;
case 4: expect(element).toHaveClass('igx-chip'); break;
case 5: expect(element).toHaveClass('igx-icon'); break;
case 6: expect(element).toHaveClass('igx-chip__remove'); break;
case 7: expect(element).toHaveClass('igx-chip'); break;
case 8: expect(element).toHaveClass('igx-icon'); break;
case 9: expect(element).toHaveClass('igx-chip__remove'); break;
case 10: expect(element).toHaveClass('igx-chip'); break;
case 11: expect(element).toHaveClass('igx-icon'); break;
case 12: expect(element).toHaveClass('igx-chip__remove'); break;
case 13: expect(element).toHaveClass('igx-button');
case 3: expect(element).toHaveClass('igx-chip'); break;
case 4: expect(element).toHaveClass('igx-icon'); break;
case 5: expect(element).toHaveClass('igx-chip__remove'); break;
case 6: expect(element).toHaveClass('igx-chip'); break;
case 7: expect(element).toHaveClass('igx-icon'); break;
case 8: expect(element).toHaveClass('igx-chip__remove'); break;
case 9: expect(element).toHaveClass('igx-chip'); break;
case 10: expect(element).toHaveClass('igx-icon'); break;
case 11: expect(element).toHaveClass('igx-chip__remove'); break;
case 12: expect(element).toHaveClass('igx-button');
expect(element.innerText).toContain('Condition'); break;
case 14: expect(element).toHaveClass('igx-button');
case 13: expect(element).toHaveClass('igx-button');
expect(element.innerText).toContain('Group'); break;
}
i++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

@if (hasSelectedItem) {
<igx-suffix [attr.aria-label]="resourceStrings.igx_combo_clearItems_placeholder" class="igx-combo__clear-button"
(click)="handleClear($event)" (keydown)="handleClearKeyDown($event)" [tabindex]="disabled ? -1 : 0" role="button">
(click)="handleClear($event)">
@if (clearIconTemplate) {
<ng-container *ngTemplateOutlet="clearIconTemplate"></ng-container>
}
Expand All @@ -45,8 +45,7 @@
</igx-suffix>
}

<igx-suffix class="igx-combo__toggle-button" (click)="onClick($event)" (keydown)="handleToggleKeyDown($event)"
[tabindex]="disabled ? -1 : 0" role="button">
<igx-suffix class="igx-combo__toggle-button" (click)="onClick($event)">
@if (toggleIconTemplate) {
<ng-container *ngTemplateOutlet="toggleIconTemplate; context: {$implicit: collapsed}"></ng-container>
}
Expand Down
Loading
Loading