import * as _ from "underscore"; import * as ko from "knockout"; enum ScrollPosition { Top, Bottom } export class AccessibleVerticalList { private items: any[] = []; private onSelect: (item: any) => void; public currentItemIndex: ko.Observable; public currentItem: ko.Computed; constructor(initialSetOfItems: any[]) { this.items = initialSetOfItems; this.currentItemIndex = this.items != null && this.items.length > 0 ? ko.observable(0) : ko.observable(-1); this.currentItem = ko.computed(() => this.items[this.currentItemIndex()]); } public setOnSelect(onSelect: (item: any) => void): void { this.onSelect = onSelect; } public onKeyDown = (source: any, event: KeyboardEvent): boolean => { const targetContainer: Element = event.target; if (this.items == null || this.items.length === 0) { // no items so this should be a noop return true; } if (event.keyCode === 32 || event.keyCode === 13) { // on space or enter keydown this.onSelect && this.onSelect(this.currentItem()); event.stopPropagation(); return false; } if (event.keyCode === 38) { // on UpArrow keydown event.preventDefault(); this.selectPreviousItem(); const targetElement = targetContainer .getElementsByClassName("accessibleListElement") .item(this.currentItemIndex()); this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Top); return false; } if (event.keyCode === 40) { // on DownArrow keydown event.preventDefault(); this.selectNextItem(); const targetElement = targetContainer .getElementsByClassName("accessibleListElement") .item(this.currentItemIndex()); this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Bottom); return false; } return true; }; public updateItemList(newItemList: any[]) { if (newItemList == null || newItemList.length === 0) { this.currentItemIndex(-1); this.items = []; return; } else if (this.currentItemIndex() < 0) { this.currentItemIndex(0); } this.items = newItemList; } public updateCurrentItem(item: any) { const updatedIndex: number = this.isItemListEmpty() ? -1 : _.indexOf(this.items, item); this.currentItemIndex(updatedIndex); } private isElementVisibleInContainer(element: Element, container: Element): boolean { const elementTop = element.getBoundingClientRect().top; const elementBottom = element.getBoundingClientRect().bottom; const containerTop = container.getBoundingClientRect().top; const containerBottom = container.getBoundingClientRect().bottom; return elementTop >= containerTop && elementBottom <= containerBottom; } private scrollElementIntoContainerViewIfNeeded( element: Element, container: Element, scrollPosition: ScrollPosition ): void { if (!this.isElementVisibleInContainer(element, container)) { if (scrollPosition === ScrollPosition.Top) { container.scrollTop = element.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop; } else { container.scrollTop = element.getBoundingClientRect().bottom - element.getBoundingClientRect().top + container.scrollTop; } } } private selectPreviousItem(): void { if (this.currentItemIndex() <= 0 || this.isItemListEmpty()) { return; } this.currentItemIndex(this.currentItemIndex() - 1); } private selectNextItem(): void { if (this.isItemListEmpty() || this.currentItemIndex() === this.items.length - 1) { return; } this.currentItemIndex(this.currentItemIndex() + 1); } private isItemListEmpty(): boolean { return this.items == null || this.items.length === 0; } }