Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scroll for typeahead #1195

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b9c5adc
initial
arins Oct 25, 2016
1c70e3d
Fix scrollable autocomplete
arins Oct 25, 2016
d1829fa
change lineending to unix style and passed lint test
arins Oct 26, 2016
5cd1d03
Change name of package to symbrio-ng2-bootstrap
Oct 26, 2016
4bddb0b
Merge branch 'development' of https://github.com/arins/ng2-bootstrap …
Oct 26, 2016
c1231e7
build error
arins Oct 26, 2016
275c9ba
Merge branch 'development' of https://github.com/arins/ng2-bootstrap …
arins Oct 26, 2016
db0c9f5
fixed scroll in most common browsers
arins Oct 26, 2016
d64d774
fixed so that scroll disapears on elements match less than scrollsize
arins Oct 26, 2016
9586760
fixing unit test and checking in new version
arins Oct 27, 2016
0c4c5c4
fixed unittests and lint errors
arins Oct 27, 2016
52c0b37
change version back and fixed binaries
arins Oct 27, 2016
2e990ff
fixed .gitattributes
arins Oct 27, 2016
e88608f
name chagned back!
arins Oct 27, 2016
aebe206
Merge branch 'development' of https://github.com/valor-software/ng2-b…
arins Oct 31, 2016
004cdcd
added more covare to unittests
arins Oct 31, 2016
8bcb34c
ignore unittest for height
arins Oct 31, 2016
fcc1254
added unittests for position spec to increase code coverage
arins Oct 31, 2016
070f395
fixed unittesting
arins Oct 31, 2016
e83e396
fxied lint error
arins Oct 31, 2016
111d6c7
more unittests
arins Oct 31, 2016
c74e26a
more branch testing
arins Nov 1, 2016
8647c81
fixed empty block tslint error
arins Nov 1, 2016
c3990a5
tried to fix more unittests
arins Nov 1, 2016
ca0b8cc
more unittests
arins Nov 1, 2016
835d648
added even more unittest
arins Nov 1, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
core.autocrlf=false
38 changes: 38 additions & 0 deletions components/position.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { positionService } from './position';

describe('positionService', () => {
it('should give correct hight', () => {
const div = document.createElement('div');
div.style.height = '10px';
div.style.padding = '0px 0px';
div.style.border = '0px 0px';
document.body.appendChild(div);
expect(positionService.getStyle(div, 'height')).toBe('10px');
div.remove();
});

it('should be focused', () => {
const input = document.createElement('input');
input.setAttribute('type', 'text');

document.body.appendChild(input);
input.focus();
expect(positionService.isFocused(input)).toBe(true);
input.remove();
});

it('should give offset', () => {
const input = document.createElement('input');
input.setAttribute('type', 'text');
input.style.border = '0px';
input.style.padding = '0px 0px';
input.style.height = '20px';
document.body.appendChild(input);
let offsetShouldBe = parseInt(positionService.getStyle(document.body, 'margin-top').replace('px', ''), 10) +
parseInt(positionService.getStyle(document.body, 'padding-top'), 10) +
parseInt(positionService.getStyle(document.body, 'border-top-width'), 10);
expect(positionService.offset(input).top).toBe(offsetShouldBe);
input.remove();
});

});
24 changes: 17 additions & 7 deletions components/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,16 @@ export class PositionService {
return targetElPos;
}

private get window():Window {
return window;
}

