src/dropdown/list/dropdown-list.component.ts
* <ibm-dropdown-list [items]="listItems"></ibm-dropdown-list>
*
* listItems = [
* {
* content: "item one",
* selected: false
* },
* {
* content: "item two",
* selected: false,
* },
* {
* content: "item three",
* selected: false
* },
* {
* content: "item four",
* selected: false
* }
* ];
*
AbstractDropdownView
AfterViewInit
OnDestroy
providers |
{
provide: AbstractDropdownView, useExisting: DropdownList
}
|
selector | ibm-dropdown-list |
template |
|
Properties |
|
Methods |
|
Inputs |
Outputs |
Accessors |
constructor(elementRef: ElementRef, i18n: I18n)
|
|||||||||
Creates an instance of
Parameters :
|
ariaLabel
|
Default value : |
items
|
The list items belonging to the |
listTpl
|
Template to bind to items in the
Type :
Default value : |
type
|
Defines whether or not the
Type :
Default value : |
blurIntent
|
Event to suggest a blur on the view. Emits after the first/last item has been focused. ex. ArrowUp -> focus first item ArrowUp -> emit event When this event fires focus should be placed on some element outside of the list - blurring the list as a result $event Type: EventEmitter
|
scroll
|
Event to emit scroll event of a list within the $event Type: EventEmitter<ScrollCustomEvent>
|
select
|
Event to emit selection of a list item within the $event Type: EventEmitter<Object>
|
doClick | ||||||
doClick(event, item)
|
||||||
Emits the selected item or items after a mouse click event has occurred.
Parameters :
Returns :
void
|
doKeyDown | |||||||||
doKeyDown(event: KeyboardEvent, item: ListItem)
|
|||||||||
Manages the keyboard accessibility for navigation and selection within a
Parameters :
Returns :
void
|
emitScroll | ||||
emitScroll(event)
|
||||
Emits the scroll event of the options list
Parameters :
Returns :
void
|
filterBy | ||||||||
filterBy(query: string)
|
||||||||
Filters the items being displayed in the DOM list.
Parameters :
Returns :
void
|
getCurrentElement |
getCurrentElement()
|
Returns the
Returns :
HTMLElement
|
getCurrentItem |
getCurrentItem()
|
Returns the
Returns :
ListItem
|
getListItems |
getListItems()
|
Returns the items as an Array
Returns :
Array<ListItem>
|
getNextElement |
getNextElement()
|
Returns the
Returns :
HTMLElement
|
getNextItem |
getNextItem()
|
Returns the
Returns :
ListItem
|
getPrevElement |
getPrevElement()
|
Returns the
Returns :
HTMLElement
|
getPrevItem |
getPrevItem()
|
Returns the
Returns :
ListItem
|
getSelected |
getSelected()
|
Returns a list containing the selected item(s) in the
Returns :
ListItem[]
|
hasNextElement |
hasNextElement()
|
Returns
Returns :
boolean
|
hasPrevElement |
hasPrevElement()
|
Returns
Returns :
boolean
|
initFocus |
initFocus()
|
Initializes focus in the list, effectively a wrapper for
Returns :
void
|
ngAfterViewInit |
ngAfterViewInit()
|
Retrieves array of list items and index of the selected item after view has rendered.
Additionally, any Observables for the
Returns :
void
|
ngOnDestroy |
ngOnDestroy()
|
Removes any Observables on destruction of the component.
Returns :
void
|
onItemBlur | ||||
onItemBlur(index)
|
||||
Parameters :
Returns :
void
|
onItemFocus | ||||
onItemFocus(index)
|
||||
Parameters :
Returns :
void
|
onItemsReady | ||||||
onItemsReady(subcription: () => void)
|
||||||
Subscribe the function passed to an internal observable that will resolve once the items are ready
Parameters :
Returns :
void
|
propagateSelected | ||||||
propagateSelected(value: Array
|
||||||
Transforms array input list of items to the correct state by updating the selected item(s).
Parameters :
Returns :
void
|
setupFocusObservable |
setupFocusObservable()
|
Initializes (or re-initializes) the Observable that handles switching focus to an element based on key input matching the first letter of the item in the list.
Returns :
void
|
updateList | ||||
updateList(items)
|
||||
Updates the displayed list of items and then retrieves the most current properties for the
Parameters :
Returns :
void
|
Protected _items |
_items:
|
Type : Array<ListItem>
|
Default value : []
|
Useful representation of the items, should be accessed via |
Protected _itemsReady |
_itemsReady:
|
Type : Observable<boolean>
|
Used to wait for items in case they are passed through an observable. |
Protected _itemsSubscription |
_itemsSubscription:
|
Type : Subscription
|
Tracks the current (if any) subscription to the items observable so we can clean up when the input is updated. |
Protected _originalItems |
_originalItems:
|
Type : Array<ListItem> | Observable<Array<ListItem>>
|
Used to retain the original items passed to the setter. |
Public displayItems |
displayItems:
|
Type : Array<ListItem>
|
Default value : []
|
Holds the list of items that will be displayed in the |
Public elementRef |
elementRef:
|
Type : ElementRef
|
Protected focusJump |
focusJump:
|
Observable bound to keydown events to control filtering. |
Protected index |
index:
|
Default value : -1
|
Maintains the index for the selected item within the |
list |
list:
|
Type : ElementRef
|
Decorators :
@ViewChild('list')
|
Maintains a reference to the view DOM element for the unordered list of items within the |
Protected listElementList |
listElementList:
|
Type : QueryList<ElementRef>
|
Decorators :
@ViewChildren('listItem')
|
An array holding the HTML list elements in the view. |
Public size |
size:
|
Type : "sm" | "md" | "lg"
|
Default value : "md"
|
Defines the rendering size of the |
items | ||||
getitems()
|
||||
setitems(value)
|
||||
The list items belonging to the
Parameters :
Returns :
void
|
import {
Component,
Input,
Output,
OnDestroy,
EventEmitter,
TemplateRef,
AfterViewInit,
ViewChild,
ElementRef,
ViewChildren,
QueryList
} from "@angular/core";
import { Observable, isObservable, Subscription, of } from "rxjs";
import { first } from "rxjs/operators";
import { I18n } from "../../i18n/i18n.module";
import { AbstractDropdownView } from "./../abstract-dropdown-view.class";
import { ListItem } from "./../list-item.interface";
import { watchFocusJump } from "./../dropdowntools";
import { ScrollCustomEvent } from "./scroll-custom-event.interface";
/**
* ```html
* <ibm-dropdown-list [items]="listItems"></ibm-dropdown-list>
* ```
* ```typescript
* listItems = [
* {
* content: "item one",
* selected: false
* },
* {
* content: "item two",
* selected: false,
* },
* {
* content: "item three",
* selected: false
* },
* {
* content: "item four",
* selected: false
* }
* ];
* ```
*/
@Component({
selector: "ibm-dropdown-list",
template: `
<ul
#list
role="listbox"
class="bx--list-box__menu bx--multi-select"
(scroll)="emitScroll($event)"
[attr.aria-label]="ariaLabel">
<li
role="option"
*ngFor="let item of displayItems; let i = index"
(click)="doClick($event, item)"
(keydown)="doKeyDown($event, item)"
(focus)="onItemFocus(i)"
(blur)="onItemBlur(i)"
class="bx--list-box__menu-item"
[ngClass]="{
'bx--list-box__menu-item--active': item.selected,
disabled: item.disabled
}"
[title]="item.content">
<div
#listItem
tabindex="-1"
class="bx--list-box__menu-item__option">
<div
*ngIf="!listTpl && type === 'multi'"
class="bx--form-item bx--checkbox-wrapper">
<label
[attr.data-contained-checkbox-state]="item.selected"
class="bx--checkbox-label">
<input
class="bx--checkbox"
type="checkbox"
[checked]="item.selected"
[disabled]="item.disabled"
tabindex="-1">
<span class="bx--checkbox-appearance"></span>
<span class="bx--checkbox-label-text">{{item.content}}</span>
</label>
</div>
<ng-container *ngIf="!listTpl && type === 'single'">{{item.content}}</ng-container>
<ng-template
*ngIf="listTpl"
[ngTemplateOutletContext]="{item: item}"
[ngTemplateOutlet]="listTpl">
</ng-template>
</div>
</li>
</ul>`,
providers: [
{
provide: AbstractDropdownView,
useExisting: DropdownList
}
]
})
export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDestroy {
@Input() ariaLabel = this.i18n.get().DROPDOWN_LIST.LABEL;
/**
* The list items belonging to the `DropdownList`.
*/
@Input() set items (value: Array<ListItem> | Observable<Array<ListItem>>) {
if (isObservable(value)) {
if (this._itemsSubscription) {
this._itemsSubscription.unsubscribe();
}
this._itemsReady = new Observable<boolean>((observer) => {
this._itemsSubscription = value.subscribe(v => {
this.updateList(v);
observer.next(true);
observer.complete();
});
});
this.onItemsReady(null);
} else {
this.updateList(value);
}
this._originalItems = value;
}
get items(): Array<ListItem> | Observable<Array<ListItem>> {
return this._originalItems;
}
/**
* Template to bind to items in the `DropdownList` (optional).
*/
@Input() listTpl: string | TemplateRef<any> = null;
/**
* Event to emit selection of a list item within the `DropdownList`.
*/
@Output() select: EventEmitter<Object> = new EventEmitter<Object>();
/**
* Event to emit scroll event of a list within the `DropdownList`.
*/
@Output() scroll: EventEmitter<ScrollCustomEvent> = new EventEmitter<ScrollCustomEvent>();
/**
* Event to suggest a blur on the view.
* Emits _after_ the first/last item has been focused.
* ex.
* ArrowUp -> focus first item
* ArrowUp -> emit event
*
* When this event fires focus should be placed on some element outside of the list - blurring the list as a result
*/
@Output() blurIntent = new EventEmitter<"top" | "bottom">();
/**
* Maintains a reference to the view DOM element for the unordered list of items within the `DropdownList`.
*/
@ViewChild("list") list: ElementRef;
/**
* Defines whether or not the `DropdownList` supports selecting multiple items as opposed to single
* item selection.
*/
@Input() type: "single" | "multi" = "single";
/**
* Defines the rendering size of the `DropdownList` input component.
*/
public size: "sm" | "md" | "lg" = "md";
/**
* Holds the list of items that will be displayed in the `DropdownList`.
* It differs from the the complete set of items when filtering is used (but
* it is always a subset of the total items in `DropdownList`).
*/
public displayItems: Array<ListItem> = [];
/**
* Maintains the index for the selected item within the `DropdownList`.
*/
protected index = -1;
/**
* An array holding the HTML list elements in the view.
*/
@ViewChildren("listItem") protected listElementList: QueryList<ElementRef>;
/**
* Observable bound to keydown events to control filtering.
*/
protected focusJump;
/**
* Tracks the current (if any) subscription to the items observable so we can clean up when the input is updated.
*/
protected _itemsSubscription: Subscription;
/**
* Used to retain the original items passed to the setter.
*/
protected _originalItems: Array<ListItem> | Observable<Array<ListItem>>;
/**
* Useful representation of the items, should be accessed via `getListItems`.
*/
protected _items: Array<ListItem> = [];
/**
* Used to wait for items in case they are passed through an observable.
*/
protected _itemsReady: Observable<boolean>;
/**
* Creates an instance of `DropdownList`.
*/
constructor(public elementRef: ElementRef, protected i18n: I18n) {}
/**
* Retrieves array of list items and index of the selected item after view has rendered.
* Additionally, any Observables for the `DropdownList` are initialized.
*/
ngAfterViewInit() {
this.index = this.getListItems().findIndex(item => item.selected);
this.setupFocusObservable();
}
/**
* Removes any Observables on destruction of the component.
*/
ngOnDestroy() {
if (this.focusJump) {
this.focusJump.unsubscribe();
}
if (this._itemsSubscription) {
this._itemsSubscription.unsubscribe();
}
}
/**
* Updates the displayed list of items and then retrieves the most current properties for the `DropdownList` from the DOM.
*/
updateList(items) {
this._items = items.map(item => Object.assign({}, item));
this.displayItems = this._items;
this.index = this._items.findIndex(item => item.selected);
this.setupFocusObservable();
setTimeout(() => {
if (this.getSelected() !== []) { return; }
if (this.type === "single") {
this.select.emit({ item: this._items.find(item => item.selected), isUpdate: true });
} else {
// abuse javascripts object mutability until we can break the API and switch to
// { items: [], isUpdate: true }
const selected = this.getSelected() || [];
selected["isUpdate"] = true;
this.select.emit(selected);
}
});
}
/**
* Filters the items being displayed in the DOM list.
*/
filterBy(query = "") {
if (query) {
this.displayItems = this.getListItems().filter(item => item.content.toLowerCase().includes(query.toLowerCase()));
} else {
this.displayItems = this.getListItems();
}
// reset the index since the list has changed visually
this.index = 0;
}
/**
* Initializes (or re-initializes) the Observable that handles switching focus to an element based on
* key input matching the first letter of the item in the list.
*/
setupFocusObservable() {
if (this.focusJump) {
this.focusJump.unsubscribe();
}
let elList = Array.from(this.list.nativeElement.querySelectorAll("li"));
this.focusJump = watchFocusJump(this.list.nativeElement, elList)
.subscribe(el => {
el.focus();
});
}
/**
* Returns the `ListItem` that is subsequent to the selected item in the `DropdownList`.
*/
getNextItem(): ListItem {
if (this.index < this.displayItems.length - 1) {
this.index++;
}
return this.displayItems[this.index];
}
/**
* Returns `true` if the selected item is not the last item in the `DropdownList`.
* TODO: standardize
*/
hasNextElement(): boolean {
if (this.index < this.displayItems.length - 1) {
return true;
}
return false;
}
/**
* Returns the `HTMLElement` for the item that is subsequent to the selected item.
*/
getNextElement(): HTMLElement {
if (this.index < this.displayItems.length - 1) {
this.index++;
}
let elem = this.listElementList.toArray()[this.index].nativeElement;
let item = this.displayItems[this.index];
if (item.disabled) {
return this.getNextElement();
}
return elem;
}
/**
* Returns the `ListItem` that precedes the selected item within `DropdownList`.
*/
getPrevItem(): ListItem {
if (this.index > 0) {
this.index--;
}
return this.displayItems[this.index];
}
/**
* Returns `true` if the selected item is not the first in the list.
* TODO: standardize
*/
hasPrevElement(): boolean {
if (this.index > 0) {
return true;
}
return false;
}
/**
* Returns the `HTMLElement` for the item that precedes the selected item.
*/
getPrevElement(): HTMLElement {
if (this.index > 0) {
this.index--;
}
let elem = this.listElementList.toArray()[this.index].nativeElement;
let item = this.displayItems[this.index];
if (item.disabled) {
return this.getPrevElement();
}
return elem;
}
/**
* Returns the `ListItem` that is selected within `DropdownList`.
*/
getCurrentItem(): ListItem {
if (this.index < 0) {
return this.displayItems[0];
}
return this.displayItems[this.index];
}
/**
* Returns the `HTMLElement` for the item that is selected within the `DropdownList`.
*/
getCurrentElement(): HTMLElement {
if (this.index < 0) {
return this.listElementList.first.nativeElement;
}
return this.listElementList.toArray()[this.index].nativeElement;
}
/**
* Returns the items as an Array
*/
getListItems(): Array<ListItem> {
return this._items;
}
/**
* Returns a list containing the selected item(s) in the `DropdownList`.
*/
getSelected(): ListItem[] {
let selected = this.getListItems().filter(item => item.selected);
if (selected.length === 0) {
return [];
}
return selected;
}
/**
* Transforms array input list of items to the correct state by updating the selected item(s).
*/
propagateSelected(value: Array<ListItem>): void {
// if we get a non-array, log out an error (since it is one)
if (!Array.isArray(value)) {
console.error(`${this.constructor.name}.propagateSelected expects an Array<ListItem>, got ${JSON.stringify(value)}`);
}
this.onItemsReady(() => {
// loop through the list items and update the `selected` state for matching items in `value`
for (let oldItem of this.getListItems()) {
// copy the item
let tempOldItem: string | ListItem = Object.assign({}, oldItem);
// deleted selected because it's what we _want_ to change
delete tempOldItem.selected;
// stringify for compare
tempOldItem = JSON.stringify(tempOldItem);
for (let newItem of value) {
// copy the item
let tempNewItem: string | ListItem = Object.assign({}, newItem);
// deleted selected because it's what we _want_ to change
delete tempNewItem.selected;
// stringify for compare
tempNewItem = JSON.stringify(tempNewItem);
// do the compare
if (tempOldItem.includes(tempNewItem)) {
oldItem.selected = newItem.selected;
// if we've found a matching item, we can stop looping
break;
} else {
oldItem.selected = false;
}
}
}
});
}
/**
* Initializes focus in the list, effectively a wrapper for `getCurrentElement().focus()`
*/
initFocus() {
// ensure we start at this first item if nothing is already selected
if (this.index < 0) {
this.index = 0;
}
this.getCurrentElement().focus();
}
/**
* Manages the keyboard accessibility for navigation and selection within a `DropdownList`.
*/
doKeyDown(event: KeyboardEvent, item: ListItem) {
// "Spacebar", "Down", and "Up" are IE specific values
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
if (this.listElementList.some(option => option.nativeElement === event.target)) {
event.preventDefault();
}
if (event.key === "Enter") {
this.doClick(event, item);
}
} else if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Down" || event.key === "Up") {
event.preventDefault();
if (event.key === "ArrowDown" || event.key === "Down") {
if (this.hasNextElement()) {
this.getNextElement().focus();
} else {
this.blurIntent.emit("bottom");
}
} else if (event.key === "ArrowUp" || event.key === "Up") {
if (this.hasPrevElement()) {
this.getPrevElement().focus();
} else {
this.blurIntent.emit("top");
}
}
}
}
/**
* Emits the selected item or items after a mouse click event has occurred.
*/
doClick(event, item) {
event.preventDefault();
if (!item.disabled) {
if (this.type === "single") {
item.selected = true;
// reset the selection
for (let otherItem of this.getListItems()) {
if (item !== otherItem) { otherItem.selected = false; }
}
this.select.emit({item});
} else {
item.selected = !item.selected;
// emit an array of selected items
this.select.emit(this.getSelected());
}
this.index = this.getListItems().indexOf(item);
}
}
onItemFocus(index) {
const element = this.listElementList.toArray()[index].nativeElement;
element.classList.add("bx--list-box__menu-item--highlighted");
element.tabIndex = 0;
}
onItemBlur(index) {
const element = this.listElementList.toArray()[index].nativeElement;
element.classList.remove("bx--list-box__menu-item--highlighted");
element.tabIndex = -1;
}
/**
* Emits the scroll event of the options list
*/
emitScroll(event) {
const atTop: boolean = event.srcElement.scrollTop === 0;
const atBottom: boolean = event.srcElement.scrollHeight - event.srcElement.scrollTop === event.srcElement.clientHeight;
const customScrollEvent = { atTop, atBottom, event };
this.scroll.emit(customScrollEvent);
}
/**
* Subscribe the function passed to an internal observable that will resolve once the items are ready
*/
onItemsReady(subcription: () => void): void {
// this subscription will auto unsubscribe because of the `first()` pipe
(this._itemsReady || of(true)).pipe(first()).subscribe(subcription);
}
}