Skip to content

Commit dbb80e9

Browse files
committed
feat(combo): update Tab and Escape key behavior
1 parent 894a1d1 commit dbb80e9

File tree

11 files changed

+192
-132
lines changed

11 files changed

+192
-132
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ All notable changes for each version of this project will be documented in this
77

88
### New Features
99

10+
- `IgxCombo`, `IgxSimpleCombo`
11+
- 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".
12+
1013
- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid`
1114
- 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.
1215

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
168168
/**
169169
* @hidden @internal
170170
*/
171-
public override onItemActionKey(key: DropDownActionKey) {
171+
public override onItemActionKey(key: DropDownActionKey, event?: KeyboardEvent) {
172172
switch (key) {
173173
case DropDownActionKey.ENTER:
174174
this.handleEnter();
@@ -177,8 +177,10 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I
177177
this.handleSpace();
178178
break;
179179
case DropDownActionKey.ESCAPE:
180-
case DropDownActionKey.TAB:
181180
this.close();
181+
break;
182+
case DropDownActionKey.TAB:
183+
this.close(event);
182184
}
183185
}
184186

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,7 +1208,8 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
12081208
return;
12091209
}
12101210
this.searchValue = '';
1211-
if (!e.event) {
1211+
const isTab = (e.event as KeyboardEvent)?.key === 'Tab';
1212+
if (!e.event || isTab) {
12121213
this.comboInput?.nativeElement.focus();
12131214
} else {
12141215
this._onTouchedCallback();
@@ -1228,13 +1229,8 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
12281229
event.stopPropagation();
12291230
this.close();
12301231
}
1231-
}
1232-
1233-
/** @hidden @internal */
1234-
public handleToggleKeyDown(eventArgs: KeyboardEvent) {
1235-
if (eventArgs.key === 'Enter' || eventArgs.key === ' ') {
1236-
eventArgs.preventDefault();
1237-
this.toggle();
1232+
if (event.key === "Tab") {
1233+
this.close();
12381234
}
12391235
}
12401236

projects/igniteui-angular/src/lib/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/src/lib/combo/combo.component.spec.ts

Lines changed: 79 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,15 +1944,93 @@ describe('igxCombo', () => {
19441944
fixture.detectChanges();
19451945
expect(firstVisibleItem.classList.contains(CSS_CLASS_FOCUSED)).toBeTruthy();
19461946
}));
1947-
it('should close the dropdown list on pressing Tab key', fakeAsync(() => {
1947+
it('should close the dropdown list on pressing Tab key and focus the next focusable element', fakeAsync(() => {
19481948
combo.toggle();
19491949
fixture.detectChanges();
19501950

19511951
const dropdownContent = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTENT}`));
1952+
const dropdownList = fixture.debugElement.query(By.css(`.${CSS_CLASS_DROPDOWNLIST_SCROLL}`)).nativeElement;
1953+
UIInteractions.triggerEventHandlerKeyDown('Tab', dropdownContent);
1954+
tick();
1955+
fixture.detectChanges();
1956+
expect(combo.collapsed).toBeTruthy();
1957+
1958+
combo.toggle();
1959+
fixture.detectChanges();
1960+
expect(combo.collapsed).toBeFalsy();
1961+
1962+
let focusedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_FOCUSED}`);
1963+
let selectedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_SELECTED}`);
1964+
expect(focusedItems.length).toEqual(0);
1965+
expect(selectedItems.length).toEqual(0);
1966+
1967+
UIInteractions.triggerEventHandlerKeyDown('ArrowDown', dropdownContent);
1968+
fixture.detectChanges();
1969+
focusedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_FOCUSED}`);
1970+
expect(focusedItems.length).toEqual(1);
1971+
19521972
UIInteractions.triggerEventHandlerKeyDown('Tab', dropdownContent);
19531973
tick();
19541974
fixture.detectChanges();
19551975
expect(combo.collapsed).toBeTruthy();
1976+
expect(document.activeElement).not.toEqual(combo.comboInput.nativeElement);
1977+
1978+
combo.toggle();
1979+
fixture.detectChanges();
1980+
expect(combo.collapsed).toBeFalsy();
1981+
1982+
UIInteractions.triggerEventHandlerKeyDown('ArrowDown', dropdownContent);
1983+
fixture.detectChanges();
1984+
focusedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_FOCUSED}`);
1985+
expect(focusedItems.length).toEqual(1);
1986+
1987+
UIInteractions.triggerEventHandlerKeyDown('Space', dropdownContent);
1988+
fixture.detectChanges();
1989+
selectedItems = dropdownList.querySelectorAll(`.${CSS_CLASS_SELECTED}`);
1990+
expect(selectedItems.length).toEqual(1);
1991+
1992+
UIInteractions.triggerEventHandlerKeyDown('Tab', dropdownContent);
1993+
tick();
1994+
fixture.detectChanges();
1995+
expect(combo.collapsed).toBeTruthy();
1996+
expect(document.activeElement).not.toEqual(combo.comboInput.nativeElement);
1997+
}));
1998+
it('should clear the selection and preserve the focus when the combo is collapsed and Escape key is pressed', fakeAsync(() => {
1999+
combo.comboInput.nativeElement.focus();
2000+
fixture.detectChanges();
2001+
expect(document.activeElement).toEqual(combo.comboInput.nativeElement);
2002+
2003+
combo.select([combo.data[0][combo.valueKey]]);
2004+
expect(combo.selection.length).toEqual(1);
2005+
fixture.detectChanges();
2006+
2007+
combo.onEscape(UIInteractions.getKeyboardEvent('keydown', 'Escape'));
2008+
tick();
2009+
fixture.detectChanges();
2010+
expect(document.activeElement).toEqual(combo.comboInput.nativeElement);
2011+
expect(combo.selection.length).toEqual(0);
2012+
}));
2013+
it('should close the combo and preserve the focus when Escape key is pressed', fakeAsync(() => {
2014+
combo.comboInput.nativeElement.focus();
2015+
fixture.detectChanges();
2016+
expect(document.activeElement).toEqual(combo.comboInput.nativeElement);
2017+
2018+
combo.toggle();
2019+
fixture.detectChanges();
2020+
expect(combo.collapsed).toBeFalsy();
2021+
2022+
const dropdownContent = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTENT}`));
2023+
2024+
UIInteractions.triggerEventHandlerKeyDown('ArrowDown', dropdownContent);
2025+
fixture.detectChanges();
2026+
2027+
UIInteractions.triggerEventHandlerKeyDown('Escape', dropdownContent);
2028+
fixture.detectChanges();
2029+
expect(document.activeElement).toEqual(combo.comboInput.nativeElement);
2030+
2031+
tick();
2032+
fixture.detectChanges();
2033+
expect(combo.collapsed).toBeTruthy();
19562034
}));
19572035
});
19582036
describe('primitive data dropdown: ', () => {
@@ -2283,37 +2361,6 @@ describe('igxCombo', () => {
22832361
cancel: false
22842362
});
22852363
});
2286-
it('should toggle combo dropdown on Enter of the focused toggle icon', fakeAsync(() => {
2287-
spyOn(combo, 'toggle').and.callThrough();
2288-
const toggleBtn = fixture.debugElement.query(By.css(`.${CSS_CLASS_TOGGLEBUTTON}`));
2289-
2290-
UIInteractions.triggerEventHandlerKeyDown('Enter', toggleBtn);
2291-
tick();
2292-
fixture.detectChanges();
2293-
expect(combo.toggle).toHaveBeenCalledTimes(1);
2294-
expect(combo.collapsed).toEqual(false);
2295-
2296-
UIInteractions.triggerEventHandlerKeyDown('Enter', toggleBtn);
2297-
tick();
2298-
fixture.detectChanges();
2299-
expect(combo.toggle).toHaveBeenCalledTimes(2);
2300-
expect(combo.collapsed).toEqual(true);
2301-
}));
2302-
it('should clear the selection on Enter of the focused clear icon', () => {
2303-
const selectedItem_1 = combo.dropdown.items[1];
2304-
combo.toggle();
2305-
fixture.detectChanges();
2306-
simulateComboItemClick(1);
2307-
expect(combo.selection[0]).toEqual(selectedItem_1.value);
2308-
expect(combo.value[0]).toEqual(selectedItem_1.value[combo.valueKey]);
2309-
2310-
const clearBtn = fixture.debugElement.query(By.css(`.${CSS_CLASS_CLEARBUTTON}`));
2311-
UIInteractions.triggerEventHandlerKeyDown('Enter', clearBtn);
2312-
fixture.detectChanges();
2313-
expect(input.nativeElement.value).toEqual('');
2314-
expect(combo.selection.length).toEqual(0);
2315-
expect(combo.value.length).toEqual(0);
2316-
});
23172364
it('should not be able to select group header', () => {
23182365
spyOn(combo.selectionChanging, 'emit').and.callThrough();
23192366
combo.toggle();

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

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

202+
@HostListener('keydown.Escape', ['$event'])
203+
public onEscape(event: Event) {
204+
if (this.collapsed) {
205+
this.deselectAllItems(true, event);
206+
}
207+
}
208+
202209
/** @hidden @internal */
203210
public get displaySearchInput(): boolean {
204211
return !this.disableFiltering || this.allowCustomValues;
@@ -267,7 +274,10 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
267274
/**
268275
* @hidden @internal
269276
*/
270-
public clearInput(event: Event): void {
277+
public handleClearItems(event: Event): void {
278+
if (this.disabled) {
279+
return;
280+
}
271281
this.deselectAllItems(true, event);
272282
if (this.collapsed) {
273283
this.getEditElement().focus();
@@ -277,26 +287,6 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
277287
event.stopPropagation();
278288
}
279289

280-
/**
281-
* @hidden @internal
282-
*/
283-
public handleClearItems(event: Event): void {
284-
if (this.disabled) {
285-
return;
286-
}
287-
this.clearInput(event);
288-
}
289-
290-
/**
291-
* @hidden @internal
292-
*/
293-
public handleClearKeyDown(eventArgs: KeyboardEvent) {
294-
if (eventArgs.key === 'Enter' || eventArgs.key === ' ') {
295-
eventArgs.preventDefault();
296-
this.clearInput(eventArgs);
297-
}
298-
}
299-
300290
/**
301291
* Select defined items
302292
*

projects/igniteui-angular/src/lib/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/src/lib/query-builder/query-builder-functions.spec.ts

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

projects/igniteui-angular/src/lib/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)