private get document():Document {
return window.document;
/**
* returns true if element is the currently focused element
* element
* @param nativeEl
*/
public isFocused(nativeEl: HTMLElement): boolean {
return this.document.activeElement === nativeEl;
}

private getStyle(nativeEl:HTMLElement, cssProp:string):string {
public getStyle(nativeEl:HTMLElement, cssProp:string):string {
// IE
if ((nativeEl as any).currentStyle) {
return (nativeEl as any).currentStyle[cssProp];
Expand All @@ -126,6 +127,14 @@ export class PositionService {
return (nativeEl.style as KeyAttribute)[cssProp];
}

private get window():Window {

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

return window;
}

private get document():Document {
return window.document;
}

/**
* Checks if a given element is statically positioned
* @param nativeEl - raw DOM element
Expand All @@ -147,6 +156,7 @@ export class PositionService {
}
return offsetParent || this.document;
};

}

export const positionService:PositionService = new PositionService();
5 changes: 5 additions & 0 deletions components/typeahead/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export class TypeaheadDirective implements OnInit {
@Input() public typeaheadWordDelimiters:string = ' ';
@Input() public typeaheadPhraseDelimiters:string = '\'"';
@Input() public typeaheadItemTemplate:TemplateRef<any>;
@Input() public typeaheadScrollable:boolean = false;
@Input() public typeaheadOptionsInScrollableView:number = 5;

// not yet implemented
@Input() private typeaheadAppendToBody:boolean;
Expand Down Expand Up @@ -55,13 +57,16 @@ export class TypeaheadDirective implements OnInit {
- `typeaheadWordDelimiters` (`?string=" "`) - should be used only in case `typeaheadSingleWords` attribute is `true`. Sets the word delimiter to break words. Defaults to space.
- `typeaheadPhraseDelimiters` (`?string="'\""`) - should be used only in case `typeaheadSingleWords` attribute is `true`. Sets the word delimiter to match exact phrase. Defaults to simple and double quotes.
- `typeaheadItemTemplate` (`?TemplateRef`) - used to specify a custom item template. Template variables exposed are called `item` and `index`;
- `typeaheadScrollable` (`?boolean`) - used to set the dropdown container to scrollable and scroll on key down/up
- `typeaheadOptionsInScrollableView` (`?number`) - used to set how many items to show in the scrollable dropdown list (only used if `typeaheadScrollable` is `true`)
- `typeaheadAppendToBody` (*not implemented*) (`?boolean=false`) - if `true` the typeahead popup will be appended to $body instead of the parent element
- `typeaheadEditable` (*not implemented*) (`?boolean=true`) - if `false` restrict model values to the ones selected from the popup only will be provided
- `typeaheadFocusFirst` (*not implemented*) (`?boolean=true`) - if `false` the first match automatically will not be focused as you type
- `typeaheadInputFormatter` (*not implemented*) (`?any`) - format the ng-model result after selection
- `typeaheadSelectOnExact` (*not implemented*) (`?boolean=false`) - if `true` automatically select an item when there is one option that exactly matches the user input
- `typeaheadSelectOnBlur` (*not implemented*) (`?boolean=false`) - if `true` select the currently highlighted match on blur
- `typeaheadFocusOnSelect` (*not implemented*) (`?boolean=true`) - if `false` don't focus the input element the typeahead directive is associated with on selection


### Typeahead events

Expand Down
186 changes: 166 additions & 20 deletions components/typeahead/typeahead-container.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@ import { TypeaheadOptions } from './typeahead-options.class';
import { TypeaheadMatch } from './typeahead-match.class';

describe('Component: TypeaheadContainer', () => {
let fixture:ComponentFixture<TypeaheadContainerComponent>;
let component:TypeaheadContainerComponent;
let fixture: ComponentFixture<TypeaheadContainerComponent>;
let testModule: any;
let component: TypeaheadContainerComponent;
let containingElement: HTMLElement;

let options = new TypeaheadOptions({ animation: false, placement: 'bottom-left', typeaheadRef: undefined, scrollable: false, optionsInScrollableView: 3 });
beforeEach(() => {
fixture = TestBed.configureTestingModule({
testModule = TestBed.configureTestingModule({
declarations: [TypeaheadContainerComponent],
providers: [{
provide: TypeaheadOptions,
useValue: new TypeaheadOptions({animation: false, placement: 'bottom-left', typeaheadRef: undefined})
useValue: options
}]
}).createComponent(TypeaheadContainerComponent);
});
fixture = testModule.createComponent(TypeaheadContainerComponent);

component = fixture.componentInstance;
fixture.detectChanges();
component.ngAfterViewInit();
containingElement = asNativeElements(fixture.debugElement.queryAll(By.css('.dropdown-menu')));
});

it('should be defined', () => {
Expand All @@ -35,12 +41,11 @@ describe('Component: TypeaheadContainer', () => {
});

describe('dropdown-menu', () => {
let dropDown:HTMLElement;

let dropDown: HTMLElement;
beforeEach(() => {
component.position(fixture.elementRef);
fixture.detectChanges();

component.ngAfterViewInit();
dropDown = fixture.debugElement.query(By.css('.dropdown-menu')).nativeElement as HTMLElement;
});

Expand All @@ -59,19 +64,23 @@ describe('Component: TypeaheadContainer', () => {
it('should have left style set', () => {
expect(dropDown.style.left).toBe('8px');
});

it('should not be scrollable', () => {
expect(getComputedStyle(dropDown).getPropertyValue('overflow-y')).toBe('auto');
});
});

describe('matches', () => {
let matches:HTMLLIElement[];
let matches: HTMLLIElement[];

beforeEach(() => {
component.query = 'fo';
component.matches = [
new TypeaheadMatch({id: 0, name: 'foo'}, 'foo'),
new TypeaheadMatch({id: 1, name: 'food'}, 'food')
new TypeaheadMatch({ id: 0, name: 'foo' }, 'foo'),
new TypeaheadMatch({ id: 1, name: 'food' }, 'food')
];
fixture.detectChanges();

component.ngAfterViewInit();
matches = asNativeElements(fixture.debugElement.queryAll(By.css('.dropdown-menu li')));
});

Expand All @@ -80,6 +89,10 @@ describe('Component: TypeaheadContainer', () => {
expect(matches.length).toBe(2);
});

it('should not show scrollbars', () => {
expect(getComputedStyle(containingElement[0]).getPropertyValue('overflow-y')).toBe('auto');
});

it('should highlight query for match', () => {
expect(matches[1].children[0].innerHTML).toBe('<strong>fo</strong>od');
});
Expand Down Expand Up @@ -125,18 +138,19 @@ describe('Component: TypeaheadContainer', () => {
});

describe('grouped matches', () => {
let itemMatches:HTMLLIElement[];
let headerMatch:HTMLLIElement;
let itemMatches: HTMLLIElement[];
let headerMatch: HTMLLIElement;

beforeEach(() => {
component.query = 'a';
component.matches = [
new TypeaheadMatch('fruits', 'fruits', true),
new TypeaheadMatch({id: 0, name: 'banana', category: 'fruits'}, 'banana'),
new TypeaheadMatch({id: 0, name: 'apple', category: 'fruits'}, 'apple')
new TypeaheadMatch({ id: 0, name: 'banana', category: 'fruits' }, 'banana'),
new TypeaheadMatch({ id: 0, name: 'apple', category: 'fruits' }, 'apple')
];

fixture.detectChanges();
component.ngAfterViewInit();
headerMatch = fixture.debugElement.query(By.css('.dropdown-header')).nativeElement;
itemMatches = asNativeElements(fixture.debugElement.queryAll(By.css('.dropdown-menu li:not(.dropdown-header)')));
});
Expand Down Expand Up @@ -166,7 +180,6 @@ describe('Component: TypeaheadContainer', () => {
describe('nextActiveMatch', () => {
it('should select the next item match', () => {
component.nextActiveMatch();

expect(component.isActive(component.matches[2])).toBeTruthy();
});

Expand All @@ -181,14 +194,12 @@ describe('Component: TypeaheadContainer', () => {
describe('prevActiveMatch', () => {
it('should skip the header match', () => {
component.prevActiveMatch();

expect(component.isActive(component.matches[0])).toBeFalsy();
});

it('should select the first match again, when triggered twice', () => {
component.prevActiveMatch();
component.prevActiveMatch();

expect(component.isActive(component.matches[1])).toBeTruthy();
});
});
Expand All @@ -201,9 +212,144 @@ describe('Component: TypeaheadContainer', () => {

it('should not be focused on focusLost()', () => {
component.focusLost();

expect(component.isFocused).toBeFalsy();
});
});

describe('scrollable matches', () => {
let itemMatches: HTMLLIElement[];
let headerMatch: HTMLLIElement;
let containingElementScrollable: HTMLElement;

beforeEach(() => {

options.scrollable = true;
options.optionsInScrollableView = 3;
fixture = testModule.createComponent(TypeaheadContainerComponent);
fixture.detectChanges();
component = fixture.componentInstance;

component.query = 'a';
component.matches = [
// new TypeaheadMatch('fruits', 'fruits', true),
new TypeaheadMatch({ id: 0, name: 'banana', category: 'fruits' }, 'banana'),
new TypeaheadMatch({ id: 1, name: 'apple', category: 'fruits' }, 'apple'),
new TypeaheadMatch({ id: 2, name: 'orange', category: 'fruits' }, 'orange'),
new TypeaheadMatch({ id: 3, name: 'pear', category: 'fruits' }, 'pear'),
new TypeaheadMatch({ id: 4, name: 'pineapple', category: 'fruits' }, 'pineapple'),
new TypeaheadMatch('berries', 'berries', true),
new TypeaheadMatch({ id: 5, name: 'strawberry', category: 'berries' }, 'strawberry'),
new TypeaheadMatch({ id: 6, name: 'raspberry', category: 'berries' }, 'raspberry'),
new TypeaheadMatch('vegatables', 'vegatables', true),
new TypeaheadMatch({ id: 7, name: 'tomato', category: 'vegatables' }, 'tomato'),
new TypeaheadMatch({ id: 8, name: 'cucumber', category: 'vegatables' }, 'cucumber')
];

fixture.detectChanges();
component.ngAfterViewInit();
let headers = fixture.debugElement.queryAll(By.css('.dropdown-header'));
if (headers) {
headerMatch = asNativeElements(headers);
}
itemMatches = asNativeElements(fixture.debugElement.queryAll(By.css('.dropdown-menu li:not(.dropdown-header)')));
containingElementScrollable = asNativeElements(fixture.debugElement.queryAll(By.css('.dropdown-menu')));
});

describe('rendering', () => {
it('should render scrollable element', () => {
expect(containingElementScrollable[0]).toBeDefined();
});

it('should not throw exception when scrollPrevious is without li elements', () => {
(component as any).liElements = undefined;
(component as any).scrollPrevious(1);
expect(component.element.nativeElement.scrollTop).toBe(0);
});

it('should not throw exception when scrollPrevious is scrolling outside of index ', () => {
(component as any).scrollPrevious(100);
expect(component.element.nativeElement.scrollTop).toBe(0);

});

it('should not throw exception when scrollNext is without li elements', () => {
(component as any).liElements = undefined;

(component as any).scrollNext(1);
expect(component.element.nativeElement.scrollTop).toBe(0);

});

it('should not throw exception when scrollNext is scrolling outside of index', () => {
(component as any).scrollNext(100);
expect(component.element.nativeElement.scrollTop).toBe(0);
});

it('should render 9 item matches', () => {
expect(itemMatches.length).toBe(9);
});

it('should show scrollbars', () => {
expect(getComputedStyle(containingElementScrollable[0]).getPropertyValue('overflow-y')).toBe('scroll');
});

xit('should show correct height on scrollable element', () => {
expect(getComputedStyle(containingElementScrollable[0]).getPropertyValue('height')).toBe('60px');
});

it('should highlight query for item match', () => {
expect(itemMatches[1].children[0].innerHTML).toBe('<strong>a</strong>pple');
});

it('should set the \"active\" class on the first item match', () => {
expect(itemMatches[0].classList.contains('active')).toBeTruthy();
});
});

describe('nextActiveMatch', () => {
it('should select the next item match', () => {
component.nextActiveMatch();
expect(component.isActive(component.matches[1])).toBeTruthy();
});
it('should select the next item match and scroll', () => {
component.nextActiveMatch();
component.nextActiveMatch();
expect(component.isActive(component.matches[2])).toBeTruthy();
expect(containingElementScrollable[0].scrollTop).toBe(itemMatches[2].offsetTop - containingElementScrollable[0].offsetHeight + itemMatches[2].offsetHeight);

});
it('should select the last item match and scroll', () => {
for (let i = 0; i < 8; i++) {
component.nextActiveMatch();
}
expect(component.isActive(component.matches[10])).toBeTruthy();
});

it('should select the first item match and scroll to top', () => {
for (let i = 0; i < 9; i++) {
component.nextActiveMatch();
}
expect(component.isActive(component.matches[0])).toBeTruthy();
expect(containingElementScrollable[0].scrollTop).toBe(0);
});
});

describe('prevActiveMatch', () => {
it('should select the last item and scroll to bottom', () => {
component.prevActiveMatch();
expect(component.isActive(component.matches[10])).toBeTruthy();
expect(containingElementScrollable[0].scrollTop <= containingElementScrollable[0].scrollHeight).toBeTruthy();
});

it('should select the prev item match', () => {
component.nextActiveMatch();
component.nextActiveMatch();
component.nextActiveMatch();
component.prevActiveMatch();
expect(component.isActive(component.matches[2])).toBeTruthy();
});
});

});

});
Loading