Skip to content

Commit a9655af

Browse files
saibaburaaviAdam Bennettadambennettpavankjadda
authored
feat: Improve tab focus on clear input with global config (ng-select#2572)
* feat: Improve tab focus on clear input with global config ng-select#2410 * Convert tabFocusOnClearButton to signal * Convert finalTabFocus to private variable * feat(ng-select): refactor tab focus handling for clear button --------- Co-authored-by: Adam Bennett <adam.bennett@lauramac.com> Co-authored-by: Adam Bennett <nyoxidetwitter@gmail.com> Co-authored-by: Pavan Kumar Jadda <17564080+pavankjadda@users.noreply.github.com>
1 parent c1d5c59 commit a9655af

File tree

4 files changed

+251
-74
lines changed

4 files changed

+251
-74
lines changed

src/ng-select/lib/config.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ export class NgSelectConfig {
1717
appearance = 'underline';
1818
clearSearchOnAdd: boolean;
1919
deselectOnClick: boolean;
20+
tabFocusOnClear = true;
2021
}

src/ng-select/lib/ng-select.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
class="ng-clear-wrapper"
7070
role="button"
7171
tabindex="0"
72-
[attr.tabindex]="tabFocusOnClearButton() ? 0 : -1"
72+
[attr.tabindex]="tabFocusOnClear() ? 0 : -1"
7373
title="{{ clearAllText }}"
7474
#clearButton>
7575
<span class="ng-clear" aria-hidden="true">×</span>

src/ng-select/lib/ng-select.component.spec.ts

Lines changed: 239 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1779,76 +1779,6 @@ describe('NgSelectComponent', () => {
17791779
});
17801780
});
17811781

