diff --git a/src/framework/theme/components/cdk/a11y/a11y.module.ts b/src/framework/theme/components/cdk/a11y/a11y.module.ts index ba53bae9ac..72355b1086 100644 --- a/src/framework/theme/components/cdk/a11y/a11y.module.ts +++ b/src/framework/theme/components/cdk/a11y/a11y.module.ts @@ -1,14 +1,17 @@ import { ModuleWithProviders, NgModule } from '@angular/core'; import { NbFocusTrapFactoryService } from './focus-trap'; - +import { NbFocusKeyManagerFactoryService } from './focus-key-manager'; @NgModule({}) export class NbA11yModule { static forRoot() { return { ngModule: NbA11yModule, - providers: [NbFocusTrapFactoryService], + providers: [ + NbFocusTrapFactoryService, + NbFocusKeyManagerFactoryService, + ], }; } } diff --git a/src/framework/theme/components/cdk/a11y/focus-key-manager.ts b/src/framework/theme/components/cdk/a11y/focus-key-manager.ts index aca69266c6..7e5ea1a604 100644 --- a/src/framework/theme/components/cdk/a11y/focus-key-manager.ts +++ b/src/framework/theme/components/cdk/a11y/focus-key-manager.ts @@ -1,4 +1,11 @@ +import { QueryList } from '@angular/core'; import { FocusableOption, FocusKeyManager } from '@angular/cdk/a11y'; export type NbFocusableOption = FocusableOption; export class NbFocusKeyManager extends FocusKeyManager {} + +export class NbFocusKeyManagerFactoryService { + create(items: QueryList | T[]): NbFocusKeyManager { + return new NbFocusKeyManager(items); + } +} diff --git a/src/framework/theme/components/select/select.component.ts b/src/framework/theme/components/select/select.component.ts index 647418922f..c3c00f54a9 100644 --- a/src/framework/theme/components/select/select.component.ts +++ b/src/framework/theme/components/select/select.component.ts @@ -37,7 +37,7 @@ import { import { NbOverlayRef, NbPortalDirective, NbScrollStrategy } from '../cdk/overlay/mapping'; import { NbOverlayService } from '../cdk/overlay/overlay-service'; import { NbTrigger, NbTriggerStrategy, NbTriggerStrategyBuilderService } from '../cdk/overlay/overlay-trigger'; -import { NbFocusKeyManager } from '../cdk/a11y/focus-key-manager'; +import { NbFocusKeyManager, NbFocusKeyManagerFactoryService } from '../cdk/a11y/focus-key-manager'; import { ESCAPE } from '../cdk/keycodes/keycodes'; import { NbComponentSize } from '../component-size'; import { NbComponentShape } from '../component-shape'; @@ -637,7 +637,8 @@ export class NbSelectComponent implements AfterViewInit, AfterContentInit, On protected hostRef: ElementRef, protected positionBuilder: NbPositionBuilderService, protected triggerStrategyBuilder: NbTriggerStrategyBuilderService, - protected cd: ChangeDetectorRef) { + protected cd: ChangeDetectorRef, + protected focusKeyManagerFactoryService: NbFocusKeyManagerFactoryService>) { } /** @@ -862,7 +863,7 @@ export class NbSelectComponent implements AfterViewInit, AfterContentInit, On } protected createKeyManager(): void { - this.keyManager = new NbFocusKeyManager>(this.options).withTypeAhead(200); + this.keyManager = this.focusKeyManagerFactoryService.create(this.options).withTypeAhead(200); } protected createPositionStrategy(): NbAdjustableConnectedPositionStrategy { @@ -887,12 +888,14 @@ export class NbSelectComponent implements AfterViewInit, AfterContentInit, On protected subscribeOnTriggers() { this.triggerStrategy.show$.subscribe(() => this.show()); - this.triggerStrategy.hide$.subscribe(($event: Event) => { - this.hide(); - if (!this.isClickedWithinComponent($event)) { - this.onTouched(); - } - }); + this.triggerStrategy.hide$ + .pipe(filter(() => this.isOpen)) + .subscribe(($event: Event) => { + this.hide(); + if (!this.isClickedWithinComponent($event)) { + this.onTouched(); + } + }); } protected subscribeOnPositionChange() { @@ -938,7 +941,10 @@ export class NbSelectComponent implements AfterViewInit, AfterContentInit, On this.keyManager.tabOut .pipe(takeWhile(() => this.alive)) - .subscribe(() => this.hide()); + .subscribe(() => { + this.hide(); + this.onTouched(); + }); } protected getContainer() { diff --git a/src/framework/theme/components/select/select.spec.ts b/src/framework/theme/components/select/select.spec.ts index bfe715c122..85c19d2167 100644 --- a/src/framework/theme/components/select/select.spec.ts +++ b/src/framework/theme/components/select/select.spec.ts @@ -18,17 +18,21 @@ import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testi import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; -import { from, zip } from 'rxjs'; +import { from, zip, Subject } from 'rxjs'; import createSpy = jasmine.createSpy; -import { NbSelectModule } from './select.module'; -import { NbThemeModule } from '../../theme.module'; -import { NbOverlayContainerAdapter } from '../cdk/adapter/overlay-container-adapter'; -import { NB_DOCUMENT } from '../../theme.options'; -import { NbSelectComponent } from './select.component'; -import { NbLayoutModule } from '../layout/layout.module'; -import { NbOptionComponent } from './option.component'; -import { NbOptionGroupComponent } from './option-group.component'; +import { + NbSelectModule, + NbThemeModule, + NbOverlayContainerAdapter, + NB_DOCUMENT, + NbSelectComponent, + NbLayoutModule, + NbOptionComponent, + NbOptionGroupComponent, + NbTriggerStrategyBuilderService, +} from '@nebular/theme'; +import { NbFocusKeyManagerFactoryService } from '@nebular/theme/components/cdk/a11y/focus-key-manager'; const eventMock = { preventDefault() {} } as Event; @@ -99,6 +103,23 @@ export class NbSelectTestComponent { groups = TEST_GROUPS; } +@Component({ + template: ` + + + + + a + b + c + + + + + `, +}) +export class BasicSelectTestComponent {} + @Component({ template: ` @@ -581,7 +602,7 @@ describe('Component: NbSelectComponent', () => { })); it(`should not call dispose on uninitialized resources`, () => { - const selectFixture = new NbSelectComponent(null, null, null, null, null, null); + const selectFixture = new NbSelectComponent(null, null, null, null, null, null, null); expect(() => selectFixture.ngOnDestroy()).not.toThrow(); }); @@ -632,6 +653,33 @@ describe('Component: NbSelectComponent', () => { expect(selectFixture.componentInstance.isOpen).toBeFalsy(); })); + + it('should mark touched when select button loose focus and select closed', fakeAsync(() => { + const touchedSpy = jasmine.createSpy('touched spy'); + + const selectFixture = TestBed.createComponent(NbSelectComponent); + const selectComponent: NbSelectComponent = selectFixture.componentInstance; + selectFixture.detectChanges(); + flush(); + + selectComponent.registerOnTouched(touchedSpy); + selectFixture.debugElement.query(By.css('.select-button')).triggerEventHandler('blur', {}); + expect(touchedSpy).toHaveBeenCalledTimes(1); + })); + + it('should not mark touched when select button loose focus and select open', fakeAsync(() => { + const touchedSpy = jasmine.createSpy('touched spy'); + + const selectFixture = TestBed.createComponent(NbSelectComponent); + const selectComponent: NbSelectComponent = selectFixture.componentInstance; + selectFixture.detectChanges(); + flush(); + + selectComponent.registerOnTouched(touchedSpy); + selectComponent.show(); + selectFixture.debugElement.query(By.css('.select-button')).triggerEventHandler('blur', {}); + expect(touchedSpy).not.toHaveBeenCalled(); + })); }); describe('NbSelectComponent - falsy values', () => { @@ -767,6 +815,109 @@ describe('NbSelectComponent - falsy values', () => { }); }); +describe('NbSelectComponent - Triggers', () => { + let fixture: ComponentFixture; + let selectComponent: NbSelectComponent; + let triggerBuilderStub; + let showTriggerStub: Subject; + let hideTriggerStub: Subject; + + beforeEach(fakeAsync(() => { + showTriggerStub = new Subject(); + hideTriggerStub = new Subject(); + triggerBuilderStub = { + trigger() { return this }, + host() { return this }, + container() { return this }, + destroy() {}, + build() { + return { show$: showTriggerStub, hide$: hideTriggerStub }; + }, + }; + + TestBed.configureTestingModule({ + imports: [ RouterTestingModule.withRoutes([]), NbThemeModule.forRoot(), NbLayoutModule, NbSelectModule ], + declarations: [ BasicSelectTestComponent ], + }); + TestBed.overrideProvider(NbTriggerStrategyBuilderService, { useValue: triggerBuilderStub }); + + fixture = TestBed.createComponent(BasicSelectTestComponent); + fixture.detectChanges(); + flush(); + + selectComponent = fixture.debugElement.query(By.directive(NbSelectComponent)).componentInstance; + })); + + it('should mark touched if clicked outside of overlay and select', fakeAsync(() => { + const touchedSpy = jasmine.createSpy('touched spy'); + selectComponent.registerOnTouched(touchedSpy); + + const elementOutsideSelect = fixture.debugElement.query(By.css('nb-layout')).nativeElement; + selectComponent.show(); + fixture.detectChanges(); + + hideTriggerStub.next({ target: elementOutsideSelect } as unknown as Event); + + expect(touchedSpy).toHaveBeenCalledTimes(1); + })); + + it('should not mark touched if clicked on the select button', fakeAsync(() => { + const touchedSpy = jasmine.createSpy('touched spy'); + selectComponent.registerOnTouched(touchedSpy); + + const selectButton = fixture.debugElement.query(By.css('.select-button')).nativeElement; + selectComponent.show(); + fixture.detectChanges(); + + hideTriggerStub.next({ target: selectButton } as unknown as Event); + + expect(touchedSpy).not.toHaveBeenCalled(); + })); +}); + +describe('NbSelectComponent - Key manager', () => { + let fixture: ComponentFixture; + let selectComponent: NbSelectComponent; + let tabOutStub: Subject; + let keyManagerFactoryStub; + let keyManagerStub; + + beforeEach(fakeAsync(() => { + tabOutStub = new Subject(); + keyManagerStub = { + withTypeAhead() { return this; }, + setActiveItem() {}, + setFirstItemActive() {}, + onKeydown() {}, + tabOut: tabOutStub, + }; + keyManagerFactoryStub = { create() { return keyManagerStub; } }; + + TestBed.configureTestingModule({ + imports: [ RouterTestingModule.withRoutes([]), NbThemeModule.forRoot(), NbLayoutModule, NbSelectModule ], + declarations: [ BasicSelectTestComponent ], + }); + TestBed.overrideProvider(NbFocusKeyManagerFactoryService, { useValue: keyManagerFactoryStub }); + + fixture = TestBed.createComponent(BasicSelectTestComponent); + fixture.detectChanges(); + flush(); + + selectComponent = fixture.debugElement.query(By.directive(NbSelectComponent)).componentInstance; + })); + + it('should mark touched when tabbing out from options list', fakeAsync(() => { + selectComponent.show(); + fixture.detectChanges(); + + const touchedSpy = jasmine.createSpy('touched spy'); + selectComponent.registerOnTouched(touchedSpy); + tabOutStub.next(); + flush(); + expect(touchedSpy).toHaveBeenCalledTimes(1); + })); +}); + describe('NbOptionComponent', () => { let fixture: ComponentFixture; let testSelectComponent: NbReactiveFormSelectComponent;