diff --git a/src/app/playground-components.ts b/src/app/playground-components.ts
index f8782fe708..309d3a8b53 100644
--- a/src/app/playground-components.ts
+++ b/src/app/playground-components.ts
@@ -1162,10 +1162,10 @@ export const PLAYGROUND_COMPONENTS: ComponentLink[] = [
name: 'Select Icon',
},
{
- path: 'select-search-showcase.component',
- link: '/select/select-search-showcase.component',
- component: 'SelectSearchShowcaseComponent',
- name: 'Select Search Showcase',
+ path: 'select-autocomplete-showcase.component',
+ link: '/select/select-autocomplete-showcase.component',
+ component: 'SelectAutocompleteShowcaseComponent',
+ name: 'Select Autocomplete Showcase',
},
],
},
diff --git a/src/framework/theme/components/option/option.component.ts b/src/framework/theme/components/option/option.component.ts
index 0d896dbcab..8dc4ef8dd2 100644
--- a/src/framework/theme/components/option/option.component.ts
+++ b/src/framework/theme/components/option/option.component.ts
@@ -82,16 +82,11 @@ import { NbSelectComponent } from '../select/select.component';
styleUrls: ['./option.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
-
-
+
`,
})
export class NbOptionComponent implements OnDestroy, AfterViewInit, NbFocusableOption, NbHighlightableOption {
-
protected disabledByGroup = false;
/**
@@ -132,11 +127,13 @@ export class NbOptionComponent implements OnDestroy, AfterViewInit, NbF
@HostBinding('attr.id')
id: string = `nb-option-${lastOptionId++}`;
- constructor(@Optional() @Inject(NB_SELECT_INJECTION_TOKEN) parent,
- protected elementRef: ElementRef,
- protected cd: ChangeDetectorRef,
- protected zone: NgZone,
- protected renderer: Renderer2) {
+ constructor(
+ @Optional() @Inject(NB_SELECT_INJECTION_TOKEN) parent,
+ protected elementRef: ElementRef,
+ protected cd: ChangeDetectorRef,
+ protected zone: NgZone,
+ protected renderer: Renderer2,
+ ) {
this.parent = parent;
}
@@ -146,9 +143,11 @@ export class NbOptionComponent implements OnDestroy, AfterViewInit, NbF
ngAfterViewInit() {
// TODO: #2254
- this.zone.runOutsideAngular(() => setTimeout(() => {
- this.renderer.addClass(this.elementRef.nativeElement, 'nb-transition');
- }));
+ this.zone.runOutsideAngular(() =>
+ setTimeout(() => {
+ this.renderer.addClass(this.elementRef.nativeElement, 'nb-transition');
+ }),
+ );
}
/**
@@ -162,6 +161,10 @@ export class NbOptionComponent implements OnDestroy, AfterViewInit, NbF
return this.elementRef.nativeElement.textContent;
}
+ get hidden() {
+ return this.elementRef.nativeElement.hidden;
+ }
+
// TODO: replace with isShowCheckbox property to control this behaviour outside, issues/1965
@HostBinding('class.multiple')
get multiple() {
@@ -188,7 +191,7 @@ export class NbOptionComponent implements OnDestroy, AfterViewInit, NbF
@HostBinding('class.active')
get activeClass() {
return this._active;
- };
+ }
protected _active: boolean = false;
@HostListener('click', ['$event'])
@@ -252,5 +255,4 @@ export class NbOptionComponent implements OnDestroy, AfterViewInit, NbF
this._active = false;
this.cd.markForCheck();
}
-
}
diff --git a/src/framework/theme/components/select-with-autocomplete/_select-with-autocomplete-filled.scss b/src/framework/theme/components/select-with-autocomplete/_select-with-autocomplete-filled.scss
new file mode 100644
index 0000000000..fc8ed37892
--- /dev/null
+++ b/src/framework/theme/components/select-with-autocomplete/_select-with-autocomplete-filled.scss
@@ -0,0 +1,57 @@
+@use '../../styles/theming' as *;
+@use '../form-field/form-field.component.theme' as form-field-theme;
+
+@mixin select-with-autocomplete-filled {
+ nb-select-with-autocomplete.appearance-filled .select-button {
+ border-style: nb-theme(select-filled-border-style);
+ border-width: nb-theme(select-filled-border-width);
+ }
+
+ @each $size in nb-get-sizes() {
+ nb-select-with-autocomplete.appearance-filled.size-#{$size} .select-button {
+ padding: nb-theme(select-filled-#{$size}-padding);
+ @include nb-ltr(padding-right, nb-theme(select-icon-offset));
+ @include nb-rtl(padding-left, nb-theme(select-icon-offset));
+ }
+
+ @include form-field-theme.nb-form-field-with-prefix(
+ 'nb-select-with-autocomplete.appearance-filled.size-#{$size} .select-button',
+ $size
+ );
+ }
+
+ @each $status in nb-get-statuses() {
+ nb-select-with-autocomplete.appearance-filled.status-#{$status} .select-button {
+ background-color: nb-theme(select-filled-#{$status}-background-color);
+ border-color: nb-theme(select-filled-#{$status}-border-color);
+ color: nb-theme(select-filled-#{$status}-text-color);
+
+ &.placeholder {
+ color: nb-theme(select-filled-#{$status}-placeholder-text-color);
+ }
+
+ &:focus {
+ background-color: nb-theme(select-filled-#{$status}-focus-background-color);
+ border-color: nb-theme(select-filled-#{$status}-focus-border-color);
+ }
+ &:hover {
+ background-color: nb-theme(select-filled-#{$status}-hover-background-color);
+ border-color: nb-theme(select-filled-#{$status}-hover-border-color);
+ }
+
+ &[disabled] {
+ background-color: nb-theme(select-filled-#{$status}-disabled-background-color);
+ border-color: nb-theme(select-filled-#{$status}-disabled-border-color);
+ color: nb-theme(select-filled-#{$status}-disabled-text-color);
+
+ nb-icon {
+ color: nb-theme(select-filled-#{$status}-disabled-icon-color);
+ }
+ }
+
+ nb-icon {
+ color: nb-theme(select-filled-#{$status}-icon-color);
+ }
+ }
+ }
+}
diff --git a/src/framework/theme/components/select-with-autocomplete/_select-with-autocomplete-hero.scss b/src/framework/theme/components/select-with-autocomplete/_select-with-autocomplete-hero.scss
new file mode 100644
index 0000000000..aba656a9b3
--- /dev/null
+++ b/src/framework/theme/components/select-with-autocomplete/_select-with-autocomplete-hero.scss
@@ -0,0 +1,57 @@
+@use '../../styles/theming' as *;
+@use '../form-field/form-field.component.theme' as form-field-theme;
+
+@mixin select-with-autocomplete-hero {
+ nb-select-with-autocomplete.appearance-hero .select-button {
+ border: none;
+ }
+
+ @each $size in nb-get-sizes() {
+ nb-select-with-autocomplete.appearance-hero.size-#{$size} .select-button {
+ padding: nb-theme(select-hero-#{$size}-padding);
+ @include nb-ltr(padding-right, nb-theme(select-icon-offset));
+ @include nb-rtl(padding-left, nb-theme(select-icon-offset));
+ }
+ @include form-field-theme.nb-form-field-with-prefix(
+ 'nb-select-with-autocomplete.appearance-hero.size-#{$size} .select-button',
+ $size
+ );
+ }
+
+ @each $status in nb-get-statuses() {
+ nb-select-with-autocomplete.appearance-hero.status-#{$status} .select-button {
+ $left-color: nb-theme(select-hero-#{$status}-left-background-color);
+ $right-color: nb-theme(select-hero-#{$status}-right-background-color);
+ background-image: linear-gradient(to right, $left-color, $right-color);
+ color: nb-theme(select-hero-#{$status}-text-color);
+
+ &.placeholder {
+ color: nb-theme(select-hero-#{$status}-placeholder-text-color);
+ }
+
+ &:focus {
+ $left-color: nb-theme(select-hero-#{$status}-focus-left-background-color);
+ $right-color: nb-theme(select-hero-#{$status}-focus-right-background-color);
+ background-image: linear-gradient(to right, $left-color, $right-color);
+ }
+ &:hover {
+ $left-color: nb-theme(select-hero-#{$status}-hover-left-background-color);
+ $right-color: nb-theme(select-hero-#{$status}-hover-right-background-color);
+ background-image: linear-gradient(to right, $left-color, $right-color);
+ }
+ &[disabled] {
+ color: nb-theme(select-hero-#{$status}-disabled-text-color);
+ background-color: nb-theme(select-hero-#{$status}-disabled-background-color);
+ background-image: none;
+
+ nb-icon {
+ color: nb-theme(select-hero-#{$status}-disabled-icon-color);
+ }
+ }
+
+ nb-icon {
+ color: nb-theme(select-hero-#{$status}-icon-color);
+ }
+ }
+ }
+}
diff --git a/src/framework/theme/components/select-with-autocomplete/_select-with-autocomplete-outline.scss b/src/framework/theme/components/select-with-autocomplete/_select-with-autocomplete-outline.scss
new file mode 100644
index 0000000000..176913da04
--- /dev/null
+++ b/src/framework/theme/components/select-with-autocomplete/_select-with-autocomplete-outline.scss
@@ -0,0 +1,77 @@
+@use '../../styles/theming' as *;
+@use '../form-field/form-field.component.theme' as form-field-theme;
+
+@mixin select-with-autocomplete-outline {
+ nb-select-with-autocomplete.appearance-outline .select-button {
+ border-style: nb-theme(select-outline-border-style);
+ border-width: nb-theme(select-outline-border-width);
+
+ &.top {
+ border-top-style: nb-theme(select-outline-adjacent-border-style);
+ border-top-width: nb-theme(select-outline-adjacent-border-width);
+ }
+ &.bottom {
+ border-bottom-style: nb-theme(select-outline-adjacent-border-style);
+ border-bottom-width: nb-theme(select-outline-adjacent-border-width);
+ }
+ }
+
+ @each $status in nb-get-statuses() {
+ nb-select-with-autocomplete.appearance-outline.status-#{$status} .select-button {
+ background-color: nb-theme(select-outline-#{$status}-background-color);
+ border-color: nb-theme(select-outline-#{$status}-border-color);
+ color: nb-theme(select-outline-#{$status}-text-color);
+
+ &.placeholder {
+ color: nb-theme(select-outline-#{$status}-placeholder-text-color);
+ }
+ nb-icon {
+ color: nb-theme(select-outline-#{$status}-icon-color);
+ }
+
+ &:focus {
+ background-color: nb-theme(select-outline-#{$status}-focus-background-color);
+ border-color: nb-theme(select-outline-#{$status}-focus-border-color);
+ }
+ &:hover {
+ background-color: nb-theme(select-outline-#{$status}-hover-background-color);
+ border-color: nb-theme(select-outline-#{$status}-hover-border-color);
+ }
+
+ &[disabled] {
+ color: nb-theme(select-outline-#{$status}-disabled-text-color);
+ background-color: nb-theme(select-outline-#{$status}-disabled-background-color);
+ border-color: nb-theme(select-outline-#{$status}-disabled-border-color);
+
+ nb-icon {
+ color: nb-theme(select-outline-#{$status}-disabled-icon-color);
+ }
+ }
+
+ &.bottom,
+ &.top {
+ border-color: nb-theme(select-outline-#{$status}-open-border-color);
+ }
+
+ &.top {
+ border-top-color: nb-theme(select-outline-#{$status}-adjacent-border-color);
+ }
+ &.bottom {
+ border-bottom-color: nb-theme(select-outline-#{$status}-adjacent-border-color);
+ }
+ }
+ }
+
+ @each $size in nb-get-sizes() {
+ nb-select-with-autocomplete.appearance-outline.size-#{$size} .select-button {
+ padding: nb-theme(select-outline-#{$size}-padding);
+ @include nb-ltr(padding-right, nb-theme(select-icon-offset));
+ @include nb-rtl(padding-left, nb-theme(select-icon-offset));
+ }
+
+ @include form-field-theme.nb-form-field-with-prefix(
+ 'nb-select-with-autocomplete.appearance-outline.size-#{$size} .select-button',
+ $size
+ );
+ }
+}
diff --git a/src/framework/theme/components/select-with-autocomplete/_select-with-autocomplete.component.theme.scss b/src/framework/theme/components/select-with-autocomplete/_select-with-autocomplete.component.theme.scss
new file mode 100644
index 0000000000..168a73f764
--- /dev/null
+++ b/src/framework/theme/components/select-with-autocomplete/_select-with-autocomplete.component.theme.scss
@@ -0,0 +1,66 @@
+/*
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+@use '../../styles/theming' as *;
+@use '../form-field/form-field.component.theme' as form-field-theme;
+@use 'select-with-autocomplete-outline';
+@use 'select-with-autocomplete-filled';
+@use 'select-with-autocomplete-hero';
+
+@mixin nb-select-with-autocomplete-theme() {
+ nb-select-with-autocomplete .select-button {
+ min-width: nb-theme(select-min-width);
+ cursor: nb-theme(select-cursor);
+ font-family: nb-theme(select-text-font-family);
+
+ &.placeholder {
+ font-family: nb-theme(select-placeholder-text-font-family);
+ }
+ &:focus {
+ outline: none;
+ }
+ &[disabled] {
+ cursor: nb-theme(select-disabled-cursor);
+ }
+ }
+
+ @each $size in nb-get-sizes() {
+ nb-select-with-autocomplete.size-#{$size} {
+ .select-button {
+ font-size: nb-theme(select-#{$size}-text-font-size);
+ font-weight: nb-theme(select-#{$size}-text-font-weight);
+ line-height: nb-theme(select-#{$size}-text-line-height);
+
+ &.placeholder {
+ font-size: nb-theme(select-#{$size}-placeholder-text-font-size);
+ font-weight: nb-theme(select-#{$size}-placeholder-text-font-weight);
+ }
+
+ &.empty::before {
+ content: ' ';
+ display: block;
+ height: nb-theme(select-#{$size}-text-line-height);
+ }
+ }
+
+ &:not(.full-width) {
+ max-width: nb-theme(select-#{$size}-max-width);
+ }
+ }
+ }
+
+ @each $shape in nb-get-shapes() {
+ nb-select-with-autocomplete.shape-#{$shape} .select-button {
+ border-radius: nb-theme(select-#{$shape}-border-radius);
+ }
+ }
+
+ @include select-with-autocomplete-outline.select-with-autocomplete-outline();
+ @include select-with-autocomplete-filled.select-with-autocomplete-filled();
+ @include select-with-autocomplete-hero.select-with-autocomplete-hero();
+
+ @include form-field-theme.nb-form-field-root-component('nb-select-with-autocomplete');
+}
diff --git a/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.component.html b/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.component.html
new file mode 100644
index 0000000000..7dda17520e
--- /dev/null
+++ b/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.component.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.component.scss b/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.component.scss
new file mode 100644
index 0000000000..6f3327cf5f
--- /dev/null
+++ b/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.component.scss
@@ -0,0 +1,59 @@
+/*!
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+@use '../../styles/theming' as *;
+
+:host {
+ display: inline-block;
+ max-width: 100%;
+
+ .select-button {
+ @include nb-ltr(text-align, left) {
+ nb-icon {
+ right: 0.2em;
+ }
+ }
+ @include nb-rtl(text-align, right) {
+ nb-icon {
+ left: 0.2em;
+ }
+ }
+ }
+}
+
+:host(.full-width) {
+ width: 100%;
+}
+
+:host(.nb-transition) {
+ .select-button {
+ @include nb-component-animation(background-color, border-color, border-radius, box-shadow, color);
+ }
+}
+
+.select-button,
+nb-form-field {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-transform: none;
+ white-space: nowrap;
+}
+
+nb-icon:not([nbSuffix]) {
+ font-size: 1.5em;
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ @include nb-ltr(right, 0.5rem);
+ @include nb-rtl(left, 0.5rem);
+ @include nb-component-animation(transform);
+}
+
+:host(.open) nb-icon:not([nbSuffix]) {
+ transform: translateY(-50%) rotate(180deg);
+}
diff --git a/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.component.ts b/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.component.ts
new file mode 100644
index 0000000000..0468276e2c
--- /dev/null
+++ b/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.component.ts
@@ -0,0 +1,1040 @@
+/*
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import {
+ AfterContentInit,
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ComponentRef,
+ ContentChild,
+ ContentChildren,
+ ElementRef,
+ EventEmitter,
+ forwardRef,
+ HostBinding,
+ Inject,
+ Input,
+ OnDestroy,
+ Output,
+ QueryList,
+ ViewChild,
+ SimpleChanges,
+ OnChanges,
+ Renderer2,
+ NgZone,
+} from '@angular/core';
+import { NgClass } from '@angular/common';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { ListKeyManager } from '@angular/cdk/a11y';
+import { merge, Subject, BehaviorSubject, from, combineLatest, animationFrameScheduler, EMPTY } from 'rxjs';
+import { startWith, switchMap, takeUntil, filter, map, finalize, take, observeOn } from 'rxjs/operators';
+
+import { NbStatusService } from '../../services/status.service';
+import {
+ NbAdjustableConnectedPositionStrategy,
+ NbAdjustment,
+ NbPosition,
+ NbPositionBuilderService,
+} from '../cdk/overlay/overlay-position';
+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, NbFocusKeyManagerFactoryService } from '../cdk/a11y/focus-key-manager';
+import { ENTER, ESCAPE } from '../cdk/keycodes/keycodes';
+import { NbComponentSize } from '../component-size';
+import { NbComponentShape } from '../component-shape';
+import { NbComponentOrCustomStatus } from '../component-status';
+import { NB_DOCUMENT } from '../../theme.options';
+import { NbOptionComponent } from '../option/option.component';
+import { convertToBoolProperty, NbBooleanInput } from '../helpers';
+import { NB_SELECT_INJECTION_TOKEN } from '../select/select-injection-tokens';
+import { NbFormFieldControl, NbFormFieldControlConfig } from '../form-field/form-field-control';
+import { NbFocusMonitor } from '../cdk/a11y/a11y.module';
+import { NbScrollStrategies } from '../cdk/adapter/block-scroll-strategy-adapter';
+import {
+ NbActiveDescendantKeyManager,
+ NbActiveDescendantKeyManagerFactoryService,
+} from '../cdk/a11y/descendant-key-manager';
+import {
+ NbSelectAppearance,
+ NbSelectCompareFunction,
+ nbSelectFormFieldControlConfigFactory,
+ NbSelectLabelComponent,
+} from '../select/select.component';
+
+/**
+ * Experimental component with autocomplete possibility.
+ * Could be changed without any prior notice.
+ * Use at your own risk.
+ *
+ * Style variables is fully inherited.
+ * Component's public API (`@Input()` and `@Output()`) works in a same way as NbSelectComponent.
+ */
+@Component({
+ selector: 'nb-select-with-autocomplete',
+ templateUrl: './select-with-autocomplete.component.html',
+ styleUrls: ['./select-with-autocomplete.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => NbSelectWithAutocompleteComponent),
+ multi: true,
+ },
+ { provide: NB_SELECT_INJECTION_TOKEN, useExisting: NbSelectWithAutocompleteComponent },
+ { provide: NbFormFieldControl, useExisting: NbSelectWithAutocompleteComponent },
+ { provide: NbFormFieldControlConfig, useFactory: nbSelectFormFieldControlConfigFactory },
+ ],
+})
+export class NbSelectWithAutocompleteComponent
+ implements OnChanges, AfterViewInit, AfterContentInit, OnDestroy, ControlValueAccessor, NbFormFieldControl
+{
+ /**
+ * Select size, available sizes:
+ * `tiny`, `small`, `medium` (default), `large`, `giant`
+ */
+ @Input() size: NbComponentSize = 'medium';
+
+ /**
+ * Select status (adds specific styles):
+ * `basic`, `primary`, `info`, `success`, `warning`, `danger`, `control`
+ */
+ @Input() status: NbComponentOrCustomStatus = 'basic';
+
+ /**
+ * Select shapes: `rectangle` (default), `round`, `semi-round`
+ */
+ @Input() shape: NbComponentShape = 'rectangle';
+
+ /**
+ * Select appearances: `outline` (default), `filled`, `hero`
+ */
+ @Input() appearance: NbSelectAppearance = 'outline';
+
+ /**
+ * Specifies class to be set on `nb-option`s container (`nb-option-list`)
+ * */
+ @Input() optionsListClass: NgClass['ngClass'];
+
+ /**
+ * Specifies class for the overlay panel with options
+ * */
+ @Input() optionsPanelClass: string | string[];
+
+ /**
+ * Specifies width (in pixels) to be set on `nb-option`s container (`nb-option-list`)
+ * */
+ @Input()
+ get optionsWidth(): number {
+ return this._optionsWidth ?? this.hostWidth;
+ }
+ set optionsWidth(value: number) {
+ this._optionsWidth = value;
+ }
+ protected _optionsWidth: number | undefined;
+
+ /**
+ * Adds `outline` styles
+ */
+ @Input()
+ @HostBinding('class.appearance-outline')
+ get outline(): boolean {
+ return this.appearance === 'outline';
+ }
+ set outline(value: boolean) {
+ if (convertToBoolProperty(value)) {
+ this.appearance = 'outline';
+ }
+ }
+ static ngAcceptInputType_outline: NbBooleanInput;
+
+ /**
+ * Adds `filled` styles
+ */
+ @Input()
+ @HostBinding('class.appearance-filled')
+ get filled(): boolean {
+ return this.appearance === 'filled';
+ }
+ set filled(value: boolean) {
+ if (convertToBoolProperty(value)) {
+ this.appearance = 'filled';
+ }
+ }
+ static ngAcceptInputType_filled: NbBooleanInput;
+
+ /**
+ * Adds `hero` styles
+ */
+ @Input()
+ @HostBinding('class.appearance-hero')
+ get hero(): boolean {
+ return this.appearance === 'hero';
+ }
+ set hero(value: boolean) {
+ if (convertToBoolProperty(value)) {
+ this.appearance = 'hero';
+ }
+ }
+ static ngAcceptInputType_hero: NbBooleanInput;
+
+ /**
+ * Disables the select
+ */
+ @Input()
+ get disabled(): boolean {
+ return !!this._disabled;
+ }
+ set disabled(value: boolean) {
+ this._disabled = convertToBoolProperty(value);
+ }
+ protected _disabled: boolean;
+ static ngAcceptInputType_disabled: NbBooleanInput;
+
+ /**
+ * If set element will fill its container
+ */
+ @Input()
+ @HostBinding('class.full-width')
+ get fullWidth(): boolean {
+ return this._fullWidth;
+ }
+ set fullWidth(value: boolean) {
+ this._fullWidth = convertToBoolProperty(value);
+ }
+ protected _fullWidth: boolean = false;
+ static ngAcceptInputType_fullWidth: NbBooleanInput;
+
+ /**
+ * Renders select placeholder if nothing selected.
+ * */
+ @Input() placeholder: string = '';
+
+ /**
+ * A function to compare option value with selected value.
+ * By default, values are compared with strict equality (`===`).
+ */
+ @Input()
+ get compareWith(): NbSelectCompareFunction {
+ return this._compareWith;
+ }
+ set compareWith(fn: NbSelectCompareFunction) {
+ if (typeof fn !== 'function') {
+ return;
+ }
+
+ this._compareWith = fn;
+
+ if (this.selectionModel.length && this.canSelectValue()) {
+ this.setSelection(this.selected);
+ }
+ }
+ protected _compareWith: NbSelectCompareFunction = (v1: any, v2: any) => v1 === v2;
+
+ /**
+ * Accepts selected item or array of selected items.
+ * */
+ @Input()
+ set selected(value) {
+ this.writeValue(value);
+ }
+ get selected() {
+ return this.multiple ? this.selectionModel.map((o) => o.value) : this.selectionModel[0].value;
+ }
+
+ /**
+ * Gives capability just write `multiple` over the element.
+ * */
+ @Input()
+ get multiple(): boolean {
+ return this._multiple;
+ }
+ set multiple(value: boolean) {
+ this._multiple = convertToBoolProperty(value);
+ }
+ protected _multiple: boolean = false;
+ static ngAcceptInputType_multiple: NbBooleanInput;
+
+ /**
+ * Determines options overlay offset (in pixels).
+ **/
+ @Input() optionsOverlayOffset = 8;
+
+ /**
+ * Determines options overlay scroll strategy.
+ **/
+ @Input() scrollStrategy: NbScrollStrategies = 'block';
+
+ /**
+ * Experimental input.
+ * Could be changed without any prior notice.
+ * Use at your own risk.
+ *
+ * It replaces the button with input when the select is opened.
+ * That replacement provides a very basic API to implement options filtering functionality.
+ * Filtering itself isn't implemented inside select.
+ * So it should be implemented by the user.
+ */
+ @Input()
+ set withOptionsAutocomplete(value: boolean) {
+ this._withOptionsAutocomplete = value;
+ this.updatePositionStrategy();
+ this.updateCurrentKeyManager();
+
+ if (!value) {
+ this.resetAutocompleteInput();
+ }
+ }
+ get withOptionsAutocomplete(): boolean {
+ return this._withOptionsAutocomplete;
+ }
+ protected _withOptionsAutocomplete: boolean = false;
+
+ @HostBinding('class')
+ get additionalClasses(): string[] {
+ if (this.statusService.isCustomStatus(this.status)) {
+ return [this.statusService.getStatusClass(this.status)];
+ }
+ return [];
+ }
+
+ /**
+ * Will be emitted when selected value changes.
+ * */
+ @Output() selectedChange: EventEmitter = new EventEmitter();
+ @Output() selectOpen: EventEmitter = new EventEmitter();
+ @Output() selectClose: EventEmitter = new EventEmitter();
+ @Output() optionsAutocompleteInputChange: EventEmitter = new EventEmitter();
+
+ /**
+ * List of `NbOptionComponent`'s components passed as content.
+ * TODO maybe it would be better provide wrapper
+ * */
+ @ContentChildren(NbOptionComponent, { descendants: true }) options: QueryList;
+
+ /**
+ * Custom select label, will be rendered instead of default enumeration with coma.
+ * */
+ @ContentChild(NbSelectLabelComponent) customLabel;
+
+ /**
+ * NbCard with options content.
+ * */
+ @ViewChild(NbPortalDirective) portal: NbPortalDirective;
+
+ @ViewChild('selectButton', { read: ElementRef }) button: ElementRef | undefined;
+ @ViewChild('optionsAutocompleteInput', { read: ElementRef }) optionsAutocompleteInput:
+ | ElementRef
+ | undefined;
+
+ /**
+ * Determines is select opened.
+ * */
+ @HostBinding('class.open')
+ get isOpen(): boolean {
+ return this.ref && this.ref.hasAttached();
+ }
+
+ get isOptionsAutocompleteAllowed(): boolean {
+ return this.withOptionsAutocomplete && !this.multiple;
+ }
+
+ get isOptionsAutocompleteInputShown(): boolean {
+ return this.isOptionsAutocompleteAllowed && this.isOpen;
+ }
+
+ /**
+ * List of selected options.
+ * */
+ selectionModel: NbOptionComponent[] = [];
+
+ positionStrategy$: BehaviorSubject = new BehaviorSubject(
+ undefined,
+ );
+
+ /**
+ * Current overlay position because of we have to toggle overlayPosition
+ * in [ngClass] direction and this directive can use only string.
+ */
+ overlayPosition: NbPosition = '' as NbPosition;
+
+ protected ref: NbOverlayRef;
+
+ protected triggerStrategy: NbTriggerStrategy;
+
+ protected alive: boolean = true;
+
+ protected destroy$ = new Subject();
+
+ protected currentKeyManager: ListKeyManager;
+ protected focusKeyManager: NbFocusKeyManager;
+ protected activeDescendantKeyManager: NbActiveDescendantKeyManager;
+
+ /**
+ * If a user assigns value before content nb-options's rendered the value will be putted in this variable.
+ * And then applied after content rendered.
+ * Only the last value will be applied.
+ * */
+ protected queue;
+
+ /**
+ * Function passed through control value accessor to propagate changes.
+ * */
+ protected onChange: Function = () => {};
+ protected onTouched: Function = () => {};
+
+ /*
+ * @docs-private
+ **/
+ status$ = new BehaviorSubject(this.status);
+
+ /*
+ * @docs-private
+ **/
+ size$ = new BehaviorSubject(this.size);
+
+ /*
+ * @docs-private
+ **/
+ focused$ = new BehaviorSubject(false);
+
+ /*
+ * @docs-private
+ **/
+ disabled$ = new BehaviorSubject(this.disabled);
+
+ /*
+ * @docs-private
+ **/
+ fullWidth$ = new BehaviorSubject(this.fullWidth);
+
+ constructor(
+ @Inject(NB_DOCUMENT) protected document,
+ protected overlay: NbOverlayService,
+ protected hostRef: ElementRef,
+ protected positionBuilder: NbPositionBuilderService,
+ protected triggerStrategyBuilder: NbTriggerStrategyBuilderService,
+ protected cd: ChangeDetectorRef,
+ protected focusKeyManagerFactoryService: NbFocusKeyManagerFactoryService,
+ protected focusMonitor: NbFocusMonitor,
+ protected renderer: Renderer2,
+ protected zone: NgZone,
+ protected statusService: NbStatusService,
+ protected activeDescendantKeyManagerFactoryService: NbActiveDescendantKeyManagerFactoryService,
+ ) {}
+
+ /**
+ * Determines is select hidden.
+ * */
+ get isHidden(): boolean {
+ return !this.isOpen;
+ }
+
+ /**
+ * Returns width of the select button.
+ * */
+ get hostWidth(): number {
+ if (this.isOptionsAutocompleteInputShown) {
+ return this.optionsAutocompleteInput.nativeElement.getBoundingClientRect().width;
+ }
+ return this.button.nativeElement.getBoundingClientRect().width;
+ }
+
+ get selectButtonClasses(): string[] {
+ const classes = [];
+
+ if (!this.selectionModel.length) {
+ classes.push('placeholder');
+ }
+ if (!this.selectionModel.length && !this.placeholder) {
+ classes.push('empty');
+ }
+ if (this.isOpen) {
+ classes.push(this.overlayPosition);
+ }
+
+ return classes;
+ }
+
+ /**
+ * Content rendered in the label.
+ * */
+ get selectionView() {
+ if (this.selectionModel.length > 1) {
+ return this.selectionModel.map((option: NbOptionComponent) => option.content).join(', ');
+ }
+
+ return this.selectionModel[0]?.content ?? '';
+ }
+
+ ngOnChanges({ disabled, status, size, fullWidth }: SimpleChanges) {
+ if (disabled) {
+ this.disabled$.next(disabled.currentValue);
+ }
+ if (status) {
+ this.status$.next(status.currentValue);
+ }
+ if (size) {
+ this.size$.next(size.currentValue);
+ }
+ if (fullWidth) {
+ this.fullWidth$.next(this.fullWidth);
+ }
+ }
+
+ ngAfterContentInit() {
+ this.options.changes
+ .pipe(
+ startWith(this.options),
+ filter(() => this.queue != null && this.canSelectValue()),
+ // Call 'writeValue' when current change detection run is finished.
+ // When writing is finished, change detection starts again, since
+ // microtasks queue is empty.
+ // Prevents ExpressionChangedAfterItHasBeenCheckedError.
+ switchMap((options: QueryList) => from(Promise.resolve(options))),
+ takeUntil(this.destroy$),
+ )
+ .subscribe(() => this.writeValue(this.queue));
+ }
+
+ ngAfterViewInit() {
+ this.triggerStrategy = this.createTriggerStrategy();
+
+ this.subscribeOnButtonFocus();
+ this.subscribeOnTriggers();
+ this.subscribeOnOptionClick();
+
+ // TODO: #2254
+ this.zone.runOutsideAngular(() =>
+ setTimeout(() => {
+ this.renderer.addClass(this.hostRef.nativeElement, 'nb-transition');
+ }),
+ );
+ }
+
+ ngOnDestroy() {
+ this.alive = false;
+
+ this.destroy$.next();
+ this.destroy$.complete();
+
+ if (this.ref) {
+ this.ref.dispose();
+ }
+ if (this.triggerStrategy) {
+ this.triggerStrategy.destroy();
+ }
+ }
+
+ onAutocompleteInputChange(event: Event) {
+ this.optionsAutocompleteInputChange.emit((event.target as HTMLInputElement).value);
+ }
+
+ show() {
+ if (this.shouldShow()) {
+ this.attachToOverlay();
+
+ this.positionStrategy$
+ .pipe(
+ switchMap((positionStrategy) => positionStrategy.positionChange ?? EMPTY),
+ take(1),
+ takeUntil(this.destroy$),
+ )
+ .subscribe(() => {
+ if (this.isOptionsAutocompleteInputShown) {
+ this.optionsAutocompleteInput.nativeElement.focus();
+ }
+ this.setActiveOption();
+ });
+
+ this.selectOpen.emit();
+
+ this.cd.markForCheck();
+ }
+ }
+
+ hide() {
+ if (this.isOpen) {
+ this.ref.detach();
+ this.cd.markForCheck();
+ this.selectClose.emit();
+
+ this.resetAutocompleteInput();
+ }
+ }
+
+ registerOnChange(fn: any): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ this.cd.markForCheck();
+ }
+
+ writeValue(value): void {
+ if (!this.alive) {
+ return;
+ }
+
+ if (this.canSelectValue()) {
+ this.setSelection(value);
+ if (this.selectionModel.length) {
+ this.queue = null;
+ }
+ } else {
+ this.queue = value;
+ }
+ }
+
+ /**
+ * Selects option or clear all selected options if value is null.
+ * */
+ protected handleOptionClick(option: NbOptionComponent) {
+ this.queue = null;
+ if (option.value == null) {
+ this.reset();
+ } else {
+ this.selectOption(option);
+ }
+
+ this.cd.markForCheck();
+ }
+
+ /**
+ * Deselect all selected options.
+ * */
+ protected reset() {
+ this.selectionModel.forEach((option: NbOptionComponent) => option.deselect());
+ this.selectionModel = [];
+ this.hide();
+ this.focusButton();
+ this.emitSelected(this.multiple ? [] : null);
+ }
+
+ /**
+ * Determines how to select option as multiple or single.
+ * */
+ protected selectOption(option: NbOptionComponent) {
+ if (this.multiple) {
+ this.handleMultipleSelect(option);
+ } else {
+ this.handleSingleSelect(option);
+ }
+ }
+
+ /**
+ * Select single option.
+ * */
+ protected handleSingleSelect(option: NbOptionComponent) {
+ const selected = this.selectionModel.pop();
+
+ if (selected && !this._compareWith(selected.value, option.value)) {
+ selected.deselect();
+ }
+
+ this.selectionModel = [option];
+ option.select();
+ this.hide();
+ this.focusButton();
+ this.emitSelected(option.value);
+ }
+
+ /**
+ * Select for multiple options.
+ * */
+ protected handleMultipleSelect(option: NbOptionComponent) {
+ if (option.selected) {
+ this.selectionModel = this.selectionModel.filter((s) => !this._compareWith(s.value, option.value));
+ option.deselect();
+ } else {
+ this.selectionModel.push(option);
+ option.select();
+ }
+
+ this.emitSelected(this.selectionModel.map((opt: NbOptionComponent) => opt.value));
+ }
+
+ protected attachToOverlay() {
+ if (!this.ref) {
+ this.createOverlay();
+ this.subscribeOnPositionChange();
+ this.createKeyManager();
+ this.subscribeOnOverlayKeys();
+ this.subscribeOnOptionsAutocompleteChange();
+ }
+
+ this.ref.attach(this.portal);
+ }
+
+ protected setActiveOption() {
+ if (this.selectionModel.length && !this.selectionModel[0].hidden) {
+ this.currentKeyManager?.setActiveItem(this.selectionModel[0]);
+ } else {
+ this.currentKeyManager?.setFirstItemActive();
+ }
+ }
+
+ protected createOverlay() {
+ const scrollStrategy = this.createScrollStrategy();
+ this.positionStrategy$.next(this.createPositionStrategy());
+ this.ref = this.overlay.create({
+ positionStrategy: this.positionStrategy$.value,
+ scrollStrategy,
+ panelClass: this.optionsPanelClass,
+ });
+ }
+
+ protected createKeyManager(): void {
+ this.activeDescendantKeyManager = this.activeDescendantKeyManagerFactoryService
+ .create(this.options)
+ .skipPredicate((option) => {
+ return this.isOptionHidden(option);
+ });
+
+ this.focusKeyManager = this.focusKeyManagerFactoryService
+ .create(this.options)
+ .withTypeAhead(200)
+ .skipPredicate((option) => {
+ return this.isOptionHidden(option);
+ });
+
+ this.updateCurrentKeyManager();
+ }
+
+ protected updateCurrentKeyManager() {
+ this.currentKeyManager?.setActiveItem(-1);
+ if (this.isOptionsAutocompleteAllowed) {
+ this.currentKeyManager = this.activeDescendantKeyManager;
+ } else {
+ this.currentKeyManager = this.focusKeyManager;
+ }
+ this.setActiveOption();
+ }
+
+ protected resetAutocompleteInput() {
+ this.optionsAutocompleteInput.nativeElement.value = this.selectionView;
+ this.optionsAutocompleteInputChange.emit('');
+ }
+
+ protected createPositionStrategy(): NbAdjustableConnectedPositionStrategy {
+ const element: ElementRef = this.isOptionsAutocompleteAllowed
+ ? this.optionsAutocompleteInput
+ : this.button;
+ return this.positionBuilder
+ .connectedTo(element)
+ .position(NbPosition.BOTTOM)
+ .offset(this.optionsOverlayOffset)
+ .adjustment(NbAdjustment.VERTICAL);
+ }
+
+ protected updatePositionStrategy(): void {
+ if (this.ref) {
+ this.positionStrategy$.next(this.createPositionStrategy());
+ this.ref.updatePositionStrategy(this.positionStrategy$.value);
+ if (this.isOpen) {
+ this.ref.updatePosition();
+ }
+ }
+ }
+
+ protected createScrollStrategy(): NbScrollStrategy {
+ return this.overlay.scrollStrategies[this.scrollStrategy]();
+ }
+
+ protected createTriggerStrategy(): NbTriggerStrategy {
+ return this.triggerStrategyBuilder
+ .trigger(NbTrigger.CLICK)
+ .host(this.hostRef.nativeElement)
+ .container(() => this.getContainer())
+ .build();
+ }
+
+ protected subscribeOnTriggers() {
+ this.triggerStrategy.show$.subscribe(() => this.show());
+ this.triggerStrategy.hide$.pipe(filter(() => this.isOpen)).subscribe(($event: Event) => {
+ this.hide();
+ if (!this.isClickedWithinComponent($event)) {
+ this.onTouched();
+ }
+ });
+ }
+
+ protected subscribeOnPositionChange() {
+ this.positionStrategy$
+ .pipe(
+ switchMap((positionStrategy) => positionStrategy.positionChange ?? EMPTY),
+ takeUntil(this.destroy$),
+ )
+ .subscribe((position: NbPosition) => {
+ this.overlayPosition = position;
+ this.cd.detectChanges();
+ });
+ }
+
+ protected subscribeOnOptionClick() {
+ /**
+ * If the user changes provided options list in the runtime we have to handle this
+ * and resubscribe on options selection changes event.
+ * Otherwise, the user will not be able to select new options.
+ * */
+ this.options.changes
+ .pipe(
+ startWith(this.options),
+ switchMap((options: QueryList) => {
+ return merge(...options.map((option) => option.click));
+ }),
+ takeUntil(this.destroy$),
+ )
+ .subscribe((clickedOption: NbOptionComponent) => this.handleOptionClick(clickedOption));
+ }
+
+ protected subscribeOnOverlayKeys(): void {
+ this.ref
+ .keydownEvents()
+ .pipe(
+ filter(() => this.isOpen),
+ takeUntil(this.destroy$),
+ )
+ .subscribe((event: KeyboardEvent) => {
+ if (event.keyCode === ESCAPE) {
+ this.hide();
+ this.focusButton();
+ } else if (event.keyCode === ENTER && this.isOptionsAutocompleteInputShown) {
+ event.preventDefault();
+ const activeItem = this.currentKeyManager.activeItem;
+ if (activeItem) {
+ this.selectOption(activeItem);
+ }
+ } else {
+ this.currentKeyManager.onKeydown(event);
+ }
+ });
+
+ merge(
+ this.focusKeyManager.tabOut.pipe(filter(() => !this.isOptionsAutocompleteInputShown)),
+ this.activeDescendantKeyManager.tabOut.pipe(filter(() => this.isOptionsAutocompleteInputShown)),
+ )
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(() => {
+ this.hide();
+ this.onTouched();
+ });
+ }
+
+ protected subscribeOnOptionsAutocompleteChange() {
+ this.optionsAutocompleteInputChange
+ .pipe(
+ observeOn(animationFrameScheduler),
+ filter(() => this.isOptionsAutocompleteInputShown),
+ takeUntil(this.destroy$),
+ )
+ .subscribe(() => {
+ if (this.isOptionHidden(this.currentKeyManager.activeItem)) {
+ this.currentKeyManager.setFirstItemActive();
+ }
+ });
+ }
+
+ protected subscribeOnButtonFocus() {
+ const buttonFocus$ = this.focusMonitor.monitor(this.button).pipe(
+ map((origin) => !!origin),
+ startWith(false),
+ finalize(() => this.focusMonitor.stopMonitoring(this.button)),
+ );
+
+ const filterInputFocus$ = this.focusMonitor.monitor(this.optionsAutocompleteInput).pipe(
+ map((origin) => !!origin),
+ startWith(false),
+ finalize(() => this.focusMonitor.stopMonitoring(this.button)),
+ );
+
+ combineLatest([buttonFocus$, filterInputFocus$])
+ .pipe(
+ map(([buttonFocus, filterInputFocus]) => buttonFocus || filterInputFocus),
+ takeUntil(this.destroy$),
+ )
+ .subscribe(this.focused$);
+ }
+
+ protected getContainer() {
+ return (
+ this.ref &&
+ this.ref.hasAttached() &&
+ >{
+ location: {
+ nativeElement: this.ref.overlayElement,
+ },
+ }
+ );
+ }
+
+ protected focusButton() {
+ /**
+ * Need to wrap with setTimeout
+ * because otherwise focus could be called
+ * when the component hasn't rerendered the button
+ * which was hidden by `isOptionsAutocompleteInputShown` property.
+ */
+ setTimeout(() => {
+ this.button?.nativeElement?.focus();
+ });
+ }
+
+ /**
+ * Propagate selected value.
+ * */
+ protected emitSelected(selected) {
+ this.onChange(selected);
+ this.selectedChange.emit(selected);
+ }
+
+ /**
+ * Set selected value in model.
+ * */
+ protected setSelection(value) {
+ const isResetValue = value == null;
+ let safeValue = value;
+
+ if (this.multiple) {
+ safeValue = value ?? [];
+ }
+
+ const isArray: boolean = Array.isArray(safeValue);
+
+ if (this.multiple && !isArray && !isResetValue) {
+ throw new Error("Can't assign single value if select is marked as multiple");
+ }
+ if (!this.multiple && isArray) {
+ throw new Error("Can't assign array if select is not marked as multiple");
+ }
+
+ const previouslySelectedOptions = this.selectionModel;
+ this.selectionModel = [];
+
+ if (this.multiple) {
+ safeValue.forEach((option) => this.selectValue(option));
+ } else {
+ this.selectValue(safeValue);
+ }
+
+ // find options which were selected before and trigger deselect
+ previouslySelectedOptions
+ .filter((option: NbOptionComponent) => !this.selectionModel.includes(option))
+ .forEach((option: NbOptionComponent) => option.deselect());
+
+ this.cd.markForCheck();
+ }
+
+ /**
+ * Selects value.
+ * */
+ protected selectValue(value) {
+ if (value == null) {
+ return;
+ }
+
+ const corresponding = this.options.find((option: NbOptionComponent) => this._compareWith(option.value, value));
+
+ if (corresponding) {
+ corresponding.select();
+ this.selectionModel.push(corresponding);
+ }
+ }
+
+ protected shouldShow(): boolean {
+ return this.isHidden && this.options?.length > 0;
+ }
+
+ /**
+ * 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);
+ }
+
+ protected canSelectValue(): boolean {
+ return !!(this.options && this.options.length);
+ }
+
+ protected isOptionHidden(option: NbOptionComponent): boolean {
+ return option.hidden;
+ }
+
+ @HostBinding('class.size-tiny')
+ get tiny(): boolean {
+ return this.size === 'tiny';
+ }
+ @HostBinding('class.size-small')
+ get small(): boolean {
+ return this.size === 'small';
+ }
+ @HostBinding('class.size-medium')
+ get medium(): boolean {
+ return this.size === 'medium';
+ }
+ @HostBinding('class.size-large')
+ get large(): boolean {
+ return this.size === 'large';
+ }
+ @HostBinding('class.size-giant')
+ get giant(): boolean {
+ return this.size === 'giant';
+ }
+ @HostBinding('class.status-primary')
+ get primary(): boolean {
+ return this.status === 'primary';
+ }
+ @HostBinding('class.status-info')
+ get info(): boolean {
+ return this.status === 'info';
+ }
+ @HostBinding('class.status-success')
+ get success(): boolean {
+ return this.status === 'success';
+ }
+ @HostBinding('class.status-warning')
+ get warning(): boolean {
+ return this.status === 'warning';
+ }
+ @HostBinding('class.status-danger')
+ get danger(): boolean {
+ return this.status === 'danger';
+ }
+ @HostBinding('class.status-basic')
+ get basic(): boolean {
+ return this.status === 'basic';
+ }
+ @HostBinding('class.status-control')
+ get control(): boolean {
+ return this.status === 'control';
+ }
+ @HostBinding('class.shape-rectangle')
+ get rectangle(): boolean {
+ return this.shape === 'rectangle';
+ }
+ @HostBinding('class.shape-round')
+ get round(): boolean {
+ return this.shape === 'round';
+ }
+ @HostBinding('class.shape-semi-round')
+ get semiRound(): boolean {
+ return this.shape === 'semi-round';
+ }
+}
diff --git a/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.module.ts b/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.module.ts
new file mode 100644
index 0000000000..f984932b28
--- /dev/null
+++ b/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.module.ts
@@ -0,0 +1,31 @@
+import { NgModule } from '@angular/core';
+
+import { NbOverlayModule } from '../cdk/overlay/overlay.module';
+import { NbSharedModule } from '../shared/shared.module';
+import { NbInputModule } from '../input/input.module';
+import { NbCardModule } from '../card/card.module';
+import { NbButtonModule } from '../button/button.module';
+import { NbSelectWithAutocompleteComponent } from './select-with-autocomplete.component';
+import { NbOptionModule } from '../option/option-list.module';
+import { NbSelectModule } from '../select/select.module';
+import { NbIconModule } from '../icon/icon.module';
+import { NbFormFieldModule } from '../form-field/form-field.module';
+
+const NB_SELECT_COMPONENTS = [NbSelectWithAutocompleteComponent];
+
+@NgModule({
+ imports: [
+ NbSharedModule,
+ NbOverlayModule,
+ NbButtonModule,
+ NbInputModule,
+ NbCardModule,
+ NbIconModule,
+ NbOptionModule,
+ NbFormFieldModule,
+ NbSelectModule,
+ ],
+ exports: [...NB_SELECT_COMPONENTS, NbOptionModule, NbSelectModule],
+ declarations: [...NB_SELECT_COMPONENTS],
+})
+export class NbSelectWithAutocompleteModule {}
diff --git a/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.spec.ts b/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.spec.ts
new file mode 100644
index 0000000000..33996c0f06
--- /dev/null
+++ b/src/framework/theme/components/select-with-autocomplete/select-with-autocomplete.spec.ts
@@ -0,0 +1,1354 @@
+/*
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import { Component, ElementRef, EventEmitter, Input, Output, QueryList, ViewChild, ViewChildren } from '@angular/core';
+import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing';
+import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { RouterTestingModule } from '@angular/router/testing';
+import { from, zip, Subject } from 'rxjs';
+import createSpy = jasmine.createSpy;
+
+import {
+ NbSelectWithAutocompleteModule as NbSelectModule,
+ NbThemeModule,
+ NbOverlayContainerAdapter,
+ NB_DOCUMENT,
+ NbSelectWithAutocompleteComponent as NbSelectComponent,
+ NbLayoutModule,
+ NbOptionComponent,
+ NbOptionGroupComponent,
+ NbTriggerStrategyBuilderService,
+ NbFocusKeyManagerFactoryService,
+} from '@nebular/theme';
+import { NbActiveDescendantKeyManagerFactoryService } from '../cdk/a11y/descendant-key-manager';
+
+const eventMock = { preventDefault() {} } as Event;
+
+const TEST_GROUPS = [
+ {
+ title: 'Group 1',
+ options: [
+ { title: 'Option 1', value: 'Option 1' },
+ { title: 'Option 2', value: 'Option 2' },
+ { title: 'Option 3', value: 'Option 3' },
+ ],
+ },
+ {
+ title: 'Group 2',
+ options: [
+ { title: 'Option 21', value: 'Option 21' },
+ { title: 'Option 22', value: 'Option 22' },
+ { title: 'Option 23', value: 'Option 23' },
+ ],
+ },
+ {
+ title: 'Group 3',
+ options: [
+ { title: 'Option 31', value: 'Option 31' },
+ { title: 'Option 32', value: 'Option 32' },
+ { title: 'Option 33', value: 'Option 33' },
+ ],
+ },
+ {
+ title: 'Group 4',
+ options: [
+ { title: 'Option 41', value: '' },
+ { title: 'Option 42', value: '0' },
+ { title: 'Option 43', value: 0 },
+ { title: 'Option 44' },
+ ],
+ },
+];
+
+@Component({
+ selector: 'nb-select-test',
+ template: `
+
+
+
+
+ {{ selected.split('').reverse().join('') }}
+
+ None
+
+ {{ option.title }}
+
+
+
+
+ `,
+})
+export class NbSelectTestComponent {
+ @Input() selected: any = null;
+ @Input() multiple: boolean;
+ @Input() customLabel: boolean;
+ @Output() selectedChange: EventEmitter = new EventEmitter();
+ @ViewChildren(NbOptionComponent) options: QueryList>;
+ groups = TEST_GROUPS;
+ opened = false;
+}
+
+@Component({
+ template: `
+
+
+
+ a
+ b
+ c
+
+
+
+ `,
+})
+export class BasicSelectTestComponent {}
+
+@Component({
+ template: `
+
+
+
+ {{ option }}
+
+
+
+ `,
+})
+export class NbSelectWithOptionsObjectsComponent {
+ @Input() compareFn = (o1: any, o2: any) => JSON.stringify(o1) === JSON.stringify(o2);
+ @Input() selected = { id: 2 };
+ @Input() options = [{ id: 1 }, { id: 2 }, { id: 3 }];
+
+ @ViewChildren(NbOptionComponent) optionComponents: QueryList;
+}
+
+@Component({
+ template: `
+
+
+
+ {{ option }}
+
+
+
+ `,
+})
+export class NbSelectWithInitiallySelectedOptionComponent {
+ @Input() selected = 1;
+ @Input() options = [1, 2, 3];
+}
+
+@Component({
+ template: `
+
+
+
+ {{ option }}
+
+
+
+ `,
+})
+export class NbReactiveFormSelectComponent {
+ options: number[] = [1];
+ showSelect: boolean = true;
+ formControl: FormControl = new FormControl();
+
+ @ViewChild(NbSelectComponent) selectComponent: NbSelectComponent;
+ @ViewChildren(NbOptionComponent) optionComponents: QueryList>;
+}
+
+@Component({
+ template: `
+
+
+
+ {{ option }}
+
+
+
+ `,
+})
+export class NbNgModelSelectComponent {
+ options: number[] = [1];
+ selectedValue: number = null;
+
+ @ViewChild(NbOptionComponent) optionComponent: NbOptionComponent;
+}
+
+@Component({
+ template: `
+
+
+
+ No value option
+ undefined value
+ undefined value
+ false value
+ 0 value
+ empty string value
+ NaN value
+ truthy value
+
+
+
+ `,
+})
+export class NbSelectWithFalsyOptionValuesComponent {
+ nanValue = NaN;
+
+ @ViewChildren(NbOptionComponent) options: QueryList>;
+ @ViewChildren(NbOptionComponent, { read: ElementRef }) optionElements: QueryList>;
+
+ get noValueOption(): NbOptionComponent {
+ return this.options.toArray()[0];
+ }
+ get noValueOptionElement(): ElementRef {
+ return this.optionElements.toArray()[0];
+ }
+ get nullOption(): NbOptionComponent {
+ return this.options.toArray()[1];
+ }
+ get nullOptionElement(): ElementRef {
+ return this.optionElements.toArray()[1];
+ }
+ get undefinedOption(): NbOptionComponent {
+ return this.options.toArray()[2];
+ }
+ get undefinedOptionElement(): ElementRef {
+ return this.optionElements.toArray()[2];
+ }
+ get falseOption(): NbOptionComponent {
+ return this.options.toArray()[3];
+ }
+ get falseOptionElement(): ElementRef {
+ return this.optionElements.toArray()[3];
+ }
+ get zeroOption(): NbOptionComponent {
+ return this.options.toArray()[4];
+ }
+ get zeroOptionElement(): ElementRef {
+ return this.optionElements.toArray()[4];
+ }
+ get emptyStringOption(): NbOptionComponent {
+ return this.options.toArray()[5];
+ }
+ get emptyStringOptionElement(): ElementRef {
+ return this.optionElements.toArray()[5];
+ }
+ get nanOption(): NbOptionComponent {
+ return this.options.toArray()[6];
+ }
+ get nanOptionElement(): ElementRef {
+ return this.optionElements.toArray()[6];
+ }
+ get truthyOption(): NbOptionComponent {
+ return this.options.toArray()[7];
+ }
+ get truthyOptionElement(): ElementRef {
+ return this.optionElements.toArray()[7];
+ }
+}
+
+@Component({
+ template: `
+
+
+
+ No value option
+ undefined value
+ undefined value
+ false value
+ 0 value
+ empty string value
+ NaN value
+ truthy value
+
+
+
+ `,
+})
+export class NbMultipleSelectWithFalsyOptionValuesComponent extends NbSelectWithFalsyOptionValuesComponent {}
+
+@Component({
+ template: `
+
+
+
+
+ 1
+
+
+
+
+ `,
+})
+export class NbOptionDisabledTestComponent {
+ optionGroupDisabled = false;
+ optionDisabled = false;
+
+ @ViewChild(NbSelectComponent) selectComponent: NbSelectComponent;
+ @ViewChild(NbOptionGroupComponent) optionGroupComponent: NbOptionGroupComponent;
+ @ViewChild(NbOptionComponent) optionComponent: NbOptionComponent;
+}
+
+describe('Component: NbSelectComponent', () => {
+ let fixture: ComponentFixture;
+ let overlayContainerService: NbOverlayContainerAdapter;
+ let overlayContainer: HTMLElement;
+ let document: Document;
+ let select: NbSelectComponent;
+
+ const setSelectedAndOpen = (selected) => {
+ fixture.componentInstance.selected = selected;
+ fixture.detectChanges();
+ select.show();
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule.withRoutes([]),
+ FormsModule,
+ ReactiveFormsModule,
+ NbThemeModule.forRoot(),
+ NbLayoutModule,
+ NbSelectModule,
+ ],
+ declarations: [
+ NbSelectTestComponent,
+ NbSelectWithOptionsObjectsComponent,
+ NbSelectWithInitiallySelectedOptionComponent,
+ NbReactiveFormSelectComponent,
+ NbNgModelSelectComponent,
+ ],
+ });
+
+ fixture = TestBed.createComponent(NbSelectTestComponent);
+ overlayContainerService = TestBed.inject(NbOverlayContainerAdapter);
+ document = TestBed.inject(NB_DOCUMENT);
+ select = fixture.debugElement.query(By.directive(NbSelectComponent)).componentInstance;
+
+ overlayContainer = document.createElement('div');
+ overlayContainerService.setContainer(overlayContainer);
+
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ select.hide();
+ overlayContainerService.clearContainer();
+ });
+
+ it('should render passed item as selected', () => {
+ setSelectedAndOpen('Option 23');
+ const selected = overlayContainer.querySelector('nb-option.selected');
+
+ expect(selected).toBeTruthy();
+ expect(selected.textContent).toContain('Option 23');
+ });
+
+ it('should render passed items as selected', () => {
+ select.multiple = true;
+ setSelectedAndOpen(['Option 1', 'Option 21', 'Option 31']);
+ const selected = overlayContainer.querySelectorAll('nb-option.selected');
+
+ expect(selected.length).toBe(3);
+ expect(selected[0].textContent).toContain('Option 1');
+ expect(selected[1].textContent).toContain('Option 21');
+ expect(selected[2].textContent).toContain('Option 31');
+ });
+
+ it('should fire selectedChange item when selection changes', (done) => {
+ setSelectedAndOpen('Option 1');
+
+ fixture.componentInstance.selectedChange.subscribe((selection) => {
+ expect(selection).toBe('Option 21');
+ done();
+ });
+
+ const option = overlayContainer.querySelectorAll('nb-option')[4];
+ option.dispatchEvent(new Event('click'));
+ });
+
+ it('should fire selectedChange items when selecting multiple one by one', (done) => {
+ select.multiple = true;
+ setSelectedAndOpen([]);
+
+ zip(
+ from([['Option 2'], ['Option 2', 'Option 21'], ['Option 2', 'Option 21', 'Option 23']]),
+ fixture.componentInstance.selectedChange,
+ ).subscribe(([expected, real]) => expect(real).toEqual(expected), null, done);
+
+ const option1 = overlayContainer.querySelectorAll('nb-option')[2];
+ const option2 = overlayContainer.querySelectorAll('nb-option')[4];
+ const option3 = overlayContainer.querySelectorAll('nb-option')[6];
+ option1.dispatchEvent(new Event('click'));
+ option2.dispatchEvent(new Event('click'));
+ option3.dispatchEvent(new Event('click'));
+ });
+
+ it('should deselect item when clicking on reselect item', () => {
+ setSelectedAndOpen('Option 1');
+
+ const option = overlayContainer.querySelector('nb-option');
+ option.dispatchEvent(new Event('click'));
+
+ expect(overlayContainer.querySelectorAll('nb-option.selected').length).toBe(0);
+ });
+
+ it('should deselect all items when clicking on reset item in multiple select', () => {
+ select.multiple = true;
+ setSelectedAndOpen(['Option 1', 'Option 2']);
+
+ const option = overlayContainer.querySelector('nb-option');
+ option.dispatchEvent(new Event('click'));
+
+ expect(overlayContainer.querySelectorAll('nb-option.selected').length).toBe(0);
+ });
+
+ it('should emit selectionChange with empty array when reset option selected in multiple select', () => {
+ select.multiple = true;
+ setSelectedAndOpen(['Option 1', 'Option 2']);
+
+ const selectionChangeSpy = createSpy('selectionChangeSpy');
+ select.selectedChange.subscribe(selectionChangeSpy);
+
+ const option = overlayContainer.querySelector('nb-option');
+ option.dispatchEvent(new Event('click'));
+
+ expect(selectionChangeSpy).toHaveBeenCalledWith([]);
+ });
+
+ it('should emit selectionChange with null when reset option selected in single select', () => {
+ setSelectedAndOpen('Option 1');
+
+ const selectionChangeSpy = createSpy('selectionChangeSpy');
+ select.selectedChange.subscribe(selectionChangeSpy);
+
+ const option = overlayContainer.querySelector('nb-option');
+ option.dispatchEvent(new Event('click'));
+
+ expect(selectionChangeSpy).toHaveBeenCalledWith(null);
+ });
+
+ it('should deselect only clicked item in multiple select', () => {
+ select.multiple = true;
+ setSelectedAndOpen(['Option 1', 'Option 2']);
+
+ const option = overlayContainer.querySelectorAll('nb-option')[1];
+ option.dispatchEvent(new Event('click'));
+
+ fixture.detectChanges();
+
+ const selected = overlayContainer.querySelectorAll('nb-option.selected');
+ expect(selected.length).toBe(1);
+ expect(selected[0].textContent).toContain('Option 2');
+ });
+
+ it('should render placeholder when nothing selected', () => {
+ select.multiple = true;
+ setSelectedAndOpen([]);
+
+ const button = fixture.nativeElement.querySelector('button');
+ expect(button.textContent).toContain('This is test select component');
+ });
+
+ it('should render default label when something selected', () => {
+ setSelectedAndOpen('Option 1');
+
+ const button = fixture.nativeElement.querySelector('button');
+ expect(button.textContent).toContain('Option 1');
+ });
+
+ it('should render custom label when something selected and custom label provided', fakeAsync(() => {
+ fixture.componentInstance.customLabel = true;
+ fixture.componentInstance.selected = 'Option 1';
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+ const button = fixture.nativeElement.querySelector('button');
+ expect(button.textContent).toContain('1 noitpO');
+ }));
+
+ it('should select initially specified value without errors', fakeAsync(() => {
+ const selectFixture = TestBed.createComponent(NbSelectWithInitiallySelectedOptionComponent);
+ selectFixture.detectChanges();
+ flush();
+ selectFixture.detectChanges();
+
+ const selectedOption = selectFixture.debugElement
+ .query(By.directive(NbSelectComponent))
+ .componentInstance.options.find((o) => o.selected);
+
+ expect(selectedOption.value).toEqual(selectFixture.componentInstance.selected);
+ const selectButton = selectFixture.nativeElement.querySelector('nb-select-with-autocomplete button') as HTMLElement;
+ expect(selectButton.textContent).toEqual(selectedOption.value.toString());
+ }));
+
+ it('should use compareWith function to compare values', fakeAsync(() => {
+ const selectFixture = TestBed.createComponent(NbSelectWithOptionsObjectsComponent);
+ const testComponent = selectFixture.componentInstance;
+ selectFixture.detectChanges();
+ flush();
+ selectFixture.detectChanges();
+
+ const selectedOption = testComponent.optionComponents.find((o) => o.selected);
+ expect(selectedOption.value).toEqual({ id: 2 });
+ }));
+
+ it('should ignore selection change if destroyed', fakeAsync(() => {
+ const selectFixture = TestBed.createComponent(NbReactiveFormSelectComponent);
+ const testSelectComponent = selectFixture.componentInstance;
+ selectFixture.detectChanges();
+ flush();
+
+ const setSelectionSpy = spyOn(testSelectComponent.selectComponent as any, 'setSelection').and.callThrough();
+ testSelectComponent.showSelect = false;
+ selectFixture.detectChanges();
+
+ expect(() => testSelectComponent.formControl.setValue(1)).not.toThrow();
+ expect(setSelectionSpy).not.toHaveBeenCalled();
+ }));
+
+ it('should select option set through formControl binding', fakeAsync(() => {
+ const selectFixture = TestBed.createComponent(NbReactiveFormSelectComponent);
+ const testComponent = selectFixture.componentInstance;
+ selectFixture.detectChanges();
+ flush();
+
+ const optionSelectSpy = spyOn(testComponent.optionComponents.first, 'select').and.callThrough();
+
+ expect(testComponent.optionComponents.first.selected).toEqual(false);
+
+ testComponent.formControl.setValue(1);
+ selectFixture.detectChanges();
+
+ expect(testComponent.optionComponents.first.selected).toEqual(true);
+ expect(optionSelectSpy).toHaveBeenCalledTimes(1);
+ }));
+
+ it('should select option set through select "selected" binding', fakeAsync(() => {
+ const selectFixture = TestBed.createComponent(NbSelectTestComponent);
+ const testComponent = selectFixture.componentInstance;
+ selectFixture.detectChanges();
+ flush();
+
+ const optionToSelect = testComponent.options.find((o) => o.value != null);
+ const optionSelectSpy = spyOn(optionToSelect, 'select').and.callThrough();
+
+ expect(optionToSelect.selected).toEqual(false);
+
+ testComponent.selected = optionToSelect.value;
+ selectFixture.detectChanges();
+
+ expect(optionToSelect.selected).toEqual(true);
+ expect(optionSelectSpy).toHaveBeenCalledTimes(1);
+ }));
+
+ it('should select option set through ngModel binding', fakeAsync(() => {
+ const selectFixture = TestBed.createComponent(NbNgModelSelectComponent);
+ const testComponent = selectFixture.componentInstance;
+ selectFixture.detectChanges();
+
+ const optionToSelect = testComponent.optionComponent;
+ const optionSelectSpy = spyOn(optionToSelect, 'select').and.callThrough();
+
+ expect(optionToSelect.selected).toEqual(false);
+
+ testComponent.selectedValue = optionToSelect.value;
+ selectFixture.detectChanges();
+ // need to call flush because NgModelDirective updates value on
+ // resolvedPromise.then
+ flush();
+ selectFixture.detectChanges();
+
+ expect(optionToSelect.selected).toEqual(true);
+ expect(optionSelectSpy).toHaveBeenCalledTimes(1);
+ }));
+
+ it('should unselect previously selected option', fakeAsync(() => {
+ const selectFixture = TestBed.createComponent(NbSelectTestComponent);
+ const testSelectComponent = selectFixture.componentInstance;
+ testSelectComponent.selected = TEST_GROUPS[0].options[0].value;
+ selectFixture.detectChanges();
+ flush();
+ selectFixture.detectChanges();
+
+ const selectedOption: NbOptionComponent = testSelectComponent.options.find((o) => o.selected);
+ const selectionChangeSpy = createSpy('selectionChangeSpy');
+ selectedOption.selectionChange.subscribe(selectionChangeSpy);
+
+ testSelectComponent.selected = TEST_GROUPS[0].options[1].value;
+ selectFixture.detectChanges();
+
+ expect(selectionChangeSpy).toHaveBeenCalledTimes(1);
+ expect(selectedOption.selected).toEqual(false);
+ }));
+
+ it('should not deselect option if option stays selected', fakeAsync(() => {
+ const selectFixture = TestBed.createComponent(NbSelectTestComponent);
+ const testSelectComponent = selectFixture.componentInstance;
+ testSelectComponent.selected = TEST_GROUPS[0].options[0].value;
+ selectFixture.detectChanges();
+ flush();
+ selectFixture.detectChanges();
+
+ const selectedOption: NbOptionComponent = testSelectComponent.options.find((o) => o.selected);
+ const selectionChangeSpy = spyOn(selectedOption, 'deselect');
+
+ testSelectComponent.selected = selectedOption.value;
+ selectFixture.detectChanges();
+
+ expect(selectionChangeSpy).not.toHaveBeenCalled();
+ }));
+
+ it(`should not call dispose on uninitialized resources`, () => {
+ const selectFixture = new NbSelectComponent(null, null, null, null, null, null, null, null, null, null, null, null);
+ expect(() => selectFixture.ngOnDestroy()).not.toThrow();
+ });
+
+ it(`should has 'empty' class when has no placeholder and text`, () => {
+ const selectFixture = TestBed.createComponent(NbSelectComponent);
+ selectFixture.detectChanges();
+ const button = selectFixture.debugElement.query(By.css('button'));
+
+ expect(button.classes.empty).toEqual(true);
+ });
+
+ it(`should set overlay width same as button inside select`, () => {
+ const selectFixture = TestBed.createComponent(NbSelectComponent);
+ const selectComponent = selectFixture.componentInstance;
+ selectFixture.detectChanges();
+
+ const selectElement: HTMLElement = selectFixture.nativeElement;
+ const buttonElement: HTMLElement = selectElement.querySelector('button');
+
+ selectElement.style.padding = '1px';
+
+ expect(selectComponent.hostWidth).not.toEqual(selectElement.offsetWidth);
+ expect(selectComponent.hostWidth).toEqual(buttonElement.offsetWidth);
+ });
+
+ it('should not open when disabled and button clicked', fakeAsync(() => {
+ const selectFixture = TestBed.createComponent(NbSelectComponent);
+ selectFixture.componentInstance.disabled = true;
+ selectFixture.detectChanges();
+ const selectButton: HTMLElement = selectFixture.debugElement.query(By.css('button')).nativeElement;
+
+ selectButton.click();
+ flush();
+ fixture.detectChanges();
+
+ expect(selectFixture.componentInstance.isOpen).toBeFalsy();
+ }));
+
+ it('should not open when disabled and toggle icon clicked', fakeAsync(() => {
+ const selectFixture = TestBed.createComponent(NbSelectComponent);
+ selectFixture.componentInstance.disabled = true;
+ selectFixture.detectChanges();
+ const selectToggleIcon: HTMLElement = selectFixture.debugElement.query(By.css('nb-icon')).nativeElement;
+
+ selectToggleIcon.click();
+ flush();
+ fixture.detectChanges();
+
+ 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(NbSelectTestComponent);
+ select = selectFixture.debugElement.query(By.directive(NbSelectComponent)).componentInstance;
+ selectFixture.detectChanges();
+ flush();
+
+ select.registerOnTouched(touchedSpy);
+ select.show();
+ selectFixture.debugElement.query(By.css('.select-button')).triggerEventHandler('blur', {});
+ expect(touchedSpy).not.toHaveBeenCalled();
+ }));
+
+ it('should emit open event after opening and close event after closing', fakeAsync(() => {
+ const selectFixture = TestBed.createComponent(NbSelectTestComponent);
+ select = selectFixture.debugElement.query(By.directive(NbSelectComponent)).componentInstance;
+
+ selectFixture.detectChanges();
+ expect(selectFixture.componentInstance.opened).toBe(false);
+ select.show();
+ selectFixture.detectChanges();
+ flush();
+ expect(selectFixture.componentInstance.opened).toBe(true);
+ select.hide();
+ selectFixture.detectChanges();
+ flush();
+ expect(selectFixture.componentInstance.opened).toBe(false);
+ }));
+});
+
+describe('NbSelectComponent - falsy values', () => {
+ let fixture: ComponentFixture;
+ let testComponent: NbSelectWithFalsyOptionValuesComponent;
+ let select: NbSelectComponent;
+
+ beforeEach(fakeAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [RouterTestingModule.withRoutes([]), NbThemeModule.forRoot(), NbLayoutModule, NbSelectModule],
+ declarations: [NbSelectWithFalsyOptionValuesComponent, NbMultipleSelectWithFalsyOptionValuesComponent],
+ });
+
+ fixture = TestBed.createComponent(NbSelectWithFalsyOptionValuesComponent);
+ testComponent = fixture.componentInstance;
+ select = fixture.debugElement.query(By.directive(NbSelectComponent)).componentInstance;
+
+ fixture.detectChanges();
+ flush();
+ }));
+
+ it('should clean selection when selected option does not have a value', fakeAsync(() => {
+ select.selected = testComponent.truthyOption.value;
+ fixture.detectChanges();
+
+ testComponent.noValueOption.onClick(eventMock);
+ fixture.detectChanges();
+
+ expect(select.selectionModel.length).toEqual(0);
+ }));
+
+ it('should clean selection when selected option has null value', fakeAsync(() => {
+ select.selected = testComponent.truthyOption.value;
+ fixture.detectChanges();
+
+ testComponent.nullOption.onClick(eventMock);
+ fixture.detectChanges();
+
+ expect(select.selectionModel.length).toEqual(0);
+ }));
+
+ it('should clean selection when selected option has undefined value', fakeAsync(() => {
+ select.selected = testComponent.truthyOption.value;
+ fixture.detectChanges();
+
+ testComponent.undefinedOption.onClick(eventMock);
+ fixture.detectChanges();
+
+ expect(select.selectionModel.length).toEqual(0);
+ }));
+
+ it('should not reset selection when selected option has false value', fakeAsync(() => {
+ select.selected = testComponent.truthyOption.value;
+ fixture.detectChanges();
+
+ testComponent.falseOption.onClick(eventMock);
+ fixture.detectChanges();
+
+ expect(select.selectionModel.length).toEqual(1);
+ }));
+
+ it('should not reset selection when selected option has zero value', fakeAsync(() => {
+ select.selected = testComponent.truthyOption.value;
+ fixture.detectChanges();
+
+ testComponent.zeroOption.onClick(eventMock);
+ fixture.detectChanges();
+
+ expect(select.selectionModel.length).toEqual(1);
+ }));
+
+ it('should not reset selection when selected option has empty string value', fakeAsync(() => {
+ select.selected = testComponent.truthyOption.value;
+ fixture.detectChanges();
+
+ testComponent.emptyStringOption.onClick(eventMock);
+ fixture.detectChanges();
+
+ expect(select.selectionModel.length).toEqual(1);
+ }));
+
+ it('should not reset selection when selected option has NaN value', fakeAsync(() => {
+ select.selected = testComponent.truthyOption.value;
+ fixture.detectChanges();
+
+ testComponent.nanOption.onClick(eventMock);
+ fixture.detectChanges();
+
+ expect(select.selectionModel.length).toEqual(1);
+ }));
+
+ it('should set class if fullWidth input set to true', () => {
+ select.fullWidth = true;
+ fixture.detectChanges();
+
+ const button = fixture.debugElement.query(By.directive(NbSelectComponent));
+ expect(button.classes['full-width']).toEqual(true);
+ });
+
+ describe('multiple', () => {
+ beforeEach(fakeAsync(() => {
+ fixture = TestBed.createComponent(NbMultipleSelectWithFalsyOptionValuesComponent);
+ testComponent = fixture.componentInstance;
+ select = fixture.debugElement.query(By.directive(NbSelectComponent)).componentInstance;
+
+ fixture.detectChanges();
+ flush();
+ select.show();
+ fixture.detectChanges();
+ }));
+
+ it('should not render checkbox on options with reset values', () => {
+ expect(testComponent.noValueOptionElement.nativeElement.querySelector('nb-checkbox')).toEqual(null);
+ expect(testComponent.nullOptionElement.nativeElement.querySelector('nb-checkbox')).toEqual(null);
+ expect(testComponent.undefinedOptionElement.nativeElement.querySelector('nb-checkbox')).toEqual(null);
+ });
+
+ it('should render checkbox on options with falsy non-reset values', () => {
+ expect(testComponent.falseOptionElement.nativeElement.querySelector('nb-checkbox')).not.toEqual(null);
+ expect(testComponent.zeroOptionElement.nativeElement.querySelector('nb-checkbox')).not.toEqual(null);
+ expect(testComponent.emptyStringOptionElement.nativeElement.querySelector('nb-checkbox')).not.toEqual(null);
+ expect(testComponent.nanOptionElement.nativeElement.querySelector('nb-checkbox')).not.toEqual(null);
+ expect(testComponent.truthyOptionElement.nativeElement.querySelector('nb-checkbox')).not.toEqual(null);
+ });
+ });
+
+ it('should select initial falsy value', fakeAsync(() => {
+ fixture = TestBed.createComponent(NbSelectWithFalsyOptionValuesComponent);
+ testComponent = fixture.componentInstance;
+ select = fixture.debugElement.query(By.directive(NbSelectComponent)).componentInstance;
+
+ select.selected = '';
+ fixture.detectChanges();
+ flush();
+
+ expect(select.selectionModel[0]).toEqual(testComponent.emptyStringOption);
+ expect(testComponent.emptyStringOption.selected).toEqual(true);
+ }));
+});
+
+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;
+ },
+ build() {
+ return { show$: showTriggerStub, hide$: hideTriggerStub, destroy() {} };
+ },
+ };
+
+ 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() {},
+ skipPredicate() {
+ return this;
+ },
+ tabOut: tabOutStub,
+ };
+ keyManagerFactoryStub = {
+ create() {
+ return keyManagerStub;
+ },
+ };
+
+ TestBed.configureTestingModule({
+ imports: [RouterTestingModule.withRoutes([]), NbThemeModule.forRoot(), NbLayoutModule, NbSelectModule],
+ declarations: [BasicSelectTestComponent],
+ });
+ TestBed.overrideProvider(NbFocusKeyManagerFactoryService, { useValue: keyManagerFactoryStub });
+ TestBed.overrideProvider(NbActiveDescendantKeyManagerFactoryService, { 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;
+ let option: NbOptionComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule.withRoutes([]),
+ FormsModule,
+ ReactiveFormsModule,
+ NbThemeModule.forRoot(),
+ NbLayoutModule,
+ NbSelectModule,
+ ],
+ declarations: [NbNgModelSelectComponent, NbSelectTestComponent, NbReactiveFormSelectComponent],
+ });
+
+ fixture = TestBed.createComponent(NbReactiveFormSelectComponent);
+ testSelectComponent = fixture.componentInstance;
+ fixture.detectChanges();
+ option = testSelectComponent.optionComponents.first;
+ });
+
+ it('should ignore selection change if destroyed', fakeAsync(() => {
+ const selectionChangeSpy = createSpy('selectionChangeSpy');
+ option.selectionChange.subscribe(selectionChangeSpy);
+
+ expect(option.selected).toEqual(false);
+ testSelectComponent.showSelect = false;
+ fixture.detectChanges();
+
+ expect((option as any).alive).toEqual(false);
+
+ option.select();
+
+ expect(option.selected).toEqual(false);
+ expect(selectionChangeSpy).not.toHaveBeenCalled();
+ }));
+
+ it('should not emit selection change when already selected', fakeAsync(() => {
+ option.select();
+ fixture.detectChanges();
+ expect(option.selected).toEqual(true);
+
+ const selectionChangeSpy = createSpy('selectionChangeSpy');
+ option.selectionChange.subscribe(selectionChangeSpy);
+ option.select();
+
+ expect(option.selected).toEqual(true);
+ expect(selectionChangeSpy).not.toHaveBeenCalled();
+ }));
+
+ it('should emit selection change when deselected', fakeAsync(() => {
+ option.select();
+ fixture.detectChanges();
+ expect(option.selected).toEqual(true);
+
+ const selectionChangeSpy = createSpy('selectionChangeSpy');
+ option.selectionChange.subscribe(selectionChangeSpy);
+ option.deselect();
+
+ expect(option.selected).toEqual(false);
+ expect(selectionChangeSpy).toHaveBeenCalledTimes(1);
+ }));
+});
+
+describe('NbOptionComponent disabled', () => {
+ let fixture: ComponentFixture;
+ let testComponent: NbOptionDisabledTestComponent;
+ let selectComponent: NbSelectComponent;
+ let optionGroupComponent: NbOptionGroupComponent;
+ let optionComponent: NbOptionComponent;
+
+ beforeEach(fakeAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [RouterTestingModule.withRoutes([]), NbThemeModule.forRoot(), NbLayoutModule, NbSelectModule],
+ declarations: [NbOptionDisabledTestComponent],
+ });
+
+ fixture = TestBed.createComponent(NbOptionDisabledTestComponent);
+ testComponent = fixture.componentInstance;
+ fixture.detectChanges();
+ flush();
+
+ selectComponent = testComponent.selectComponent;
+ optionGroupComponent = testComponent.optionGroupComponent;
+ optionComponent = testComponent.optionComponent;
+ }));
+
+ it('should has disabled attribute if disabled set to true', () => {
+ selectComponent.show();
+ testComponent.optionDisabled = true;
+ fixture.detectChanges();
+
+ const option = fixture.debugElement.query(By.directive(NbOptionComponent));
+ expect(option.attributes.disabled).toEqual('');
+ });
+
+ it('should has disabled attribute if group disabled set to true', fakeAsync(() => {
+ selectComponent.show();
+ testComponent.optionGroupDisabled = true;
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ const option = fixture.debugElement.query(By.directive(NbOptionComponent));
+ expect(option.attributes.disabled).toEqual('');
+ }));
+});
+
+describe('NbSelect - dynamic options', () => {
+ let fixture: ComponentFixture;
+ let testComponent: NbReactiveFormSelectComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule.withRoutes([]),
+ FormsModule,
+ ReactiveFormsModule,
+ NbThemeModule.forRoot(),
+ NbLayoutModule,
+ NbSelectModule,
+ ],
+ declarations: [NbReactiveFormSelectComponent],
+ });
+
+ fixture = TestBed.createComponent(NbReactiveFormSelectComponent);
+ testComponent = fixture.componentInstance;
+ });
+
+ describe('Set value from queue', () => {
+ let selectComponent: NbSelectComponent;
+
+ beforeEach(() => {
+ // Force select to cache the value as there is no options to select.
+ testComponent.options = [];
+ testComponent.formControl = new FormControl(1);
+ fixture.detectChanges();
+
+ selectComponent = fixture.debugElement.query(By.directive(NbSelectComponent)).componentInstance;
+ });
+
+ it('should set value from queue when options added dynamically (after change detection run)', fakeAsync(() => {
+ expect(selectComponent.selectionModel.length).toEqual(0);
+
+ testComponent.options = [1];
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ expect(selectComponent.selectionModel[0]).toEqual(testComponent.optionComponents.first);
+ }));
+
+ it('should set value from queue when options change', fakeAsync(() => {
+ testComponent.options = [0];
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ expect(selectComponent.selectionModel.length).toEqual(0);
+
+ testComponent.options.push(1);
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ expect(selectComponent.selectionModel[0]).toEqual(testComponent.optionComponents.last);
+ }));
+ });
+
+ describe('Clear queue after value set', () => {
+ /*
+ We can ensure queue is clean by spying on `writeValue` calls on select. It will be called only if options
+ change and queue has a value.
+ */
+
+ it('should clear queue after option selected by click', fakeAsync(() => {
+ testComponent.options = [];
+ testComponent.formControl = new FormControl(1);
+ fixture.detectChanges();
+
+ const selectComponent: NbSelectComponent = fixture.debugElement.query(
+ By.directive(NbSelectComponent),
+ ).componentInstance;
+ testComponent.options = [0];
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ testComponent.optionComponents.first.onClick({ preventDefault() {} } as Event);
+ fixture.detectChanges();
+
+ const writeValueSpy = spyOn(selectComponent, 'writeValue').and.callThrough();
+ testComponent.options.push(1);
+ fixture.detectChanges();
+ flush();
+ expect(writeValueSpy).not.toHaveBeenCalled();
+ }));
+
+ it(`should clear queue after option selected via 'selected' input`, fakeAsync(() => {
+ testComponent.options = [];
+ testComponent.formControl = new FormControl(1);
+ fixture.detectChanges();
+
+ const selectComponent: NbSelectComponent = fixture.debugElement.query(
+ By.directive(NbSelectComponent),
+ ).componentInstance;
+ testComponent.options = [0];
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ selectComponent.selected = 0;
+ fixture.detectChanges();
+
+ const writeValueSpy = spyOn(selectComponent, 'writeValue').and.callThrough();
+ testComponent.options.push(1);
+ fixture.detectChanges();
+ flush();
+ expect(writeValueSpy).not.toHaveBeenCalled();
+ }));
+
+ it('should clear queue after options change and selection model change', fakeAsync(() => {
+ testComponent.options = [];
+ testComponent.formControl = new FormControl(1);
+ fixture.detectChanges();
+
+ const selectComponent: NbSelectComponent = fixture.debugElement.query(
+ By.directive(NbSelectComponent),
+ ).componentInstance;
+ const writeValueSpy = spyOn(selectComponent, 'writeValue').and.callThrough();
+
+ testComponent.options = [1];
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ expect(writeValueSpy).toHaveBeenCalledTimes(1);
+
+ testComponent.options.push(2);
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ expect(writeValueSpy).toHaveBeenCalledTimes(1);
+ }));
+
+ it('should not clear queue after options change and selection model is empty', fakeAsync(() => {
+ testComponent.options = [];
+ testComponent.formControl = new FormControl(2);
+ fixture.detectChanges();
+
+ const selectComponent: NbSelectComponent = fixture.debugElement.query(
+ By.directive(NbSelectComponent),
+ ).componentInstance;
+ const writeValueSpy = spyOn(selectComponent, 'writeValue').and.callThrough();
+
+ testComponent.options = [0];
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+ expect(writeValueSpy).toHaveBeenCalledTimes(1);
+
+ testComponent.options.push(1);
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+ expect(writeValueSpy).toHaveBeenCalledTimes(2);
+ }));
+ });
+});
+
+@Component({
+ template: `
+
+
+
+ {{ option }}
+
+
+
+ `,
+})
+export class NbSelectWithExperimentalSearchComponent {
+ options: number[] = [1, 2, 3, 4, 5];
+ selectedValue: number = null;
+ filterValue: string = '';
+ isOpened: boolean = false;
+ @ViewChild(NbSelectComponent) selectComponent: NbSelectComponent;
+}
+
+describe('NbSelect - experimental search', () => {
+ let fixture: ComponentFixture;
+ let testComponent: NbSelectWithExperimentalSearchComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule.withRoutes([]),
+ FormsModule,
+ NbThemeModule.forRoot(),
+ NbLayoutModule,
+ NbSelectModule,
+ ],
+ declarations: [NbSelectWithExperimentalSearchComponent],
+ });
+
+ fixture = TestBed.createComponent(NbSelectWithExperimentalSearchComponent);
+ testComponent = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it("should update search input and don't emit filterChange when value of select is changed", fakeAsync(() => {
+ const searchInput = testComponent.selectComponent.optionSearchInput.nativeElement;
+
+ expect(searchInput.value).toEqual('');
+ expect(testComponent.filterValue).toEqual('');
+
+ testComponent.selectedValue = 1;
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+ expect(searchInput.value).toEqual('1');
+ expect(testComponent.filterValue).toEqual('');
+
+ testComponent.selectedValue = 2;
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+ expect(searchInput.value).toEqual('2');
+ expect(testComponent.filterValue).toEqual('');
+ }));
+
+ 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('input')).triggerEventHandler('blur', {});
+ expect(touchedSpy).toHaveBeenCalledTimes(1);
+ }));
+
+ it('should make filter value empty and restore input to default after select is closed', fakeAsync(() => {
+ const searchInput = fixture.debugElement.query(By.css('input'));
+
+ testComponent.selectedValue = 1;
+
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ const initialValue = searchInput.nativeElement.value;
+
+ testComponent.selectComponent.show();
+ searchInput.triggerEventHandler('input', { target: { value: '123' } });
+
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ expect(testComponent.filterValue).toBe('123');
+
+ testComponent.selectComponent.hide();
+
+ fixture.detectChanges();
+ flush();
+ fixture.detectChanges();
+
+ expect(testComponent.filterValue).toBe('');
+ expect(searchInput.nativeElement.value).toBe(initialValue);
+ }));
+});
diff --git a/src/framework/theme/components/select/select.component.html b/src/framework/theme/components/select/select.component.html
index 61a4e1ad2a..400faf85c9 100644
--- a/src/framework/theme/components/select/select.component.html
+++ b/src/framework/theme/components/select/select.component.html
@@ -1,5 +1,4 @@
-
-
-
-
-
= new EventEmitter();
- @Output() selectOpen: EventEmitter = new EventEmitter();
- @Output() selectClose: EventEmitter = new EventEmitter();
- @Output() optionSearchChange: EventEmitter = new EventEmitter();
/**
* List of `NbOptionComponent`'s components passed as content.
@@ -741,8 +726,7 @@ export class NbSelectComponent
* */
@ViewChild(NbPortalDirective) portal: NbPortalDirective;
- @ViewChild('selectButton', { read: ElementRef }) button: ElementRef | undefined;
- @ViewChild('optionSearchInput', { read: ElementRef }) optionSearchInput: ElementRef | undefined;
+ @ViewChild('selectButton', { read: ElementRef }) button: ElementRef;
/**
* Determines is select opened.
@@ -752,10 +736,6 @@ export class NbSelectComponent
return this.ref && this.ref.hasAttached();
}
- get isOptionSearchInputAllowed(): boolean {
- return this.withOptionSearch && this.isOpen && !this.multiple;
- }
-
/**
* List of selected options.
* */
@@ -842,9 +822,6 @@ export class NbSelectComponent
* Returns width of the select button.
* */
get hostWidth(): number {
- if (this.isOptionSearchInputAllowed) {
- return this.optionSearchInput.nativeElement.getBoundingClientRect().width;
- }
return this.button.nativeElement.getBoundingClientRect().width;
}
@@ -872,7 +849,7 @@ export class NbSelectComponent
return this.selectionModel.map((option: NbOptionComponent) => option.content).join(', ');
}
- return this.selectionModel[0]?.content ?? '';
+ return this.selectionModel[0].content;
}
ngOnChanges({ disabled, status, size, fullWidth }: SimpleChanges) {
@@ -934,24 +911,14 @@ export class NbSelectComponent
}
}
- onInput(event: Event) {
- this.optionSearchChange.emit((event.target as HTMLInputElement).value);
- }
-
show() {
if (this.shouldShow()) {
this.attachToOverlay();
this.positionStrategy.positionChange.pipe(take(1), takeUntil(this.destroy$)).subscribe(() => {
- if (this.isOptionSearchInputAllowed) {
- this.optionSearchInput.nativeElement.focus();
- } else {
- this.setActiveOption();
- }
+ this.setActiveOption();
});
- this.selectOpen.emit();
-
this.cd.markForCheck();
}
}
@@ -960,10 +927,6 @@ export class NbSelectComponent
if (this.isOpen) {
this.ref.detach();
this.cd.markForCheck();
- this.selectClose.emit();
-
- this.optionSearchInput.nativeElement.value = this.selectionView;
- this.optionSearchChange.emit('');
}
}
@@ -1098,11 +1061,8 @@ export class NbSelectComponent
}
protected createPositionStrategy(): NbAdjustableConnectedPositionStrategy {
- const element: ElementRef = this.withOptionSearch
- ? this.optionSearchInput
- : this.button;
return this.positionBuilder
- .connectedTo(element)
+ .connectedTo(this.button)
.position(NbPosition.BOTTOM)
.offset(this.optionsOverlayOffset)
.adjustment(NbAdjustment.VERTICAL);
@@ -1163,9 +1123,9 @@ export class NbSelectComponent
)
.subscribe((event: KeyboardEvent) => {
if (event.keyCode === ESCAPE) {
- this.hide();
this.button.nativeElement.focus();
- } else if (!this.isOptionSearchInputAllowed) {
+ this.hide();
+ } else {
this.keyManager.onKeydown(event);
}
});
@@ -1177,21 +1137,11 @@ export class NbSelectComponent
}
protected subscribeOnButtonFocus() {
- const buttonFocus$ = this.focusMonitor.monitor(this.button).pipe(
- map((origin) => !!origin),
- startWith(false),
- finalize(() => this.focusMonitor.stopMonitoring(this.button)),
- );
-
- const filterInputFocus$ = this.focusMonitor.monitor(this.optionSearchInput).pipe(
- map((origin) => !!origin),
- startWith(false),
- finalize(() => this.focusMonitor.stopMonitoring(this.button)),
- );
-
- combineLatest([buttonFocus$, filterInputFocus$])
+ this.focusMonitor
+ .monitor(this.button)
.pipe(
- map(([buttonFocus, filterInputFocus]) => buttonFocus || filterInputFocus),
+ map((origin) => !!origin),
+ finalize(() => this.focusMonitor.stopMonitoring(this.button)),
takeUntil(this.destroy$),
)
.subscribe(this.focused$);
diff --git a/src/framework/theme/components/select/select.module.ts b/src/framework/theme/components/select/select.module.ts
index 47b39289ef..c0244f79ce 100644
--- a/src/framework/theme/components/select/select.module.ts
+++ b/src/framework/theme/components/select/select.module.ts
@@ -8,21 +8,11 @@ import { NbButtonModule } from '../button/button.module';
import { NbSelectComponent, NbSelectLabelComponent } from './select.component';
import { NbOptionModule } from '../option/option-list.module';
import { NbIconModule } from '../icon/icon.module';
-import { NbFormFieldModule } from '../form-field/form-field.module';
const NB_SELECT_COMPONENTS = [NbSelectComponent, NbSelectLabelComponent];
@NgModule({
- imports: [
- NbSharedModule,
- NbOverlayModule,
- NbButtonModule,
- NbInputModule,
- NbCardModule,
- NbIconModule,
- NbOptionModule,
- NbFormFieldModule,
- ],
+ imports: [NbSharedModule, NbOverlayModule, NbButtonModule, NbInputModule, NbCardModule, NbIconModule, NbOptionModule],
exports: [...NB_SELECT_COMPONENTS, NbOptionModule],
declarations: [...NB_SELECT_COMPONENTS],
})
diff --git a/src/framework/theme/components/select/select.spec.ts b/src/framework/theme/components/select/select.spec.ts
index 89cd092bd9..e30c3e5e4f 100644
--- a/src/framework/theme/components/select/select.spec.ts
+++ b/src/framework/theme/components/select/select.spec.ts
@@ -73,8 +73,6 @@ const TEST_GROUPS = [
[multiple]="multiple"
[selected]="selected"
(selectedChange)="selectedChange.emit($event)"
- (selectOpen)="opened = true"
- (selectClose)="opened = false"
>
{{ selected.split('').reverse().join('') }}
@@ -95,7 +93,6 @@ export class NbSelectTestComponent {
@Output() selectedChange: EventEmitter = new EventEmitter();
@ViewChildren(NbOptionComponent) options: QueryList>;
groups = TEST_GROUPS;
- opened = false;
}
@Component({
@@ -692,22 +689,6 @@ describe('Component: NbSelectComponent', () => {
selectFixture.debugElement.query(By.css('.select-button')).triggerEventHandler('blur', {});
expect(touchedSpy).not.toHaveBeenCalled();
}));
-
- it('should emit open event after opening and close event after closing', fakeAsync(() => {
- const selectFixture = TestBed.createComponent(NbSelectTestComponent);
- select = selectFixture.debugElement.query(By.directive(NbSelectComponent)).componentInstance;
-
- selectFixture.detectChanges();
- expect(selectFixture.componentInstance.opened).toBe(false);
- select.show();
- selectFixture.detectChanges();
- flush();
- expect(selectFixture.componentInstance.opened).toBe(true);
- select.hide();
- selectFixture.detectChanges();
- flush();
- expect(selectFixture.componentInstance.opened).toBe(false);
- }));
});
describe('NbSelectComponent - falsy values', () => {
@@ -1235,115 +1216,3 @@ describe('NbSelect - dynamic options', () => {
}));
});
});
-
-@Component({
- template: `
-
-
-
- {{ option }}
-
-
-
- `,
-})
-export class NbSelectWithExperimentalSearchComponent {
- options: number[] = [1, 2, 3, 4, 5];
- selectedValue: number = null;
- filterValue: string = '';
- isOpened: boolean = false;
- @ViewChild(NbSelectComponent) selectComponent: NbSelectComponent;
-}
-
-describe('NbSelect - experimental search', () => {
- let fixture: ComponentFixture;
- let testComponent: NbSelectWithExperimentalSearchComponent;
-
- beforeEach(() => {
- TestBed.configureTestingModule({
- imports: [
- RouterTestingModule.withRoutes([]),
- FormsModule,
- NbThemeModule.forRoot(),
- NbLayoutModule,
- NbSelectModule,
- ],
- declarations: [NbSelectWithExperimentalSearchComponent],
- });
-
- fixture = TestBed.createComponent(NbSelectWithExperimentalSearchComponent);
- testComponent = fixture.componentInstance;
-
- fixture.detectChanges();
- });
-
- it("should update search input and don't emit filterChange when value of select is changed", fakeAsync(() => {
- const searchInput = testComponent.selectComponent.optionSearchInput.nativeElement;
-
- expect(searchInput.value).toEqual('');
- expect(testComponent.filterValue).toEqual('');
-
- testComponent.selectedValue = 1;
- fixture.detectChanges();
- flush();
- fixture.detectChanges();
- expect(searchInput.value).toEqual('1');
- expect(testComponent.filterValue).toEqual('');
-
- testComponent.selectedValue = 2;
- fixture.detectChanges();
- flush();
- fixture.detectChanges();
- expect(searchInput.value).toEqual('2');
- expect(testComponent.filterValue).toEqual('');
- }));
-
- 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('input')).triggerEventHandler('blur', {});
- expect(touchedSpy).toHaveBeenCalledTimes(1);
- }));
-
- it('should make filter value empty and restore input to default after select is closed', fakeAsync(() => {
- const searchInput = fixture.debugElement.query(By.css('input'));
-
- testComponent.selectedValue = 1;
-
- fixture.detectChanges();
- flush();
- fixture.detectChanges();
-
- const initialValue = searchInput.nativeElement.value;
-
- testComponent.selectComponent.show();
- searchInput.triggerEventHandler('input', { target: { value: '123' } });
-
- fixture.detectChanges();
- flush();
- fixture.detectChanges();
-
- expect(testComponent.filterValue).toBe('123');
-
- testComponent.selectComponent.hide();
-
- fixture.detectChanges();
- flush();
- fixture.detectChanges();
-
- expect(testComponent.filterValue).toBe('');
- expect(searchInput.nativeElement.value).toBe(initialValue);
- }));
-});
diff --git a/src/framework/theme/public_api.ts b/src/framework/theme/public_api.ts
index e7d02dc0e4..bcc56f9a55 100644
--- a/src/framework/theme/public_api.ts
+++ b/src/framework/theme/public_api.ts
@@ -181,7 +181,9 @@ export * from './components/tooltip/tooltip.module';
export * from './components/tooltip/tooltip.directive';
export * from './components/tooltip/tooltip.component';
export * from './components/select/select.module';
+export * from './components/select-with-autocomplete/select-with-autocomplete.module';
export * from './components/select/select.component';
+export * from './components/select-with-autocomplete/select-with-autocomplete.component';
export * from './components/option/option-list.module';
export * from './components/option/option.component';
export * from './components/option/option-group.component';
diff --git a/src/framework/theme/styles/global/_components.scss b/src/framework/theme/styles/global/_components.scss
index 8937e1eb52..407200bc9b 100644
--- a/src/framework/theme/styles/global/_components.scss
+++ b/src/framework/theme/styles/global/_components.scss
@@ -36,6 +36,7 @@
@forward '../../components/popover/popover.component.theme';
@forward '../../components/context-menu/context-menu.component.theme';
@forward '../../components/select/select.component.theme';
+@forward '../../components/select-with-autocomplete/select-with-autocomplete.component.theme';
@forward '../../components/option/option-list.component.theme';
@forward '../../components/toastr/toast.component.theme';
@forward '../../components/tooltip/tooltip.component.theme';
@@ -77,6 +78,8 @@
@use '../../components/popover/popover.component.theme' as popover-theme;
@use '../../components/context-menu/context-menu.component.theme' as context-menu-theme;
@use '../../components/select/select.component.theme' as select-theme;
+@use '../../components/select-with-autocomplete/select-with-autocomplete.component.theme' as
+ select-with-autocomplete-theme;
@use '../../components/option/option-list.component.theme' as option-list-theme;
@use '../../components/toastr/toast.component.theme' as toast-theme;
@use '../../components/tooltip/tooltip.component.theme' as tooltip-theme;
@@ -120,6 +123,7 @@
@include popover-theme.nb-popover-theme();
@include context-menu-theme.nb-context-menu-theme();
@include select-theme.nb-select-theme();
+ @include select-with-autocomplete-theme.nb-select-with-autocomplete-theme();
@include option-list-theme.nb-option-list-theme();
@include toast-theme.nb-toast-theme();
@include tooltip-theme.nb-tooltip-theme();
diff --git a/src/playground/with-layout/select/select-search-showcase.component.html b/src/playground/with-layout/select/select-autocomplete-showcase.component.html
similarity index 50%
rename from src/playground/with-layout/select/select-search-showcase.component.html
rename to src/playground/with-layout/select/select-autocomplete-showcase.component.html
index c023421de4..1ff4f77818 100644
--- a/src/playground/with-layout/select/select-search-showcase.component.html
+++ b/src/playground/with-layout/select/select-autocomplete-showcase.component.html
@@ -1,10 +1,11 @@
- Toggle autocomplete: {{ withAutocomplete }}
+
Option empty
Option 0
@@ -12,14 +13,27 @@
Option 2
Option 3
Option 4
-
-
+
+
+ Option empty
+ Option 0
+ Option 1
+ Option 2
+ Option 3
+ Option 4
+
+
Option empty
Option 0
Option 1
Option 2
Option 3
Option 4
-
+
diff --git a/src/playground/with-layout/select/select-search-showcase.component.ts b/src/playground/with-layout/select/select-autocomplete-showcase.component.ts
similarity index 78%
rename from src/playground/with-layout/select/select-search-showcase.component.ts
rename to src/playground/with-layout/select/select-autocomplete-showcase.component.ts
index 429f8686b5..e4532752a9 100644
--- a/src/playground/with-layout/select/select-search-showcase.component.ts
+++ b/src/playground/with-layout/select/select-autocomplete-showcase.component.ts
@@ -7,10 +7,11 @@
import { Component } from '@angular/core';
@Component({
- selector: 'npg-select-search-showcase',
- templateUrl: './select-search-showcase.component.html',
+ selector: 'npg-select-autocomplete-showcase',
+ templateUrl: './select-autocomplete-showcase.component.html',
})
-export class SelectSearchShowcaseComponent {
+export class SelectAutocompleteShowcaseComponent {
+ withAutocomplete = true;
selectedItem = '2';
filterValue = '';
diff --git a/src/playground/with-layout/select/select-routing.module.ts b/src/playground/with-layout/select/select-routing.module.ts
index da1b2b25ed..0641f11d9b 100644
--- a/src/playground/with-layout/select/select-routing.module.ts
+++ b/src/playground/with-layout/select/select-routing.module.ts
@@ -23,7 +23,7 @@ import { SelectInteractiveComponent } from './select-interactive.component';
import { SelectTestComponent } from './select-test.component';
import { SelectCompareWithComponent } from './select-compare-with.component';
import { SelectIconComponent } from './select-icon.component';
-import { SelectSearchShowcaseComponent } from './select-search-showcase.component';
+import { SelectAutocompleteShowcaseComponent } from './select-autocomplete-showcase.component';
const routes: Route[] = [
{
@@ -95,8 +95,8 @@ const routes: Route[] = [
component: SelectIconComponent,
},
{
- path: 'select-search-showcase.component',
- component: SelectSearchShowcaseComponent,
+ path: 'select-autocomplete-showcase.component',
+ component: SelectAutocompleteShowcaseComponent,
},
];
diff --git a/src/playground/with-layout/select/select.module.ts b/src/playground/with-layout/select/select.module.ts
index 1f1ba089df..d1e84b832c 100644
--- a/src/playground/with-layout/select/select.module.ts
+++ b/src/playground/with-layout/select/select.module.ts
@@ -12,9 +12,9 @@ import {
NbCardModule,
NbFormFieldModule,
NbIconModule,
- NbInputModule,
NbRadioModule,
NbSelectModule,
+ NbSelectWithAutocompleteModule,
} from '@nebular/theme';
import { SelectRoutingModule } from './select-routing.module';
import { SelectCleanComponent } from './select-clean.component';
@@ -34,7 +34,7 @@ import { SelectInteractiveComponent } from './select-interactive.component';
import { SelectTestComponent } from './select-test.component';
import { SelectCompareWithComponent } from './select-compare-with.component';
import { SelectIconComponent } from './select-icon.component';
-import { SelectSearchShowcaseComponent } from './select-search-showcase.component';
+import { SelectAutocompleteShowcaseComponent } from './select-autocomplete-showcase.component';
@NgModule({
declarations: [
@@ -55,12 +55,13 @@ import { SelectSearchShowcaseComponent } from './select-search-showcase.componen
SelectTestComponent,
SelectCompareWithComponent,
SelectIconComponent,
- SelectSearchShowcaseComponent,
+ SelectAutocompleteShowcaseComponent,
],
imports: [
FormsModule,
ReactiveFormsModule,
NbSelectModule,
+ NbSelectWithAutocompleteModule,
SelectRoutingModule,
NbCardModule,
CommonModule,
@@ -68,7 +69,6 @@ import { SelectSearchShowcaseComponent } from './select-search-showcase.componen
NbButtonModule,
NbIconModule,
NbFormFieldModule,
- NbInputModule,
],
})
export class SelectModule {}