Skip to content

Commit c3cb139

Browse files
MonikaKirkovaLipatarkaraivanov
authored
Update Combo and Simple Combo Keyboard Navigation & Add Escape Key behavior (#16246)
--------- Co-authored-by: Nikolay Alipiev <nikolay.alipiev@gmail.com> Co-authored-by: Radoslav Karaivanov <rkaraivanov@infragistics.com>
1 parent ba8a2b2 commit c3cb139

File tree

11 files changed

+197
-133
lines changed

11 files changed

+197
-133
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes for each version of this project will be documented in this
44

55
## 21.2.0
66

7+
### New Features
8+
9+
- `IgxCombo`, `IgxSimpleCombo`
10+
- 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".
11+
712
### Breaking Changes
813

914
- `igxForOf`, `igxGrid`, `igxTreeGrid`, `igxHierarchicalGrid`, `igxPivotGrid`

projects/igniteui-angular/combo/src/combo/combo-dropdown.component.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
155155
/**
156156
* @hidden @internal
157157
*/
158-
public override onItemActionKey(key: DropDownActionKey) {
158+
public override onItemActionKey(key: DropDownActionKey, event?: KeyboardEvent) {
159159
switch (key) {
160160
case DropDownActionKey.ENTER:
161161
this.handleEnter();
@@ -164,8 +164,10 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
164164
this.handleSpace();
165165
break;
166166
case DropDownActionKey.ESCAPE:
167-
case DropDownActionKey.TAB:
168167
this.close();
168+
break;
169+
case DropDownActionKey.TAB:
170+
this.close(event);
169171
}
170172
}
171173

projects/igniteui-angular/combo/src/combo/combo.common.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,7 +1221,8 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
12211221
return;
12221222
}
12231223
this.searchValue = '';
1224-
if (!e.event) {
1224+
const isTab = (e.event as KeyboardEvent)?.key === 'Tab';
1225+
if (!e.event || isTab) {
12251226
this.comboInput?.nativeElement.focus();
12261227
} else {
12271228
this._onTouchedCallback();
@@ -1241,13 +1242,8 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
12411242
event.stopPropagation();
12421243
this.close();
12431244
}
1244-
}
1245-
1246-
/** @hidden @internal */
1247-
public handleToggleKeyDown(eventArgs: KeyboardEvent) {
1248-
if (eventArgs.key === 'Enter' || eventArgs.key === ' ') {
1249-
eventArgs.preventDefault();
1250-
this.toggle();
1245+
if (event.key === 'Tab') {
1246+
this.close();
12511247
}
12521248
}
12531249

projects/igniteui-angular/combo/src/combo/combo.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
</ng-container>
2121
@if (displayValue) {
2222
<igx-suffix [attr.aria-label]="resourceStrings.igx_combo_clearItems_placeholder" class="igx-combo__clear-button"
23-
(click)="handleClearItems($event)" (keydown)="handleClearKeyDown($event)" [tabindex]="disabled ? -1 : 0" role="button">
23+
(click)="handleClearItems($event)">
2424
@if (clearIconTemplate) {
2525
<ng-container *ngTemplateOutlet="clearIconTemplate"></ng-container>
2626
}
@@ -29,7 +29,7 @@
2929
}
3030
</igx-suffix>
3131
}
32-
<igx-suffix class="igx-combo__toggle-button" (keydown)="handleToggleKeyDown($event)" [tabindex]="disabled ? -1 : 0" role="button">
32+
<igx-suffix class="igx-combo__toggle-button">
3333
@if (toggleIconTemplate) {
3434
<ng-container *ngTemplateOutlet="toggleIconTemplate; context: {$implicit: collapsed}"></ng-container>
3535
}

projects/igniteui-angular/combo/src/combo/combo.component.spec.ts

Lines changed: 79 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1798,15 +1798,93 @@ describe('igxCombo', () => {
17981798
fixture.detectChanges();
17991799
expect(firstVisibleItem.classList.contains(CSS_CLASS_FOCUSED)).toBeTruthy();
18001800
}));
1801-
it('should close the dropdown list on pressing Tab key', fakeAsync(() => {
1801+
it('should close the dropdown list on pressing Tab key and focus the next focusable element', fakeAsync(() => {
18021802
combo.toggle();
18031803
fixture.detectChanges();
18041804

18051805
const dropdownContent = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTENT}`));
1806+
const dropdownList = fixture.debugElement.query(By.css(`.${CSS_CLASS_DROPDOWNLIST_SCROLL}`)).nativeElement;
1807+
UIInteractions.triggerEventHandlerKeyDown('Tab', dropdownContent);
1808+
tick();
1809+
fixture.detectChanges();
1810+
expect(combo.collapsed).toBeTruthy();
1811+
1812+
combo.toggle();
1813+
fixture.detectChanges();
1814+
expect(combo.collapsed).toBeFalsy();
1815+
1816+
let focusedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_FOCUSED}`);
1817+
let selectedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_SELECTED}`);
1818+
expect(focusedItems.length).toEqual(0);
1819+
expect(selectedItems.length).toEqual(0);
1820+
1821+
UIInteractions.triggerEventHandlerKeyDown('ArrowDown', dropdownContent);
1822+
fixture.detectChanges();
1823+
focusedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_FOCUSED}`);
1824+
expect(focusedItems.length).toEqual(1);
1825+
18061826
UIInteractions.triggerEventHandlerKeyDown('Tab', dropdownContent);
18071827
tick();
18081828
fixture.detectChanges();
18091829
expect(combo.collapsed).toBeTruthy();
1830+
expect(document.activeElement).not.toEqual(combo.comboInput.nativeElement);
1831+
1832+
combo.toggle();
1833+
fixture.detectChanges();
1834+
expect(combo.collapsed).toBeFalsy();
1835+
1836+
UIInteractions.triggerEventHandlerKeyDown('ArrowDown', dropdownContent);
1837+
fixture.detectChanges();
1838+
focusedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_FOCUSED}`);
1839+
expect(focusedItems.length).toEqual(1);
1840+
1841+
UIInteractions.triggerEventHandlerKeyDown('Space', dropdownContent);
1842+
fixture.detectChanges();
1843+
selectedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_SELECTED}`);
1844+
expect(selectedItems.length).toEqual(1);
1845+
1846+
UIInteractions.triggerEventHandlerKeyDown('Tab', dropdownContent);
1847+
tick();
1848+
fixture.detectChanges();
1849+
expect(combo.collapsed).toBeTruthy();
1850+
expect(document.activeElement).not.toEqual(combo.comboInput.nativeElement);
1851+
}));
1852+
it('should clear the selection and preserve the focus when the combo is collapsed and Escape key is pressed', fakeAsync(() => {
1853+
combo.comboInput.nativeElement.focus();
1854+
fixture.detectChanges();
1855+
expect(document.activeElement).toEqual(combo.comboInput.nativeElement);
1856+
1857+
combo.select([combo.data[0][combo.valueKey]]);
1858+
expect(combo.selection.length).toEqual(1);
1859+
fixture.detectChanges();
1860+
1861+
combo.onEscape(UIInteractions.getKeyboardEvent('keydown', 'Escape'));
1862+
tick();
1863+
fixture.detectChanges();
1864+
expect(document.activeElement).toEqual(combo.comboInput.nativeElement);
1865+
expect(combo.selection.length).toEqual(0);
1866+
}));
1867+
it('should close the combo and preserve the focus when Escape key is pressed', fakeAsync(() => {
1868+
combo.comboInput.nativeElement.focus();
1869+
fixture.detectChanges();
1870+
expect(document.activeElement).toEqual(combo.comboInput.nativeElement);
1871+
1872+
combo.toggle();
1873+
fixture.detectChanges();
1874+
expect(combo.collapsed).toBeFalsy();
1875+
1876+
const dropdownContent = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTENT}`));
1877+
1878+
UIInteractions.triggerEventHandlerKeyDown('ArrowDown', dropdownContent);
1879+
fixture.detectChanges();
1880+
1881+
UIInteractions.triggerEventHandlerKeyDown('Escape', dropdownContent);
1882+
fixture.detectChanges();
1883+
expect(document.activeElement).toEqual(combo.comboInput.nativeElement);
1884+
1885+
tick();
1886+
fixture.detectChanges();
1887+
expect(combo.collapsed).toBeTruthy();
18101888
}));
18111889
});
18121890
describe('primitive data dropdown: ', () => {
@@ -2137,37 +2215,6 @@ describe('igxCombo', () => {
21372215
cancel: false
21382216
});
21392217
});
2140-
it('should toggle combo dropdown on Enter of the focused toggle icon', fakeAsync(() => {
2141-
spyOn(combo, 'toggle').and.callThrough();
2142-
const toggleBtn = fixture.debugElement.query(By.css(`.${CSS_CLASS_TOGGLEBUTTON}`));
2143-
2144-
UIInteractions.triggerEventHandlerKeyDown('Enter', toggleBtn);
2145-
tick();
2146-
fixture.detectChanges();
2147-
expect(combo.toggle).toHaveBeenCalledTimes(1);
2148-
expect(combo.collapsed).toEqual(false);
2149-
2150-
UIInteractions.triggerEventHandlerKeyDown('Enter', toggleBtn);
2151-
tick();
2152-
fixture.detectChanges();
2153-
expect(combo.toggle).toHaveBeenCalledTimes(2);
2154-
expect(combo.collapsed).toEqual(true);
2155-
}));
2156-
it('should clear the selection on Enter of the focused clear icon', () => {
2157-
const selectedItem_1 = combo.dropdown.items[1];
2158-
combo.toggle();
2159-
fixture.detectChanges();
2160-
simulateComboItemClick(1);
2161-
expect(combo.selection[0]).toEqual(selectedItem_1.value);
2162-
expect(combo.value[0]).toEqual(selectedItem_1.value[combo.valueKey]);
2163-
2164-
const clearBtn = fixture.debugElement.query(By.css(`.${CSS_CLASS_CLEARBUTTON}`));
2165-
UIInteractions.triggerEventHandlerKeyDown('Enter', clearBtn);
2166-
fixture.detectChanges();
2167-
expect(input.nativeElement.value).toEqual('');
2168-
expect(combo.selection.length).toEqual(0);
2169-
expect(combo.value.length).toEqual(0);
2170-
});
21712218
it('should not be able to select group header', () => {
21722219
spyOn(combo.selectionChanging, 'emit').and.callThrough();
21732220
combo.toggle();

projects/igniteui-angular/combo/src/combo/combo.component.ts

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,13 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
185185
this.open();
186186
}
187187

188+
@HostListener('keydown.Escape', ['$event'])
189+
public onEscape(event: Event) {
190+
if (this.collapsed) {
191+
this.deselectAllItems(true, event);
192+
}
193+
}
194+
188195
/** @hidden @internal */
189196
public get displaySearchInput(): boolean {
190197
return !this.disableFiltering || this.allowCustomValues;
@@ -253,7 +260,10 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
253260
/**
254261
* @hidden @internal
255262
*/
256-
public clearInput(event: Event): void {
263+
public handleClearItems(event: Event): void {
264+
if (this.disabled) {
265+
return;
266+
}
257267
this.deselectAllItems(true, event);
258268
if (this.collapsed) {
259269
this.getEditElement().focus();
@@ -263,26 +273,6 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
263273
event.stopPropagation();
264274
}
265275

266-
/**
267-
* @hidden @internal
268-
*/
269-
public handleClearItems(event: Event): void {
270-
if (this.disabled) {
271-
return;
272-
}
273-
this.clearInput(event);
274-
}
275-
276-
/**
277-
* @hidden @internal
278-
*/
279-
public handleClearKeyDown(eventArgs: KeyboardEvent) {
280-
if (eventArgs.key === 'Enter' || eventArgs.key === ' ') {
281-
eventArgs.preventDefault();
282-
this.clearInput(eventArgs);
283-
}
284-
}
285-
286276
/**
287277
* Select defined items
288278
*

projects/igniteui-angular/drop-down/src/drop-down/drop-down-navigation.directive.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,10 @@ export class IgxDropDownItemNavigationDirective implements IDropDownNavigationDi
7171
if (navKeys.indexOf(key) === -1) { // If key has appropriate function in DD
7272
return;
7373
}
74-
event.preventDefault();
75-
event.stopPropagation();
74+
if (key !== 'tab') { // Prevent default behavior for all keys except Tab
75+
event.preventDefault();
76+
event.stopPropagation();
77+
}
7678
} else { // If dropdown is closed, do nothing
7779
return;
7880
}

projects/igniteui-angular/query-builder/src/query-builder/query-builder-functions.spec.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -628,21 +628,20 @@ export class QueryBuilderFunctions {
628628
switch (i) {
629629
case 0: expect(element).toHaveClass('igx-input-group__input'); break;
630630
case 1: expect(element).toHaveClass('igx-input-group__input'); break;
631-
case 2: expect(element).toHaveClass('igx-combo__toggle-button'); break;
632-
case 3: expect(element).toHaveClass('igx-button');
631+
case 2: expect(element).toHaveClass('igx-button');
633632
expect(element.innerText).toContain('and'); break;
634-
case 4: expect(element).toHaveClass('igx-chip'); break;
635-
case 5: expect(element).toHaveClass('igx-icon'); break;
636-
case 6: expect(element).toHaveClass('igx-chip__remove'); break;
637-
case 7: expect(element).toHaveClass('igx-chip'); break;
638-
case 8: expect(element).toHaveClass('igx-icon'); break;
639-
case 9: expect(element).toHaveClass('igx-chip__remove'); break;
640-
case 10: expect(element).toHaveClass('igx-chip'); break;
641-
case 11: expect(element).toHaveClass('igx-icon'); break;
642-
case 12: expect(element).toHaveClass('igx-chip__remove'); break;
643-
case 13: expect(element).toHaveClass('igx-button');
633+
case 3: expect(element).toHaveClass('igx-chip'); break;
634+
case 4: expect(element).toHaveClass('igx-icon'); break;
635+
case 5: expect(element).toHaveClass('igx-chip__remove'); break;
636+
case 6: expect(element).toHaveClass('igx-chip'); break;
637+
case 7: expect(element).toHaveClass('igx-icon'); break;
638+
case 8: expect(element).toHaveClass('igx-chip__remove'); break;
639+
case 9: expect(element).toHaveClass('igx-chip'); break;
640+
case 10: expect(element).toHaveClass('igx-icon'); break;
641+
case 11: expect(element).toHaveClass('igx-chip__remove'); break;
642+
case 12: expect(element).toHaveClass('igx-button');
644643
expect(element.innerText).toContain('Condition'); break;
645-
case 14: expect(element).toHaveClass('igx-button');
644+
case 13: expect(element).toHaveClass('igx-button');
646645
expect(element.innerText).toContain('Group'); break;
647646
}
648647
i++;

projects/igniteui-angular/simple-combo/src/simple-combo/simple-combo.component.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

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

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

0 commit comments

Comments
 (0)