File

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

Description

 * <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
 *     }
 * ];
 *

Implements

AbstractDropdownView AfterViewInit OnDestroy

Metadata

providers { provide: AbstractDropdownView, useExisting: DropdownList }
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>

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

constructor(elementRef: ElementRef, i18n: I18n)

Creates an instance of DropdownList.

Parameters :
Name Type Optional
elementRef ElementRef No
i18n I18n No

Inputs

ariaLabel

Default value : this.i18n.get().DROPDOWN_LIST.LABEL

items

The list items belonging to the DropdownList.

listTpl

Template to bind to items in the DropdownList (optional).

Type : string | TemplateRef<any>

Default value : null

type

Defines whether or not the DropdownList supports selecting multiple items as opposed to single item selection.

Type : "single" | "multi"

Default value : "single"

Outputs

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 DropdownList.

$event Type: EventEmitter<ScrollCustomEvent>
select

Event to emit selection of a list item within the DropdownList.

$event Type: EventEmitter<Object>

Methods

doClick
doClick(event, item)

Emits the selected item or items after a mouse click event has occurred.

Parameters :
Name Optional
event No
item No
Returns : void
doKeyDown
doKeyDown(event: KeyboardEvent, item: ListItem)

Manages the keyboard accessibility for navigation and selection within a DropdownList.

Parameters :
Name Type Optional
event KeyboardEvent No
item ListItem No
Returns : void
emitScroll
emitScroll(event)

Emits the scroll event of the options list

Parameters :
Name Optional
event No
Returns : void
filterBy
filterBy(query: string)

Filters the items being displayed in the DOM list.

Parameters :
Name Type Optional Default value
query string No ""
Returns : void
getCurrentElement
getCurrentElement()

Returns the HTMLElement for the item that is selected within the DropdownList.

Returns : HTMLElement
getCurrentItem
getCurrentItem()

Returns the ListItem that is selected within DropdownList.

Returns : ListItem
getListItems
getListItems()

Returns the items as an Array

Returns : Array<ListItem>
getNextElement
getNextElement()

Returns the HTMLElement for the item that is subsequent to the selected item.

Returns : HTMLElement
getNextItem
getNextItem()

Returns the ListItem that is subsequent to the selected item in the DropdownList.

Returns : ListItem
getPrevElement
getPrevElement()

Returns the HTMLElement for the item that precedes the selected item.

Returns : HTMLElement
getPrevItem
getPrevItem()

Returns the ListItem that precedes the selected item within DropdownList.

Returns : ListItem
getSelected
getSelected()

Returns a list containing the selected item(s) in the DropdownList.

Returns : ListItem[]
hasNextElement
hasNextElement()

Returns true if the selected item is not the last item in the DropdownList. TODO: standardize

Returns : boolean
hasPrevElement
hasPrevElement()

Returns true if the selected item is not the first in the list. TODO: standardize

Returns : boolean
initFocus
initFocus()

Initializes focus in the list, effectively a wrapper for getCurrentElement().focus()

Returns : void
ngAfterViewInit
ngAfterViewInit()

Retrieves array of list items and index of the selected item after view has rendered. Additionally, any Observables for the DropdownList are initialized.

Returns : void
ngOnDestroy
ngOnDestroy()

Removes any Observables on destruction of the component.

Returns : void
onItemBlur
onItemBlur(index)
Parameters :
Name Optional
index No
Returns : void
onItemFocus
onItemFocus(index)
Parameters :
Name Optional
index No
Returns : void
onItemsReady
onItemsReady(subcription: () => void)

Subscribe the function passed to an internal observable that will resolve once the items are ready

Parameters :
Name Type Optional
subcription function No
Returns : void
propagateSelected
propagateSelected(value: Array)

Transforms array input list of items to the correct state by updating the selected item(s).

Parameters :
Name Type Optional
value Array<ListItem> No
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 DropdownList from the DOM.

Parameters :
Name Optional
items No
Returns : void

Properties

Protected _items
_items: Array<ListItem>
Type : Array<ListItem>
Default value : []

Useful representation of the items, should be accessed via getListItems.

Protected _itemsReady
_itemsReady: Observable<boolean>
Type : Observable<boolean>

Used to wait for items in case they are passed through an observable.

Protected _itemsSubscription
_itemsSubscription: Subscription
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: Array<ListItem> | Observable<Array<ListItem>>
Type : Array<ListItem> | Observable<Array<ListItem>>

Used to retain the original items passed to the setter.

Public displayItems
displayItems: Array<ListItem>
Type : Array<ListItem>
Default value : []

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 elementRef
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 DropdownList.

list
list: ElementRef
Type : ElementRef
Decorators :
@ViewChild('list')

Maintains a reference to the view DOM element for the unordered list of items within the DropdownList.

Protected listElementList
listElementList: QueryList<ElementRef>
Type : QueryList<ElementRef>
Decorators :
@ViewChildren('listItem')

An array holding the HTML list elements in the view.

Public size
size: "sm" | "md" | "lg"
Type : "sm" | "md" | "lg"
Default value : "md"

Defines the rendering size of the DropdownList input component.

Accessors

items
getitems()
setitems(value)

The list items belonging to the DropdownList.

Parameters :
Name Optional
value No
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);
	}
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""