Skip to content

Commit

Permalink
feat(select): options search (#3091)
Browse files Browse the repository at this point in the history
  • Loading branch information
sashaqred committed Aug 4, 2022
1 parent a546295 commit ff4f738
Show file tree
Hide file tree
Showing 12 changed files with 313 additions and 29 deletions.
6 changes: 6 additions & 0 deletions src/app/playground-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,12 @@ export const PLAYGROUND_COMPONENTS: ComponentLink[] = [
component: 'SelectIconComponent',
name: 'Select Icon',
},
{
path: 'select-search-showcase.component',
link: '/select/select-search-showcase.component',
component: 'SelectSearchShowcaseComponent',
name: 'Select Search Showcase',
},
],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
:host {
display: flex;
align-items: center;
&[hidden] {
display: none;
}
}

.nb-form-control-container {
Expand Down
3 changes: 3 additions & 0 deletions src/framework/theme/components/option/option.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

:host {
display: flex;
&[hidden] {
display: none;
}

&:hover {
cursor: pointer;
Expand Down
20 changes: 20 additions & 0 deletions src/framework/theme/components/select/select.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<button
[hidden]="isOptionSearchInputAllowed"
[disabled]="disabled"
[ngClass]="selectButtonClasses"
(blur)="trySetTouched()"
Expand Down Expand Up @@ -29,6 +30,25 @@
</nb-icon>
</button>

<nb-form-field [hidden]="!isOptionSearchInputAllowed">
<input
nbInput
fullWidth
#optionSearchInput
[value]="selectionView"
[placeholder]="placeholder"
[status]="status"
[shape]="shape"
[fieldSize]="size"
(blur)="trySetTouched()"
(keydown.arrowDown)="show()"
(keydown.arrowUp)="show()"
(click)="$event.stopPropagation()"
(input)="onInput($event)"
/>
<nb-icon nbSuffix icon="chevron-up-outline" pack="nebular-essentials" aria-hidden="true"> </nb-icon>
</nb-form-field>

<nb-option-list
*nbPortal
[size]="size"
Expand Down
7 changes: 4 additions & 3 deletions src/framework/theme/components/select/select.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
}
}

.select-button {
.select-button,
nb-form-field {
position: relative;
width: 100%;
overflow: hidden;
Expand All @@ -43,7 +44,7 @@
white-space: nowrap;
}

nb-icon {
nb-icon:not([nbSuffix]) {
font-size: 1.5em;
position: absolute;
top: 50%;
Expand All @@ -53,6 +54,6 @@ nb-icon {
@include nb-component-animation(transform);
}

:host(.open) nb-icon {
:host(.open) nb-icon:not([nbSuffix]) {
transform: translateY(-50%) rotate(180deg);
}
72 changes: 61 additions & 11 deletions src/framework/theme/components/select/select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
} from '@angular/core';
import { NgClass } from '@angular/common';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { merge, Subject, BehaviorSubject, from } from 'rxjs';
import { merge, Subject, BehaviorSubject, from, combineLatest } from 'rxjs';
import { startWith, switchMap, takeUntil, filter, map, finalize, take } from 'rxjs/operators';

import { NbStatusService } from '../../services/status.service';
Expand Down Expand Up @@ -697,6 +697,18 @@ export class NbSelectComponent
**/
@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() withOptionSearch: boolean = false;

@HostBinding('class')
get additionalClasses(): string[] {
if (this.statusService.isCustomStatus(this.status)) {
Expand All @@ -709,6 +721,9 @@ export class NbSelectComponent
* Will be emitted when selected value changes.
* */
@Output() selectedChange: EventEmitter<any> = new EventEmitter();
@Output() selectOpen: EventEmitter<void> = new EventEmitter();
@Output() selectClose: EventEmitter<void> = new EventEmitter();
@Output() optionSearchChange: EventEmitter<string> = new EventEmitter();

/**
* List of `NbOptionComponent`'s components passed as content.
Expand All @@ -726,7 +741,8 @@ export class NbSelectComponent
* */
@ViewChild(NbPortalDirective) portal: NbPortalDirective;

@ViewChild('selectButton', { read: ElementRef }) button: ElementRef<HTMLButtonElement>;
@ViewChild('selectButton', { read: ElementRef }) button: ElementRef<HTMLButtonElement> | undefined;
@ViewChild('optionSearchInput', { read: ElementRef }) optionSearchInput: ElementRef<HTMLInputElement> | undefined;

/**
* Determines is select opened.
Expand All @@ -736,6 +752,10 @@ export class NbSelectComponent
return this.ref && this.ref.hasAttached();
}

get isOptionSearchInputAllowed(): boolean {
return this.withOptionSearch && this.isOpen && !this.multiple;
}

/**
* List of selected options.
* */
Expand Down Expand Up @@ -822,6 +842,9 @@ 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;
}

Expand Down Expand Up @@ -849,7 +872,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) {
Expand Down Expand Up @@ -911,14 +934,24 @@ 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(() => {
this.setActiveOption();
if (this.isOptionSearchInputAllowed) {
this.optionSearchInput.nativeElement.focus();
} else {
this.setActiveOption();
}
});

this.selectOpen.emit();

this.cd.markForCheck();
}
}
Expand All @@ -927,6 +960,10 @@ export class NbSelectComponent
if (this.isOpen) {
this.ref.detach();
this.cd.markForCheck();
this.selectClose.emit();

this.optionSearchInput.nativeElement.value = this.selectionView;
this.optionSearchChange.emit('');
}
}

Expand Down Expand Up @@ -1061,8 +1098,11 @@ export class NbSelectComponent
}

protected createPositionStrategy(): NbAdjustableConnectedPositionStrategy {
const element: ElementRef<HTMLInputElement | HTMLButtonElement> = this.withOptionSearch
? this.optionSearchInput
: this.button;
return this.positionBuilder
.connectedTo(this.button)
.connectedTo(element)
.position(NbPosition.BOTTOM)
.offset(this.optionsOverlayOffset)
.adjustment(NbAdjustment.VERTICAL);
Expand Down Expand Up @@ -1123,9 +1163,9 @@ export class NbSelectComponent
)
.subscribe((event: KeyboardEvent) => {
if (event.keyCode === ESCAPE) {
this.button.nativeElement.focus();
this.hide();
} else {
this.button.nativeElement.focus();
} else if (!this.isOptionSearchInputAllowed) {
this.keyManager.onKeydown(event);
}
});
Expand All @@ -1137,11 +1177,21 @@ export class NbSelectComponent
}

protected subscribeOnButtonFocus() {
this.focusMonitor
.monitor(this.button)
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$])
.pipe(
map((origin) => !!origin),
finalize(() => this.focusMonitor.stopMonitoring(this.button)),
map(([buttonFocus, filterInputFocus]) => buttonFocus || filterInputFocus),
takeUntil(this.destroy$),
)
.subscribe(this.focused$);
Expand Down
15 changes: 5 additions & 10 deletions src/framework/theme/components/select/select.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ 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,
];
const NB_SELECT_COMPONENTS = [NbSelectComponent, NbSelectLabelComponent];

@NgModule({
imports: [
Expand All @@ -23,12 +21,9 @@ const NB_SELECT_COMPONENTS = [
NbCardModule,
NbIconModule,
NbOptionModule,
NbFormFieldModule,
],
exports: [
...NB_SELECT_COMPONENTS,
NbOptionModule,
],
exports: [...NB_SELECT_COMPONENTS, NbOptionModule],
declarations: [...NB_SELECT_COMPONENTS],
})
export class NbSelectModule {
}
export class NbSelectModule {}

0 comments on commit ff4f738

Please sign in to comment.