1782-
describe('tab', () => {
1783-
it('should close dropdown when there are no items', fakeAsync(() => {
1784-
select.filter('random stuff');
1785-
tick(200);
1786-
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
1787-
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
1788-
expect(select.isOpen).toBeFalsy();
1789-
}));
1790-
1791-
it('should close dropdown when [selectOnTab]="false"', fakeAsync(() => {
1792-
fixture.componentInstance.selectOnTab = false;
1793-
tickAndDetectChanges(fixture);
1794-
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
1795-
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
1796-
expect(select.selectedItems).toEqual([]);
1797-
expect(select.isOpen).toBeFalsy();
1798-
}));
1799-
1800-
it('should close dropdown and keep selected value', fakeAsync(() => {
1801-
fixture.componentInstance.selectedCity = fixture.componentInstance.cities[0];
1802-
tickAndDetectChanges(fixture);
1803-
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
1804-
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
1805-
tickAndDetectChanges(fixture);
1806-
const result = [
1807-
jasmine.objectContaining({
1808-
value: fixture.componentInstance.cities[0],
1809-
}),
1810-
];
1811-
expect(select.selectedItems).toEqual(result);
1812-
expect(select.isOpen).toBeFalsy();
1813-
}));
1814-
1815-
it('should mark first item on filter when tab', fakeAsync(() => {
1816-
tick(200);
1817-
fixture.componentInstance.select.filter('pab');
1818-
tick(200);
1819-
1820-
const result = jasmine.objectContaining({
1821-
value: fixture.componentInstance.cities[2],
1822-
});
1823-
expect(fixture.componentInstance.select.itemsList.markedItem).toEqual(result);
1824-
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
1825-
expect(fixture.componentInstance.select.selectedItems).toEqual([result]);
1826-
}));
1827-
1828-
it('should focus on clear button when tab pressed while not opened and clear showing', fakeAsync(() => {
1829-
selectOption(fixture, KeyCode.ArrowDown, 0);
1830-
tickAndDetectChanges(fixture);
1831-
expect(select.showClear()).toBeTruthy();
1832-
1833-
select.searchInput.nativeElement.focus();
1834-
const focusOnClear = spyOn(select, 'focusOnClear');
1835-
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
1836-
expect(focusOnClear).toHaveBeenCalled();
1837-
}));
1838-
1839-
it('should not focus on clear button when tab pressed if [tabFocusOnClearButton]="false"', fakeAsync(() => {
1840-
fixture.componentInstance.tabFocusOnClearButton = false;
1841-
selectOption(fixture, KeyCode.ArrowDown, 0);
1842-
tickAndDetectChanges(fixture);
1843-
expect(select.showClear()).toBeTruthy();
1844-
1845-
select.searchInput.nativeElement.focus();
1846-
const focusOnClear = spyOn(select, 'focusOnClear');
1847-
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
1848-
expect(focusOnClear).not.toHaveBeenCalled();
1849-
}));
1850-
});
1851-
18521782
describe('backspace', () => {
18531783
it('should remove selected value', fakeAsync(() => {
18541784
fixture.componentInstance.selectedCity = fixture.componentInstance.cities[0];
@@ -2000,6 +1930,244 @@ describe('NgSelectComponent', () => {
20001930
});
20011931
});
20021932

1933+
describe('Keyboard events (tab)', () => {
1934+
function genericFixture() {
1935+
const fixture = createTestingModule(
1936+
NgSelectTestComponent,
1937+
`<ng-select [items]="cities"
1938+
bindLabel="name"
1939+
[loading]="citiesLoading"
1940+
[selectOnTab]="selectOnTab"
1941+
[multiple]="multiple"
1942+
[tabFocusOnClearButton]="tabFocusOnClearButton"
1943+
[(ngModel)]="selectedCity">
1944+
</ng-select>`,
1945+
);
1946+
const select = fixture.componentInstance.select;
1947+
return { fixture, select };
1948+
}
1949+
1950+
it('should close dropdown when there are no items', fakeAsync(() => {
1951+
const { fixture, select } = genericFixture();
1952+
select.filter('random stuff');
1953+
tick(200);
1954+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
1955+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
1956+
expect(select.isOpen).toBeFalsy();
1957+
}));
1958+
1959+
it('should close dropdown when [selectOnTab]="false"', fakeAsync(() => {
1960+
const { fixture, select } = genericFixture();
1961+
fixture.componentInstance.selectOnTab = false;
1962+
tickAndDetectChanges(fixture);
1963+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
1964+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
1965+
expect(select.selectedItems).toEqual([]);
1966+
expect(select.isOpen).toBeFalsy();
1967+
}));
1968+
1969+
it('should close dropdown and keep selected value', fakeAsync(() => {
1970+
const { fixture, select } = genericFixture();
1971+
fixture.componentInstance.selectedCity = fixture.componentInstance.cities[0];
1972+
tickAndDetectChanges(fixture);
1973+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
1974+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
1975+
tickAndDetectChanges(fixture);
1976+
const result = [
1977+
jasmine.objectContaining({
1978+
value: fixture.componentInstance.cities[0],
1979+
}),
1980+
];
1981+
expect(select.selectedItems).toEqual(result);
1982+
expect(select.isOpen).toBeFalsy();
1983+
}));
1984+
1985+
it('should mark first item on filter when tab', fakeAsync(() => {
1986+
const { fixture } = genericFixture();
1987+
tick(200);
1988+
fixture.componentInstance.select.filter('pab');
1989+
tick(200);
1990+
1991+
const result = jasmine.objectContaining({
1992+
value: fixture.componentInstance.cities[2],
1993+
});
1994+
expect(fixture.componentInstance.select.itemsList.markedItem).toEqual(result);
1995+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
1996+
expect(fixture.componentInstance.select.selectedItems).toEqual([result]);
1997+
}));
1998+
1999+
it('should focus on clear button when tab pressed while not opened and clear showing', fakeAsync(() => {
2000+
const { fixture, select } = genericFixture();
2001+
fixture.componentInstance.tabFocusOnClearButton = true;
2002+
selectOption(fixture, KeyCode.ArrowDown, 0);
2003+
tickAndDetectChanges(fixture);
2004+
expect(select.showClear()).toBeTruthy();
2005+
2006+
select.searchInput.nativeElement.focus();
2007+
const focusOnClear = spyOn(select, 'focusOnClear');
2008+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
2009+
expect(focusOnClear).toHaveBeenCalled();
2010+
}));
2011+
2012+
it('should not focus on clear button when tab pressed if global flag is false and [tabFocusOnClearButton]="false"', fakeAsync(() => {
2013+
const config = new NgSelectConfig();
2014+
config.tabFocusOnClear = false;
2015+
const fixture = createTestingModule(
2016+
NgSelectTestComponent,
2017+
`<ng-select [items]="cities"
2018+
bindLabel="name"
2019+
[loading]="citiesLoading"
2020+
[selectOnTab]="selectOnTab"
2021+
[multiple]="multiple"
2022+
[tabFocusOnClearButton]="tabFocusOnClearButton"
2023+
[(ngModel)]="selectedCity">
2024+
</ng-select>`,
2025+
config,
2026+
);
2027+
const select = fixture.componentInstance.select;
2028+
fixture.componentInstance.tabFocusOnClearButton = false;
2029+
selectOption(fixture, KeyCode.ArrowDown, 0);
2030+
tickAndDetectChanges(fixture);
2031+
expect(select.showClear()).toBeTruthy();
2032+
2033+
select.searchInput.nativeElement.focus();
2034+
const focusOnClear = spyOn(select, 'focusOnClear');
2035+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
2036+
expect(focusOnClear).not.toHaveBeenCalled();
2037+
}));
2038+
2039+
it('should not focus on clear button when tab pressed if global flag is true and [tabFocusOnClearButton]="false"', fakeAsync(() => {
2040+
const config = new NgSelectConfig();
2041+
config.tabFocusOnClear = true;
2042+
const fixture = createTestingModule(
2043+
NgSelectTestComponent,
2044+
`<ng-select [items]="cities"
2045+
bindLabel="name"
2046+
[loading]="citiesLoading"
2047+
[selectOnTab]="selectOnTab"
2048+
[multiple]="multiple"
2049+
[tabFocusOnClearButton]="tabFocusOnClearButton"
2050+
[(ngModel)]="selectedCity">
2051+
</ng-select>`,
2052+
config,
2053+
);
2054+
const select = fixture.componentInstance.select;
2055+
fixture.componentInstance.tabFocusOnClearButton = false;
2056+
selectOption(fixture, KeyCode.ArrowDown, 0);
2057+
tickAndDetectChanges(fixture);
2058+
expect(select.showClear()).toBeTruthy();
2059+
2060+
select.searchInput.nativeElement.focus();
2061+
const focusOnClear = spyOn(select, 'focusOnClear');
2062+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
2063+
expect(focusOnClear).not.toHaveBeenCalled();
2064+
}));
2065+
2066+
it('should focus on clear button when tab pressed if global flag is false and [tabFocusOnClearButton]="true"', fakeAsync(() => {
2067+
const config = new NgSelectConfig();
2068+
config.tabFocusOnClear = false;
2069+
const fixture = createTestingModule(
2070+
NgSelectTestComponent,
2071+
`<ng-select [items]="cities"
2072+
bindLabel="name"
2073+
[loading]="citiesLoading"
2074+
[selectOnTab]="selectOnTab"
2075+
[multiple]="multiple"
2076+
[tabFocusOnClearButton]="tabFocusOnClearButton"
2077+
[(ngModel)]="selectedCity">
2078+
</ng-select>`,
2079+
config,
2080+
);
2081+
const select = fixture.componentInstance.select;
2082+
fixture.componentInstance.tabFocusOnClearButton = true;
2083+
selectOption(fixture, KeyCode.ArrowDown, 0);
2084+
tickAndDetectChanges(fixture);
2085+
expect(select.showClear()).toBeTruthy();
2086+
2087+
select.searchInput.nativeElement.focus();
2088+
const focusOnClear = spyOn(select, 'focusOnClear');
2089+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
2090+
expect(focusOnClear).toHaveBeenCalled();
2091+
}));
2092+
2093+
it('should focus on clear button when tab pressed if global flag is true and [tabFocusOnClearButton]="true"', fakeAsync(() => {
2094+
const config = new NgSelectConfig();
2095+
config.tabFocusOnClear = true;
2096+
const fixture = createTestingModule(
2097+
NgSelectTestComponent,
2098+
`<ng-select [items]="cities"
2099+
bindLabel="name"
2100+
[loading]="citiesLoading"
2101+
[selectOnTab]="selectOnTab"
2102+
[multiple]="multiple"
2103+
[tabFocusOnClearButton]="tabFocusOnClearButton"
2104+
[(ngModel)]="selectedCity">
2105+
</ng-select>`,
2106+
config,
2107+
);
2108+
const select = fixture.componentInstance.select;
2109+
fixture.componentInstance.tabFocusOnClearButton = true;
2110+
selectOption(fixture, KeyCode.ArrowDown, 0);
2111+
tickAndDetectChanges(fixture);
2112+
expect(select.showClear()).toBeTruthy();
2113+
2114+
select.searchInput.nativeElement.focus();
2115+
const focusOnClear = spyOn(select, 'focusOnClear');
2116+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
2117+
expect(focusOnClear).toHaveBeenCalled();
2118+
}));
2119+
2120+
it('should not focus on clear button when tab pressed if global flag is false and [tabFocusOnClearButton] is not provided', fakeAsync(() => {
2121+
const config = new NgSelectConfig();
2122+
config.tabFocusOnClear = false;
2123+
const fixture = createTestingModule(
2124+
NgSelectTestComponent,
2125+
`<ng-select [items]="cities"
2126+
bindLabel="name"
2127+
[loading]="citiesLoading"
2128+
[selectOnTab]="selectOnTab"
2129+
[multiple]="multiple"
2130+
[(ngModel)]="selectedCity">
2131+
</ng-select>`,
2132+
config,
2133+
);
2134+
const select = fixture.componentInstance.select;
2135+
selectOption(fixture, KeyCode.ArrowDown, 0);
2136+
tickAndDetectChanges(fixture);
2137+
expect(select.showClear()).toBeTruthy();
2138+
2139+
select.searchInput.nativeElement.focus();
2140+
const focusOnClear = spyOn(select, 'focusOnClear');
2141+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
2142+
expect(focusOnClear).not.toHaveBeenCalled();
2143+
}));
2144+
2145+
it('should focus on clear button when tab pressed if global flag is true and [tabFocusOnClearButton] is not provided', fakeAsync(() => {
2146+
const config = new NgSelectConfig();
2147+
config.tabFocusOnClear = true;
2148+
const fixture = createTestingModule(
2149+
NgSelectTestComponent,
2150+
`<ng-select [items]="cities"
2151+
bindLabel="name"
2152+
[loading]="citiesLoading"
2153+
[selectOnTab]="selectOnTab"
2154+
[multiple]="multiple"
2155+
[(ngModel)]="selectedCity">
2156+
</ng-select>`,
2157+
config,
2158+
);
2159+
const select = fixture.componentInstance.select;
2160+
selectOption(fixture, KeyCode.ArrowDown, 0);
2161+
tickAndDetectChanges(fixture);
2162+
expect(select.showClear()).toBeTruthy();
2163+
2164+
select.searchInput.nativeElement.focus();
2165+
const focusOnClear = spyOn(select, 'focusOnClear');
2166+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);
2167+
expect(focusOnClear).toHaveBeenCalled();
2168+
}));
2169+
});
2170+
20032171
describe('Outside click', () => {
20042172
let fixture: ComponentFixture<NgSelectTestComponent>;
20052173
let select: NgSelectComponent;
@@ -4876,7 +5044,7 @@ class NgSelectTestComponent {
48765044
filter = new Subject<string>();
48775045
searchFn: (term: string, item: any) => boolean = null;
48785046
selectOnTab = true;
4879-
tabFocusOnClearButton = true;
5047+
tabFocusOnClearButton: boolean;
48805048
hideSelected = false;
48815049
citiesLoading = false;
48825050
selectedCityId: number;

0 commit comments

Comments
 (0)