Skip to content

Make toggle button focusable and add aria label to the combo input #15921

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 21, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsBG = {
igx_combo_empty_message: 'Списъкът е празен',
igx_combo_filter_search_placeholder: 'Въведете термин за търсене',
igx_combo_addCustomValues_placeholder: 'Добавяне на елемент',
igx_combo_clearItems_placeholder: 'Изчистване на избора'
igx_combo_clearItems_placeholder: 'Изчистване на избора',
igx_combo_aria_label_options: 'Има избрани опции',
igx_combo_aria_label_no_options: 'Няма избрани опции'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsCS = {
igx_combo_empty_message: 'Seznam je prázdný',
igx_combo_filter_search_placeholder: 'Zadejte hledaný výraz',
igx_combo_addCustomValues_placeholder: 'Přidat položku',
igx_combo_clearItems_placeholder: 'Vymazat výběr'
igx_combo_clearItems_placeholder: 'Vymazat výběr',
igx_combo_aria_label_options: 'Vybrané možnosti',
igx_combo_aria_label_no_options: 'Žádné možnosti nejsou vybrány'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsDA = {
igx_combo_empty_message: 'Listen er tom',
igx_combo_filter_search_placeholder: 'Indtast en søgeterm',
igx_combo_addCustomValues_placeholder: 'Tilføj element',
igx_combo_clearItems_placeholder: 'Ryd markering'
igx_combo_clearItems_placeholder: 'Ryd markering',
igx_combo_aria_label_options: 'Valgte muligheder',
igx_combo_aria_label_no_options: 'Ingen valgte muligheder'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsDE = {
igx_combo_empty_message: 'Die Liste ist leer',
igx_combo_filter_search_placeholder: 'Suchbegriff eingeben',
igx_combo_addCustomValues_placeholder: 'Element hinzufügen',
igx_combo_clearItems_placeholder: 'Auswahl löschen'
igx_combo_clearItems_placeholder: 'Auswahl löschen',
igx_combo_aria_label_options: 'Ausgewählte Optionen',
igx_combo_aria_label_no_options: 'Keine Optionen ausgewählt'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsES = {
igx_combo_empty_message: 'La lista está vacía',
igx_combo_filter_search_placeholder: 'Escriba un término de búsqueda',
igx_combo_addCustomValues_placeholder: 'Agregar elemento',
igx_combo_clearItems_placeholder: 'Borrar selección'
igx_combo_clearItems_placeholder: 'Borrar selección',
igx_combo_aria_label_options: 'Opciones seleccionadas',
igx_combo_aria_label_no_options: 'No hay opciones seleccionadas'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsFR = {
igx_combo_empty_message: 'La liste est vide',
igx_combo_filter_search_placeholder: 'Entrez un terme de recherche',
igx_combo_addCustomValues_placeholder: 'Ajouter un élément',
igx_combo_clearItems_placeholder: 'Effacer la sélection'
igx_combo_clearItems_placeholder: 'Effacer la sélection',
igx_combo_aria_label_options: 'Options sélectionnées',
igx_combo_aria_label_no_options: 'Aucune option sélectionnée'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsHU = {
igx_combo_empty_message: 'Üres a lista',
igx_combo_filter_search_placeholder: 'Írjon be egy keresési kifejezést',
igx_combo_addCustomValues_placeholder: 'Elem hozzáadása',
igx_combo_clearItems_placeholder: 'Kiválasztás törlése'
igx_combo_clearItems_placeholder: 'Kiválasztás törlése',
igx_combo_aria_label_options: 'Kiválasztott lehetőségek',
igx_combo_aria_label_no_options: 'Nincsenek kiválasztott lehetőségek'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsIT = {
igx_combo_empty_message: 'L\'elenco è vuoto',
igx_combo_filter_search_placeholder: 'Immettere il testo di ricerca',
igx_combo_addCustomValues_placeholder: 'Aggiungi elemento',
igx_combo_clearItems_placeholder: 'Cancella selezione'
igx_combo_clearItems_placeholder: 'Cancella selezione',
igx_combo_aria_label_options: 'Opzioni selezionate',
igx_combo_aria_label_no_options: 'Nessuna opzione selezionata'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsJA = {
igx_combo_empty_message: 'リストが空です',
igx_combo_filter_search_placeholder: '検索条件の入力',
igx_combo_addCustomValues_placeholder: '項目の追加',
igx_combo_clearItems_placeholder: '選択のクリア'
igx_combo_clearItems_placeholder: '選択のクリア',
igx_combo_aria_label_options: '選択されたオプション',
igx_combo_aria_label_no_options: '選択されたオプションはありません'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsKO = {
igx_combo_empty_message: '목록이 비어 있음',
igx_combo_filter_search_placeholder: '검색어 입력',
igx_combo_addCustomValues_placeholder: '항목 추가',
igx_combo_clearItems_placeholder: '선택 지우기'
igx_combo_clearItems_placeholder: '선택 지우기',
igx_combo_aria_label_options: '선택된 옵션',
igx_combo_aria_label_no_options: '선택된 옵션 없음'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsNB = {
igx_combo_empty_message: 'Listen er tom',
igx_combo_filter_search_placeholder: 'Skriv inn søkeord',
igx_combo_addCustomValues_placeholder: 'Legg til element',
igx_combo_clearItems_placeholder: 'Fjern valg'
igx_combo_clearItems_placeholder: 'Fjern valg',
igx_combo_aria_label_options: 'Valgte alternativer',
igx_combo_aria_label_no_options: 'Ingen valgte alternativer'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsNL = {
igx_combo_empty_message: 'De lijst is leeg',
igx_combo_filter_search_placeholder: 'Typ een zoekterm',
igx_combo_addCustomValues_placeholder: 'Item toevoegen',
igx_combo_clearItems_placeholder: 'Selectie wissen'
igx_combo_clearItems_placeholder: 'Selectie wissen',
igx_combo_aria_label_options: 'Geselecteerde opties',
igx_combo_aria_label_no_options: 'Geen geselecteerde opties'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsPL = {
igx_combo_empty_message: 'Lista jest pusta',
igx_combo_filter_search_placeholder: 'Wprowadź tekst wyszukiwania',
igx_combo_addCustomValues_placeholder: 'Dodaj element',
igx_combo_clearItems_placeholder: 'Wyczyść wybór'
igx_combo_clearItems_placeholder: 'Wyczyść wybór',
igx_combo_aria_label_options: 'Wybrane opcje',
igx_combo_aria_label_no_options: 'Brak wybranych opcji'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsPT = {
igx_combo_empty_message: 'A lista está vazia',
igx_combo_filter_search_placeholder: 'Digite um termo de pesquisa',
igx_combo_addCustomValues_placeholder: 'Adicionar item',
igx_combo_clearItems_placeholder: 'Limpar seleção'
igx_combo_clearItems_placeholder: 'Limpar seleção',
igx_combo_aria_label_options: 'Opções selecionadas',
igx_combo_aria_label_no_options: 'Nenhuma opção selecionada'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsRO = {
igx_combo_empty_message: 'Lista este goală',
igx_combo_filter_search_placeholder: 'Introduceți termenul de căutare',
igx_combo_addCustomValues_placeholder: 'Adăugați element',
igx_combo_clearItems_placeholder: 'Ștergeți selecția'
igx_combo_clearItems_placeholder: 'Ștergeți selecția',
igx_combo_aria_label_options: 'Opțiuni selectate',
igx_combo_aria_label_no_options: 'Nicio opțiune selectată'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsSV = {
igx_combo_empty_message: 'Listan är tom',
igx_combo_filter_search_placeholder: 'Ange sökterm',
igx_combo_addCustomValues_placeholder: 'Lägg till objekt',
igx_combo_clearItems_placeholder: 'Rensa urval'
igx_combo_clearItems_placeholder: 'Rensa urval',
igx_combo_aria_label_options: 'Valda alternativ',
igx_combo_aria_label_no_options: 'Inga valda alternativ'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsTR = {
igx_combo_empty_message: 'Liste boş',
igx_combo_filter_search_placeholder: 'Arama terimi girin',
igx_combo_addCustomValues_placeholder: 'Öğe ekle',
igx_combo_clearItems_placeholder: 'Seçimi temizle'
igx_combo_clearItems_placeholder: 'Seçimi temizle',
igx_combo_aria_label_options: 'Seçilen seçenekler',
igx_combo_aria_label_no_options: 'Seçilen seçenek yok'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsZHHANS = {
igx_combo_empty_message: '列表为空',
igx_combo_filter_search_placeholder: '输入搜索字符串',
igx_combo_addCustomValues_placeholder: '添加项目',
igx_combo_clearItems_placeholder: '清除选择'
igx_combo_clearItems_placeholder: '清除选择',
igx_combo_aria_label_options: '选定的选项',
igx_combo_aria_label_no_options: '没有选定的选项'
} satisfies MakeRequired<IComboResourceStrings>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const ComboResourceStringsZHHANT = {
igx_combo_empty_message: '清單是空的',
igx_combo_filter_search_placeholder: '輸入搜尋字串',
igx_combo_addCustomValues_placeholder: '新增項目',
igx_combo_clearItems_placeholder: '清除選擇'
igx_combo_clearItems_placeholder: '清除選擇',
igx_combo_aria_label_options: '已選擇的選項',
igx_combo_aria_label_no_options: '沒有已選擇的選項'
} satisfies MakeRequired<IComboResourceStrings>;
14 changes: 14 additions & 0 deletions projects/igniteui-angular/src/lib/combo/combo.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,20 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
}
}

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

/** @hidden @internal */
public getAriaLabel(): string {
return this.displayValue ? this.resourceStrings.igx_combo_aria_label_options : this.resourceStrings.igx_combo_aria_label_no_options;
}


/** @hidden @internal */
public registerOnChange(fn: any): void {
this._onChangeCallback = fn;
Expand Down
3 changes: 2 additions & 1 deletion projects/igniteui-angular/src/lib/combo/combo.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
role="combobox" aria-haspopup="listbox"
[attr.aria-expanded]="!dropdown.collapsed" [attr.aria-controls]="dropdown.listId"
[attr.aria-labelledby]="ariaLabelledBy || label?.id || placeholder"
[attr.aria-label]="getAriaLabel()"
(blur)="onBlur()" />
<ng-container ngProjectAs="igx-suffix">
<ng-content select="igx-suffix,[igxSuffix]"></ng-content>
Expand All @@ -28,7 +29,7 @@
}
</igx-suffix>
}
<igx-suffix class="igx-combo__toggle-button">
<igx-suffix class="igx-combo__toggle-button" (keydown)="handleToggleKeyDown($event)" [tabindex]="disabled ? -1 : 0" role="button">
@if (toggleIconTemplate) {
<ng-container *ngTemplateOutlet="toggleIconTemplate; context: {$implicit: collapsed}"></ng-container>
}
Expand Down
21 changes: 21 additions & 0 deletions projects/igniteui-angular/src/lib/combo/combo.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,7 @@ describe('igxCombo', () => {
expect(input.nativeElement.getAttribute('aria-expanded')).toMatch('false');
expect(input.nativeElement.getAttribute('aria-controls')).toEqual(combo.dropdown.listId);
expect(input.nativeElement.getAttribute('aria-labelledby')).toEqual(combo.placeholder);
expect(input.nativeElement.getAttribute('aria-label')).toEqual('No options selected');

const dropdown = fixture.debugElement.query(By.css(`div[role="listbox"]`));
expect(dropdown.nativeElement.getAttribute('aria-labelledby')).toEqual(combo.placeholder);
Expand All @@ -1079,6 +1080,10 @@ describe('igxCombo', () => {
tick();
fixture.detectChanges();
expect(list.nativeElement.getAttribute('aria-activedescendant')).toEqual(combo.dropdown.focusedItem.id);

combo.select(['Illinois', 'Mississippi', 'Ohio']);
fixture.detectChanges();
expect(input.nativeElement.getAttribute('aria-label')).toEqual('Selected options');
}));
it('should render aria-expanded attribute properly', fakeAsync(() => {
expect(input.nativeElement.getAttribute('aria-expanded')).toMatch('false');
Expand Down Expand Up @@ -2269,6 +2274,22 @@ 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ export interface IComboResourceStrings {
igx_combo_filter_search_placeholder?: string;
igx_combo_addCustomValues_placeholder?: string;
igx_combo_clearItems_placeholder?: string;
igx_combo_aria_label_options?: string;
igx_combo_aria_label_no_options?: string;
}

export const ComboResourceStringsEN: IComboResourceStrings = {
igx_combo_empty_message: 'The list is empty',
igx_combo_filter_search_placeholder: 'Enter a Search Term',
igx_combo_addCustomValues_placeholder: 'Add Item',
igx_combo_clearItems_placeholder: 'Clear Selection'

igx_combo_clearItems_placeholder: 'Clear Selection',
igx_combo_aria_label_options: 'Selected options',
igx_combo_aria_label_no_options: 'No options selected'
};
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@
%igx-combo__toggle-button {
color: var-get($theme, 'toggle-button-foreground-focus');
background: var-get($theme, 'toggle-button-background-focus');

&:focus {
color: color($color: 'secondary');
}
}

%igx-combo__clear-button {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -626,20 +626,21 @@ 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-button');
case 2: expect(element).toHaveClass('igx-combo__toggle-button'); break;
case 3: expect(element).toHaveClass('igx-button');
expect(element.innerText).toContain('and'); break;
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 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');
expect(element.innerText).toContain('Condition'); break;
case 14: 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 @@ -17,6 +17,7 @@
[attr.aria-expanded]="!this.dropdown.collapsed" [attr.aria-controls]="this.dropdown.listId"
[attr.aria-labelledby]="this.ariaLabelledBy || this.label?.id || this.placeholder"
[attr.placeholder]="placeholder" [disabled]="disabled" [igxTextSelection]="!composing"
[attr.aria-label]="getAriaLabel()"
(input)="handleInputChange($event)" (click)="handleInputClick()"
(keyup)="handleKeyUp($event)" (keydown)="handleKeyDown($event)" (blur)="onBlur()" (paste)="handleInputChange($event)"/>

Expand Down Expand Up @@ -44,7 +45,8 @@
</igx-suffix>
}

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