Skip to content

Commit ea5db26

Browse files
authored
Merge pull request #416 from cal-smith/dropdown
Dropdown enhancements
2 parents bb165b1 + 0ab7cc5 commit ea5db26

File tree

3 files changed

+66
-33
lines changed

3 files changed

+66
-33
lines changed

src/combobox/combobox.component.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { AbstractDropdownView } from "./../dropdown/abstract-dropdown-view.class";
1717
import { ListItem } from "./../dropdown/list-item.interface";
1818
import { NG_VALUE_ACCESSOR } from "@angular/forms";
19+
import { filter } from "rxjs/operators";
1920

2021
/**
2122
* ComboBoxes are similar to dropdowns, except a combobox provides an input field for users to search items and (optionally) add their own.
@@ -262,6 +263,10 @@ export class ComboBox implements OnChanges, OnInit, AfterViewInit, AfterContentI
262263
setTimeout(() => {
263264
this.updateSelected();
264265
});
266+
267+
this.view.blurIntent.pipe(filter(v => v === "top")).subscribe(() => {
268+
this.elementRef.nativeElement.querySelector(".bx--text-input").focus();
269+
});
265270
}
266271
}
267272

@@ -291,10 +296,6 @@ export class ComboBox implements OnChanges, OnInit, AfterViewInit, AfterContentI
291296
ev.stopPropagation();
292297
this.openDropdown();
293298
setTimeout(() => this.view.getCurrentElement().focus(), 0);
294-
} else if ((ev.key === "ArrowUp" || ev.key === "Up") // `"Up"` is IE specific value
295-
&& this.dropdownMenu.nativeElement.contains(ev.target)
296-
&& !this.view.hasPrevElement()) {
297-
this.elementRef.nativeElement.querySelector(".bx--text-input").focus();
298299
}
299300
}
300301

src/dropdown/abstract-dropdown-view.class.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ export class AbstractDropdownView {
1919
* Emits selection events to other class.
2020
*/
2121
@Output() select: EventEmitter<Object>;
22+
/**
23+
* Event to suggest a blur on the view.
24+
* Emits _after_ the first/last item has been focused.
25+
* ex.
26+
* ArrowUp -> focus first item
27+
* ArrowUp -> emit event
28+
*
29+
* It's recommended that the implementing view include a specific type union of possible blurs
30+
* ex. `@Output() blurIntent = new EventEmitter<"top" | "bottom">();`
31+
*/
32+
@Output() blurIntent: EventEmitter<any>;
2233
/**
2334
* Specifies whether or not the `DropdownList` supports selecting multiple items as opposed to single
2435
* item selection.

src/dropdown/list/dropdown-list.component.ts

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
TemplateRef,
88
AfterViewInit,
99
ViewChild,
10-
ElementRef
10+
ElementRef,
11+
ViewChildren,
12+
QueryList
1113
} from "@angular/core";
1214

1315
import { I18n } from "../../i18n/i18n.module";
@@ -51,7 +53,9 @@ import { Observable, isObservable, Subscription } from "rxjs";
5153
role="listbox"
5254
class="bx--list-box__menu"
5355
[attr.aria-label]="ariaLabel">
54-
<li tabindex="-1"
56+
<li
57+
#listItem
58+
tabindex="-1"
5559
role="option"
5660
*ngFor="let item of displayItems; let i = index"
5761
(click)="doClick($event, item)"
@@ -118,6 +122,16 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
118122
* Event to emit selection of a list item within the `DropdownList`.
119123
*/
120124
@Output() select: EventEmitter<Object> = new EventEmitter<Object>();
125+
/**
126+
* Event to suggest a blur on the view.
127+
* Emits _after_ the first/last item has been focused.
128+
* ex.
129+
* ArrowUp -> focus first item
130+
* ArrowUp -> emit event
131+
*
132+
* When this event fires focus should be placed on some element outside of the list - blurring the list as a result
133+
*/
134+
@Output() blurIntent = new EventEmitter<"top" | "bottom">();
121135
/**
122136
* Maintains a reference to the view DOM element for the unordered list of items within the `DropdownList`.
123137
*/
@@ -148,7 +162,7 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
148162
/**
149163
* An array holding the HTML list elements in the view.
150164
*/
151-
protected listElementList: HTMLElement[];
165+
@ViewChildren("listItem") protected listElementList: QueryList<ElementRef>;
152166
/**
153167
* Observable bound to keydown events to control filtering.
154168
*/
@@ -176,7 +190,6 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
176190
* Additionally, any Observables for the `DropdownList` are initialized.
177191
*/
178192
ngAfterViewInit() {
179-
this.listElementList = Array.from(this.list.nativeElement.querySelectorAll("li")) as HTMLElement[];
180193
this.index = this.getListItems().findIndex(item => item.selected);
181194
this.setupFocusObservable();
182195
}
@@ -196,9 +209,6 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
196209
updateList(items) {
197210
this._items = items.map(item => Object.assign({}, item));
198211
this.displayItems = this._items;
199-
setTimeout(() => {
200-
this.listElementList = Array.from(this.list.nativeElement.querySelectorAll("li")) as HTMLElement[];
201-
}, 0);
202212
this.index = this._items.findIndex(item => item.selected);
203213
this.setupFocusObservable();
204214
setTimeout(() => {
@@ -219,6 +229,8 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
219229
} else {
220230
this.displayItems = this.getListItems();
221231
}
232+
// reset the index since the list has changed visually
233+
this.index = 0;
222234
}
223235

224236
/**
@@ -240,18 +252,18 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
240252
* Returns the `ListItem` that is subsequent to the selected item in the `DropdownList`.
241253
*/
242254
getNextItem(): ListItem {
243-
if (this.index < this.getListItems().length - 1) {
255+
if (this.index < this.displayItems.length - 1) {
244256
this.index++;
245257
}
246-
return this.getListItems()[this.index];
258+
return this.displayItems[this.index];
247259
}
248260

249261
/**
250262
* Returns `true` if the selected item is not the last item in the `DropdownList`.
251263
* TODO: standardize
252264
*/
253265
hasNextElement(): boolean {
254-
if (this.index < this.getListItems().length - 1) {
266+
if (this.index < this.displayItems.length - 1) {
255267
return true;
256268
}
257269
return false;
@@ -261,11 +273,11 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
261273
* Returns the `HTMLElement` for the item that is subsequent to the selected item.
262274
*/
263275
getNextElement(): HTMLElement {
264-
if (this.index < this.getListItems().length - 1) {
276+
if (this.index < this.displayItems.length - 1) {
265277
this.index++;
266278
}
267-
let elem = this.listElementList[this.index];
268-
let item = this.getListItems()[this.index];
279+
let elem = this.listElementList.toArray()[this.index].nativeElement;
280+
let item = this.displayItems[this.index];
269281
if (item.disabled) {
270282
return this.getNextElement();
271283
}
@@ -279,7 +291,7 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
279291
if (this.index > 0) {
280292
this.index--;
281293
}
282-
return this.getListItems()[this.index];
294+
return this.displayItems[this.index];
283295
}
284296

285297
/**
@@ -300,8 +312,8 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
300312
if (this.index > 0) {
301313
this.index--;
302314
}
303-
let elem = this.listElementList[this.index];
304-
let item = this.getListItems()[this.index];
315+
let elem = this.listElementList.toArray()[this.index].nativeElement;
316+
let item = this.displayItems[this.index];
305317
if (item.disabled) {
306318
return this.getPrevElement();
307319
}
@@ -313,19 +325,19 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
313325
*/
314326
getCurrentItem(): ListItem {
315327
if (this.index < 0) {
316-
return this.getListItems()[0];
328+
return this.displayItems[0];
317329
}
318-
return this.getListItems()[this.index];
330+
return this.displayItems[this.index];
319331
}
320332

321333
/**
322334
* Returns the `HTMLElement` for the item that is selected within the `DropdownList`.
323335
*/
324336
getCurrentElement(): HTMLElement {
325337
if (this.index < 0) {
326-
return this.listElementList[0];
338+
return this.listElementList.first.nativeElement;
327339
}
328-
return this.listElementList[this.index];
340+
return this.listElementList.toArray()[this.index].nativeElement;
329341
}
330342

331343
/**
@@ -376,6 +388,10 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
376388
* Initializes focus in the list, effectively a wrapper for `getCurrentElement().focus()`
377389
*/
378390
initFocus() {
391+
// ensure we start at this first item if nothing is already selected
392+
if (this.index < 0) {
393+
this.index = 0;
394+
}
379395
this.getCurrentElement().focus();
380396
}
381397

@@ -391,14 +407,17 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
391407
}
392408
} else if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Down" || event.key === "Up") {
393409
event.preventDefault();
394-
// this.checkScrollArrows();
395-
if ((event.key === "ArrowDown" || event.key === "Down") && this.hasNextElement()) {
396-
this.getNextElement().focus();
410+
if (event.key === "ArrowDown" || event.key === "Down") {
411+
if (this.hasNextElement()) {
412+
this.getNextElement().focus();
413+
} else {
414+
this.blurIntent.emit("bottom");
415+
}
397416
} else if (event.key === "ArrowUp" || event.key === "Up") {
398417
if (this.hasPrevElement()) {
399418
this.getPrevElement().focus();
400-
} else if (this.getSelected()) {
401-
this.clearSelected.nativeElement.focus();
419+
} else {
420+
this.blurIntent.emit("top");
402421
}
403422
}
404423
}
@@ -427,12 +446,14 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
427446
}
428447

429448
onItemFocus(index) {
430-
this.listElementList[index].classList.add("bx--list-box__menu-item--highlighted");
431-
this.listElementList[index].tabIndex = 0;
449+
const element = this.listElementList.toArray()[index].nativeElement;
450+
element.classList.add("bx--list-box__menu-item--highlighted");
451+
element.tabIndex = 0;
432452
}
433453

434454
onItemBlur(index) {
435-
this.listElementList[index].classList.remove("bx--list-box__menu-item--highlighted");
436-
this.listElementList[index].tabIndex = -1;
455+
const element = this.listElementList.toArray()[index].nativeElement;
456+
element.classList.remove("bx--list-box__menu-item--highlighted");
457+
element.tabIndex = -1;
437458
}
438459
}

0 commit comments

Comments
 (0)