Skip to content

Commit

Permalink
feat: focus first item on open/keyup
Browse files Browse the repository at this point in the history
  • Loading branch information
Tadeuš Varnas committed Sep 22, 2017
1 parent 4ad79aa commit 72f4dd0
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 29 deletions.
22 changes: 16 additions & 6 deletions src/lib/src/items-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ export class ItemsList {
}

filter(term: string, bindLabel: string) {
this._markedItemIndex = -1;
this.unmarkCurrentItem();
const filterFuncVal = this.getDefaultFilterFunc(term, bindLabel);
this.filteredItems = term ? this.items.filter(val => filterFuncVal(val)) : this.items;
this.markItem(0);
}

clearFilter() {
Expand All @@ -63,12 +64,13 @@ export class ItemsList {
}

markSelection() {
const lastSelected = this._selected[this._selected.length - 1];
this._markedItemIndex = lastSelected ? this.filteredItems.indexOf(lastSelected) : 0;
this.markedItem = this.filteredItems[this._markedItemIndex];
if (this.markedItem) {
this.markedItem.marked = true;
if (this.filteredItems.length === 0) {
return;
}

const lastSelected = this._selected[this._selected.length - 1];
const index = lastSelected ? this.filteredItems.indexOf(lastSelected) : 0;
this.markItem(index);
}

unmarkCurrentItem() {
Expand Down Expand Up @@ -106,4 +108,12 @@ export class ItemsList {
.indexOf(searchHelper.stripSpecialChars(term).toUpperCase()) > -1;
};
}

private markItem(index: number) {
this._markedItemIndex = index;
this.markedItem = this.filteredItems[this._markedItemIndex];
if (this.markedItem) {
this.markedItem.marked = true;
}
}
}
90 changes: 68 additions & 22 deletions src/lib/src/ng-select.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('NgSelectComponent', function () {

it('update parent selected model on value change', fakeAsync(() => {
// select second city
selectOption(fixture, KeyCode.ArrowDown, 2);
selectOption(fixture, KeyCode.ArrowDown, 1);

fixture.detectChanges();
tick();
Expand Down Expand Up @@ -74,7 +74,7 @@ describe('NgSelectComponent', function () {
</ng-select>`);

// from component to model
selectOption(fixture, KeyCode.ArrowDown, 1);
selectOption(fixture, KeyCode.ArrowDown, 0);

fixture.detectChanges();
tick();
Expand All @@ -100,7 +100,7 @@ describe('NgSelectComponent', function () {
</ng-select>`);

// from component to model
selectOption(fixture, KeyCode.ArrowDown, 1);
selectOption(fixture, KeyCode.ArrowDown, 0);

fixture.detectChanges();
tick();
Expand Down Expand Up @@ -232,9 +232,15 @@ describe('NgSelectComponent', function () {
expect(fixture.componentInstance.select.isOpen).toBe(true);
});

it('should mark first item on open', () => {
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Enter);
expect(fixture.componentInstance.select.value).toEqual(fixture.componentInstance.cities[0]);
});

it('select next value on arrow down', () => {
selectOption(fixture, KeyCode.ArrowDown, 1);
expect(fixture.componentInstance.select.value).toEqual(fixture.componentInstance.cities[0]);
expect(fixture.componentInstance.select.value).toEqual(fixture.componentInstance.cities[1]);
});

it('select first value on arrow down when current selected value is last', async(() => {
Expand Down Expand Up @@ -278,18 +284,15 @@ describe('NgSelectComponent', function () {

it('close opened dropdown on esc click', () => {
fixture.componentInstance.select.isOpen = true;

triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Esc);

expect(fixture.componentInstance.select.isOpen).toBe(false);
});

it('close opened dropdown on tab click', () => {
fixture.componentInstance.select.isOpen = true;

it('should close opened dropdown and select marked value on tab click', () => {
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Tab);

expect(fixture.componentInstance.select.isOpen).toBe(false);
expect(fixture.componentInstance.select.value).toEqual(fixture.componentInstance.cities[0]);
expect(fixture.componentInstance.select.isOpen).toBeFalsy()
});
});

Expand Down Expand Up @@ -392,16 +395,25 @@ describe('NgSelectComponent', function () {
}));

it('should toggle selected item', fakeAsync(() => {
selectOption(fixture, KeyCode.ArrowDown, 1);
selectOption(fixture, KeyCode.ArrowDown, 0);
selectOption(fixture, KeyCode.ArrowDown, 2);
detectChanges();
expect((<NgOption[]>fixture.componentInstance.select.value).length).toBe(2);

selectOption(fixture, KeyCode.ArrowDown, 1);
detectChanges();
expect((<NgOption[]>fixture.componentInstance.select.value).length).toBe(1);
expect(fixture.componentInstance.select.value[0].name).toBe('Pabrade');
}));

it('should not toggle item on enter when dropdown is closed', () => {
selectOption(fixture, KeyCode.ArrowDown, 0);
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Esc);
expect((<NgOption[]>fixture.componentInstance.select.value).length).toBe(1);
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Enter);
expect((<NgOption[]>fixture.componentInstance.select.value).length).toBe(1);
})

function detectChanges() {
fixture.detectChanges();
tick();
Expand Down Expand Up @@ -444,7 +456,7 @@ describe('NgSelectComponent', function () {
describe('Filter', () => {
let fixture: ComponentFixture<NgSelectFilterTestCmp>;

it('filter items with default filter', fakeAsync(() => {
it('should filter using default implementation', fakeAsync(() => {
fixture = createTestingModule(
NgSelectFilterTestCmp,
`<ng-select [items]="cities"
Expand All @@ -456,28 +468,62 @@ describe('NgSelectComponent', function () {
fixture.componentInstance.select.onFilter({ target: { value: 'vilnius' } });
tick(200);

expect(fixture.componentInstance.select.itemsList.filteredItems).toEqual([{ id: 1, name: 'Vilnius' }]);
expect(fixture.componentInstance.select.itemsList.filteredItems).toEqual([{ id: 1, name: 'Vilnius', marked: true }]);
}));

it('filter items with custom observable typeahead', async(() => {
it('should mark first item on filter', fakeAsync(() => {
fixture = createTestingModule(
NgSelectFilterTestCmp,
`<ng-select [items]="cities"
[typeahead]="customFilter"
labelKey="name"
[(ngModel)]="selectedCity">
</ng-select>`);

fixture.detectChanges();
fixture.componentInstance.select.onFilter({ target: { value: 'vilnius' } });
fixture.componentInstance.select.onFilter({ target: { value: 'pab' } });
tick(200);
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Enter);
expect(fixture.componentInstance.select.value).toEqual(fixture.componentInstance.cities[2])
}));

fixture.componentInstance.customeFilter.subscribe(term => {
expect(term).toBe('vilnius');
describe('with typeahead', () => {
beforeEach(() => {
fixture = createTestingModule(
NgSelectFilterTestCmp,
`<ng-select [items]="cities"
[typeahead]="customFilter"
labelKey="name"
[(ngModel)]="selectedCity">
</ng-select>`);
fixture.detectChanges();
});
}));

});
it('should push term to custom observable', async(() => {
fixture.componentInstance.customFilter.subscribe(term => {
expect(term).toBe('vilnius');
});
fixture.componentInstance.select.onFilter({ target: { value: 'vilnius' } });
}));

it('should mark first item when typeahead results are loaded', async(() => {
fixture.componentInstance.customFilter.subscribe();
fixture.componentInstance.select.onFilter({ target: { value: 'buk' } });
fixture.componentInstance.cities = [{ id: 4, name: 'Bukiskes' }];
fixture.detectChanges();
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Enter);
expect(fixture.componentInstance.select.value).toEqual(jasmine.objectContaining({ id: 4, name: 'Bukiskes' }))
}));

it('should start and stop loading indicator', async(() => {
fixture.componentInstance.customFilter.subscribe();
fixture.componentInstance.select.onFilter({ target: { value: 'buk' } });
expect(fixture.componentInstance.select.isLoading).toBeTruthy();
fixture.componentInstance.cities = [{ id: 4, name: 'Bukiskes' }];
fixture.detectChanges();
expect(fixture.componentInstance.select.isLoading).toBeFalsy();
}));
});
});
});

function selectOption(fixture, key: KeyCode, steps: number) {
Expand Down Expand Up @@ -656,5 +702,5 @@ class NgSelectFilterTestCmp {
{ id: 3, name: 'Pabrade' },
];

customeFilter = new Subject<string>();
customFilter = new Subject<string>();
}
18 changes: 17 additions & 1 deletion src/lib/src/ng-select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ export class NgSelectComponent implements OnInit, ControlValueAccessor {
set items(items: any[]) {
this._items = items || [];
this.itemsList = new ItemsList(this._items, this.multiple);
this.isLoading = false;

if (this.isTypeahead()) {
this.handleItemsChange();
}
}

get value(): NgOption | NgOption[] {
Expand Down Expand Up @@ -129,6 +132,8 @@ export class NgSelectComponent implements OnInit, ControlValueAccessor {
this.handleEnter($event);
break;
case KeyCode.Tab:
this.handleTab($event);
break;
case KeyCode.Esc:
this.close();
break;
Expand Down Expand Up @@ -318,6 +323,11 @@ export class NgSelectComponent implements OnInit, ControlValueAccessor {
}
}

private handleItemsChange() {
this.isLoading = false;
this.itemsList.markSelection();
}

private selectWriteValue(value: any) {
this.validateWriteValue(value);
let index = -1;
Expand Down Expand Up @@ -361,6 +371,12 @@ export class NgSelectComponent implements OnInit, ControlValueAccessor {
this.dropdownList.scrollInto(this.itemsList.markedItem);
}

private handleTab($event: KeyboardEvent) {
if (this.isOpen) {
this.toggle(this.itemsList.markedItem);
}
}

private handleEnter($event: KeyboardEvent) {
if (this.isOpen) {
this.toggle(this.itemsList.markedItem);
Expand Down

0 comments on commit 72f4dd0

Please sign in to comment.