diff --git a/src/framework/theme/components/checkbox/checkbox.component.ts b/src/framework/theme/components/checkbox/checkbox.component.ts
index 9f4369364f..ba8fcc971d 100644
--- a/src/framework/theme/components/checkbox/checkbox.component.ts
+++ b/src/framework/theme/components/checkbox/checkbox.component.ts
@@ -4,7 +4,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*/
-import { Component, Input, HostBinding, forwardRef } from '@angular/core';
+import { Component, Input, HostBinding, forwardRef, ChangeDetectorRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { convertToBoolProperty } from '../helpers';
@@ -58,7 +58,8 @@ import { convertToBoolProperty } from '../helpers';
+ (change)="value = !value"
+ (blur)="setTouched()">
@@ -123,9 +124,10 @@ export class NbCheckboxComponent implements ControlValueAccessor {
set value(val) {
this._value = val;
this.onChange(val);
- this.onTouched();
}
+ constructor(private changeDetector: ChangeDetectorRef) {}
+
registerOnChange(fn: any) {
this.onChange = fn;
}
@@ -135,10 +137,15 @@ export class NbCheckboxComponent implements ControlValueAccessor {
}
writeValue(val: any) {
- this.value = val;
+ this._value = val;
+ this.changeDetector.detectChanges();
}
setDisabledState(val: boolean) {
this.disabled = convertToBoolProperty(val);
}
+
+ setTouched() {
+ this.onTouched();
+ }
}
diff --git a/src/framework/theme/components/datepicker/datepicker.component.ts b/src/framework/theme/components/datepicker/datepicker.component.ts
index e695927bdb..bcebcdef8f 100644
--- a/src/framework/theme/components/datepicker/datepicker.component.ts
+++ b/src/framework/theme/components/datepicker/datepicker.component.ts
@@ -151,6 +151,8 @@ export abstract class NbBasePicker extends NbDatepicker implements O
* */
protected queue: T;
+ protected blur$: Subject = new Subject();
+
constructor(@Inject(NB_DOCUMENT) protected document,
protected positionBuilder: NbPositionBuilderService,
protected overlay: NbOverlayService,
@@ -172,6 +174,17 @@ export abstract class NbBasePicker extends NbDatepicker implements O
return this.onChange$.asObservable();
}
+ get isShown(): boolean {
+ return this.ref && this.ref.hasAttached();
+ }
+
+ /**
+ * Emits when datepicker looses focus.
+ */
+ get blur(): Observable {
+ return this.blur$.asObservable();
+ }
+
protected abstract get pickerValueChange(): Observable;
ngOnDestroy() {
@@ -250,7 +263,10 @@ export abstract class NbBasePicker extends NbDatepicker implements O
protected subscribeOnTriggers() {
const triggerStrategy = this.createTriggerStrategy();
triggerStrategy.show$.pipe(takeWhile(() => this.alive)).subscribe(() => this.show());
- triggerStrategy.hide$.pipe(takeWhile(() => this.alive)).subscribe(() => this.hide());
+ triggerStrategy.hide$.pipe(takeWhile(() => this.alive)).subscribe(() => {
+ this.blur$.next();
+ this.hide();
+ });
}
protected instantiatePicker() {
diff --git a/src/framework/theme/components/datepicker/datepicker.directive.ts b/src/framework/theme/components/datepicker/datepicker.directive.ts
index ec7639cd82..7132bd9bda 100644
--- a/src/framework/theme/components/datepicker/datepicker.directive.ts
+++ b/src/framework/theme/components/datepicker/datepicker.directive.ts
@@ -15,8 +15,8 @@ import {
Validators,
} from '@angular/forms';
import { Type } from '@angular/core/src/type';
-import { fromEvent, Observable } from 'rxjs';
-import { map, takeWhile } from 'rxjs/operators';
+import { fromEvent, Observable, merge } from 'rxjs';
+import { map, takeWhile, filter, take } from 'rxjs/operators';
import { NB_DOCUMENT } from '../../theme.options';
import { NbDateService } from '../calendar-kit';
@@ -99,6 +99,10 @@ export abstract class NbDatepicker {
abstract hide();
abstract shouldHide(): boolean;
+
+ abstract get isShown(): boolean;
+
+ abstract get blur(): Observable;
}
export const NB_DATE_ADAPTER = new InjectionToken>('Datepicker Adapter');
@@ -225,8 +229,8 @@ export class NbDatepickerDirective implements OnDestroy, ControlValueAccessor
* */
protected picker: NbDatepicker;
protected alive: boolean = true;
- protected onChange: (D) => void = () => {
- };
+ protected onChange: (D) => void = () => {};
+ protected onTouched: () => void = () => {};
/**
* Form control validators will be called in validators context, so, we need to bind them.
@@ -276,9 +280,11 @@ export class NbDatepickerDirective implements OnDestroy, ControlValueAccessor
}
registerOnTouched(fn: any): void {
+ this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
+ this.input.disabled = isDisabled;
}
/**
@@ -367,6 +373,16 @@ export class NbDatepickerDirective implements OnDestroy, ControlValueAccessor
this.hidePicker();
}
});
+
+ merge(
+ this.picker.blur,
+ fromEvent(this.input, 'blur').pipe(
+ filter(() => !this.picker.isShown && this.document.activeElement !== this.input),
+ ),
+ ).pipe(
+ takeWhile(() => this.alive),
+ take(1),
+ ).subscribe(() => this.onTouched());
}
protected writePicker(value: D) {
diff --git a/src/framework/theme/components/radio/radio-group.component.ts b/src/framework/theme/components/radio/radio-group.component.ts
index 179d74b2da..ef90cfa199 100644
--- a/src/framework/theme/components/radio/radio-group.component.ts
+++ b/src/framework/theme/components/radio/radio-group.component.ts
@@ -16,12 +16,17 @@ import {
OnDestroy,
Output,
QueryList,
+ PLATFORM_ID,
+ Inject,
+ ElementRef,
} from '@angular/core';
-import { NbRadioComponent } from './radio.component';
-import { merge } from 'rxjs';
-import { takeWhile } from 'rxjs/operators';
+import { isPlatformBrowser } from '@angular/common';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { fromEvent, merge } from 'rxjs';
+import { filter, switchMap, take, takeWhile } from 'rxjs/operators';
import { convertToBoolProperty } from '../helpers';
+import { NB_DOCUMENT } from '../../theme.options';
+import { NbRadioComponent } from './radio.component';
/**
@@ -104,14 +109,21 @@ export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, Contr
protected name: string;
protected alive: boolean = true;
protected onChange = (value: any) => {};
+ protected onTouched = () => {};
- constructor(protected cd: ChangeDetectorRef) {}
+ constructor(
+ protected cd: ChangeDetectorRef,
+ protected hostElement: ElementRef,
+ @Inject(PLATFORM_ID) protected platformId,
+ @Inject(NB_DOCUMENT) protected document,
+ ) {}
ngAfterContentInit() {
this.updateNames();
this.updateValues();
this.updateDisabled();
this.subscribeOnRadiosValueChange();
+ this.subscribeOnRadiosBlur();
}
ngOnDestroy() {
@@ -123,6 +135,7 @@ export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, Contr
}
registerOnTouched(fn: any): void {
+ this.onTouched = fn;
}
writeValue(value: any): void {
@@ -171,4 +184,23 @@ export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, Contr
protected markRadiosForCheck() {
this.radios.forEach((radio: NbRadioComponent) => radio.markForCheck());
}
+
+ protected subscribeOnRadiosBlur() {
+ if (!isPlatformBrowser(this.platformId)) {
+ return;
+ }
+
+ const hostElement = this.hostElement.nativeElement;
+ fromEvent(hostElement, 'focusin')
+ .pipe(
+ filter(event => hostElement.contains(event.target as Node)),
+ switchMap(() => merge(
+ fromEvent(this.document, 'focusin'),
+ fromEvent(this.document, 'click'),
+ )),
+ filter(event => !hostElement.contains(event.target as Node)),
+ take(1),
+ )
+ .subscribe(() => this.onTouched());
+ }
}
diff --git a/src/framework/theme/components/radio/radio.component.ts b/src/framework/theme/components/radio/radio.component.ts
index 6a28ec0df1..675a408591 100644
--- a/src/framework/theme/components/radio/radio.component.ts
+++ b/src/framework/theme/components/radio/radio.component.ts
@@ -87,6 +87,7 @@ import { convertToBoolProperty } from '../helpers';
styleUrls: ['./radio.component.scss'],
})
export class NbRadioComponent {
+
@Input() name: string;
@Input() checked: boolean;
@@ -100,6 +101,8 @@ export class NbRadioComponent {
@Output() valueChange: EventEmitter = new EventEmitter();
+ @Output() blur: EventEmitter = new EventEmitter();
+
disabled: boolean;
constructor(protected cd: ChangeDetectorRef) {}
diff --git a/src/framework/theme/components/radio/radio.spec.ts b/src/framework/theme/components/radio/radio.spec.ts
index cf47708060..0ee3dbacb1 100644
--- a/src/framework/theme/components/radio/radio.spec.ts
+++ b/src/framework/theme/components/radio/radio.spec.ts
@@ -8,6 +8,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NbRadioModule } from './radio.module';
import { NbRadioComponent } from './radio.component';
+import { NB_DOCUMENT } from '../../theme.options';
import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core';
import { By } from '@angular/platform-browser';
@@ -35,6 +36,7 @@ describe('radio', () => {
TestBed.configureTestingModule({
imports: [NbRadioModule],
declarations: [NbRadioTestComponent],
+ providers: [ { provide: NB_DOCUMENT, useValue: document } ],
});
fixture = TestBed.createComponent(NbRadioTestComponent);
diff --git a/src/framework/theme/components/select/select.component.html b/src/framework/theme/components/select/select.component.html
index 70689f8dea..dd437c5b74 100644
--- a/src/framework/theme/components/select/select.component.html
+++ b/src/framework/theme/components/select/select.component.html
@@ -8,7 +8,8 @@
[fullWidth]="fullWidth"
[outline]="outline"
[class.opened]="isOpened"
- [ngClass]="overlayPosition">
+ [ngClass]="overlayPosition"
+ (blur)="trySetTouched()">
diff --git a/src/framework/theme/components/select/select.component.ts b/src/framework/theme/components/select/select.component.ts
index ab8d7f639b..94d1e5efb9 100644
--- a/src/framework/theme/components/select/select.component.ts
+++ b/src/framework/theme/components/select/select.component.ts
@@ -41,6 +41,7 @@ import {
NbTriggerStrategyBuilder,
} from '../cdk';
import { NbOptionComponent } from './option.component';
+import { NbButtonComponent } from '../button/button.component';
import { NB_DOCUMENT } from '../../theme.options';
import { convertToBoolProperty } from '../helpers';
@@ -112,7 +113,7 @@ export class NbSelectLabelComponent {
*
* @stacked-example(Select statuses, select/select-status.component)
*
- * There are three select sizes:
+ * There are four select sizes:
*
* @stacked-example(Select sizes, select/select-sizes.component)
*
@@ -233,6 +234,8 @@ export class NbSelectComponent implements OnInit, AfterViewInit, AfterContent
* */
@ViewChild(NbPortalDirective) portal: NbPortalDirective;
+ @ViewChild(NbButtonComponent, { read: ElementRef }) button: ElementRef;
+
multiple: boolean = false;
/**
@@ -270,10 +273,11 @@ export class NbSelectComponent implements OnInit, AfterViewInit, AfterContent
* Function passed through control value accessor to propagate changes.
* */
protected onChange: Function = () => {};
+ protected onTouched: Function = () => {};
constructor(@Inject(NB_DOCUMENT) protected document,
protected overlay: NbOverlayService,
- protected hostRef: ElementRef,
+ protected hostRef: ElementRef,
protected positionBuilder: NbPositionBuilderService,
protected cd: ChangeDetectorRef) {
}
@@ -349,9 +353,12 @@ export class NbSelectComponent implements OnInit, AfterViewInit, AfterContent
}
registerOnTouched(fn: any): void {
+ this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ this.cd.detectChanges();
}
writeValue(value: T | T[]): void {
@@ -386,6 +393,7 @@ export class NbSelectComponent implements OnInit, AfterViewInit, AfterContent
this.selectionModel.forEach((option: NbOptionComponent) => option.deselect());
this.selectionModel = [];
this.hide();
+ this.button.nativeElement.focus();
this.emitSelected(null);
}
@@ -413,6 +421,7 @@ export class NbSelectComponent implements OnInit, AfterViewInit, AfterContent
this.selectionModel = [option];
option.select();
this.hide();
+ this.button.nativeElement.focus();
this.emitSelected(option.value);
}
@@ -464,7 +473,12 @@ export class NbSelectComponent implements OnInit, AfterViewInit, AfterContent
triggerStrategy.hide$
.pipe(takeWhile(() => this.alive))
- .subscribe(() => this.hide());
+ .subscribe(($event: Event) => {
+ this.hide();
+ if (!this.isClickedWithinComponent($event)) {
+ this.onTouched();
+ }
+ });
}
protected subscribeOnPositionChange() {
@@ -541,4 +555,18 @@ export class NbSelectComponent implements OnInit, AfterViewInit, AfterContent
this.selectionModel.push(corresponding);
}
}
+
+ /**
+ * Sets touched if focus moved outside of button and overlay,
+ * ignoring the case when focus moved to options overlay.
+ */
+ trySetTouched() {
+ if (this.isHidden) {
+ this.onTouched();
+ }
+ }
+
+ protected isClickedWithinComponent($event: Event) {
+ return this.hostRef.nativeElement === $event.target || this.hostRef.nativeElement.contains($event.target as Node);
+ }
}