Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(select): options search #3091

Merged
merged 13 commits into from
Aug 4, 2022
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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it hidden? why not *ngIf?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just simpler. Nebular subscribes to some button events like focus. And with *ngIf we should implement unsubscription and resubscription mechanisms. Looks too complicated for now

[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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't we use FormControl for optionSearchInput?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FormControl adds some complexity to updating search input value when something in select is updated. Probably should be improved, but for now, I think it's not bad.

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 {}
Loading