diff --git a/projects/components/src/multi-select/multi-select.component.scss b/projects/components/src/multi-select/multi-select.component.scss index 29ee683be..a101e05b2 100644 --- a/projects/components/src/multi-select/multi-select.component.scss +++ b/projects/components/src/multi-select/multi-select.component.scss @@ -64,7 +64,7 @@ } .multi-select-content { - @include dropdown(); + @include dropdown(6px); min-width: 120px; .multi-select-option { @@ -91,4 +91,13 @@ background: $gray-1; } } + + .search-bar { + display: flex; + height: 34px; + margin-top: 2px; + cursor: pointer; + font-size: 14px; + align-items: center; + } } diff --git a/projects/components/src/multi-select/multi-select.component.test.ts b/projects/components/src/multi-select/multi-select.component.test.ts index 63466e49b..0c9aa3f00 100644 --- a/projects/components/src/multi-select/multi-select.component.test.ts +++ b/projects/components/src/multi-select/multi-select.component.test.ts @@ -1,5 +1,6 @@ import { fakeAsync, flush } from '@angular/core/testing'; import { IconType } from '@hypertrace/assets-library'; +import { SearchBoxComponent } from '@hypertrace/components'; import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; import { MockComponent } from 'ng-mocks'; import { DividerComponent } from '../divider/divider.component'; @@ -14,7 +15,7 @@ describe('Multi Select Component', () => { component: MultiSelectComponent, imports: [LetAsyncModule], entryComponents: [SelectOptionComponent], - declarations: [MockComponent(LabelComponent), MockComponent(DividerComponent)], + declarations: [MockComponent(LabelComponent), MockComponent(DividerComponent), MockComponent(SearchBoxComponent)], shallow: true }); @@ -250,4 +251,40 @@ describe('Multi Select Component', () => { expect(spectator.element).toHaveText(selectionOptions[1].label); expect(spectator.query('.trigger-content')!.getAttribute('style')).toBe('justify-content: flex-end;'); })); + + test('should show searchbox if applicable and function as expected', fakeAsync(() => { + spectator = hostFactory( + ` + + + + `, + { + hostProps: { + options: selectionOptions, + enableSearch: true + } + } + ); + + spectator.tick(); + expect(spectator.query('.search-bar')).toExist(); + spectator.click('.search-bar'); + + spectator.triggerEventHandler(SearchBoxComponent, 'valueChange', 'fi'); + spectator.tick(); + + let options = spectator.queryAll('.multi-select-option', { root: true }); + expect(options.length).toBe(1); + expect(options[0]).toContainText('first'); + + spectator.triggerEventHandler(SearchBoxComponent, 'valueChange', 'i'); + spectator.tick(); + + options = spectator.queryAll('.multi-select-option', { root: true }); + expect(options.length).toBe(2); + expect(options[0]).toContainText('first'); + expect(options[1]).toContainText('third'); + flush(); + })); }); diff --git a/projects/components/src/multi-select/multi-select.component.ts b/projects/components/src/multi-select/multi-select.component.ts index 36cacf9f7..b842283de 100644 --- a/projects/components/src/multi-select/multi-select.component.ts +++ b/projects/components/src/multi-select/multi-select.component.ts @@ -14,6 +14,7 @@ import { LoggerService, queryListAndChanges$, TypedSimpleChanges } from '@hypert import { EMPTY, merge, Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { IconSize } from '../icon/icon-size'; +import { SearchBoxDisplayMode } from '../search-box/search-box.component'; import { SelectOption } from '../select/select-option'; import { SelectOptionComponent } from '../select/select-option.component'; import { SelectSize } from '../select/select-size'; @@ -56,16 +57,23 @@ import { MultiSelectJustify } from './multi-select-justify'; + + + Select All - - - + + + implements AfterContentInit, OnChanges { @Input() public showBorder: boolean = false; + @Input() + public enableSearch: boolean = false; + @Input() public justify: MultiSelectJustify = MultiSelectJustify.Left; @@ -122,12 +133,14 @@ export class MultiSelectComponent implements AfterContentInit, OnChanges { public popoverOpen: boolean = false; public selected$?: Observable[]>; public triggerLabel?: string; + public filteredItems?: SelectOptionComponent[]; public constructor(private readonly loggerService: LoggerService) {} public ngAfterContentInit(): void { this.selected$ = this.buildObservableOfSelected(); this.setTriggerLabel(); + this.filteredItems = this.items?.toArray(); } public ngOnChanges(changes: TypedSimpleChanges): void { @@ -137,6 +150,10 @@ export class MultiSelectComponent implements AfterContentInit, OnChanges { this.setTriggerLabel(); } + public searchOptions(searchText: string): void { + this.filteredItems = this.items?.filter(item => item.label.toLowerCase().includes(searchText.toLowerCase())); + } + public onAllSelectionChange(): void { this.selected = this.areAllOptionsSelected() ? [] : this.items!.map(item => item.value); // Select All or none this.setSelection(); diff --git a/projects/components/src/multi-select/multi-select.module.ts b/projects/components/src/multi-select/multi-select.module.ts index 84e413994..25786a4ed 100644 --- a/projects/components/src/multi-select/multi-select.module.ts +++ b/projects/components/src/multi-select/multi-select.module.ts @@ -6,10 +6,20 @@ import { IconModule } from '../icon/icon.module'; import { LabelModule } from '../label/label.module'; import { LetAsyncModule } from '../let-async/let-async.module'; import { PopoverModule } from '../popover/popover.module'; +import { TraceSearchBoxModule } from '../search-box/search-box.module'; import { MultiSelectComponent } from './multi-select.component'; @NgModule({ - imports: [FormsModule, CommonModule, IconModule, LabelModule, LetAsyncModule, PopoverModule, DividerModule], + imports: [ + FormsModule, + CommonModule, + IconModule, + LabelModule, + LetAsyncModule, + PopoverModule, + DividerModule, + TraceSearchBoxModule + ], declarations: [MultiSelectComponent], exports: [MultiSelectComponent] }) diff --git a/projects/components/src/search-box/search-box.component.scss b/projects/components/src/search-box/search-box.component.scss index 2d3cf9be3..aea96f99b 100644 --- a/projects/components/src/search-box/search-box.component.scss +++ b/projects/components/src/search-box/search-box.component.scss @@ -6,8 +6,6 @@ display: flex; position: relative; align-items: center; - border: 1px solid $color-border; - border-radius: 6px; padding: 0 8px; .icon { @@ -25,6 +23,7 @@ &.focused { caret-color: $blue-5; border: 1px solid $blue-5; + border-radius: 6px; box-shadow: 0 1px 3px rgba(0, 83, 215, 0.16); &:hover { @@ -57,3 +56,14 @@ } } } + +.border { + border: 1px solid $color-border; + border-radius: 6px; +} + +.no-border { + &:hover { + border: none; + } +} diff --git a/projects/components/src/search-box/search-box.component.test.ts b/projects/components/src/search-box/search-box.component.test.ts index 3e34b611f..3fbf19541 100644 --- a/projects/components/src/search-box/search-box.component.test.ts +++ b/projects/components/src/search-box/search-box.component.test.ts @@ -1,7 +1,7 @@ import { fakeAsync } from '@angular/core/testing'; import { runFakeRxjs } from '@hypertrace/test-utils'; import { createHostFactory, Spectator } from '@ngneat/spectator/jest'; -import { SearchBoxComponent } from './search-box.component'; +import { SearchBoxComponent, SearchBoxDisplayMode } from './search-box.component'; describe('Search box Component', () => { let spectator: Spectator; @@ -32,6 +32,24 @@ describe('Search box Component', () => { }); })); + test('should apply no-border class correctly', () => { + spectator = createHost(``, { + hostProps: { + displayMode: SearchBoxDisplayMode.NoBorder + } + }); + + expect(spectator.query('.ht-search-box')?.classList).toContain('no-border'); + expect(spectator.query('.ht-search-box')?.classList).not.toContain('border'); + }); + + test('should apply border class correctly by default', () => { + spectator = createHost(``); + + expect(spectator.query('.ht-search-box')?.classList).not.toContain('no-border'); + expect(spectator.query('.ht-search-box')?.classList).toContain('border'); + }); + test('should work with arbitrary debounce time', fakeAsync(() => { spectator = createHost( ``, diff --git a/projects/components/src/search-box/search-box.component.ts b/projects/components/src/search-box/search-box.component.ts index 392214dea..44b21b3bd 100644 --- a/projects/components/src/search-box/search-box.component.ts +++ b/projects/components/src/search-box/search-box.component.ts @@ -11,7 +11,7 @@ import { IconSize } from '../icon/icon-size'; changeDetection: ChangeDetectionStrategy.OnPush, providers: [SubscriptionLifecycle], template: ` - + = new EventEmitter(); @@ -91,3 +94,8 @@ export class SearchBoxComponent implements OnInit, OnChanges { ); } } + +export const enum SearchBoxDisplayMode { + Border = 'border', + NoBorder = 'no-border' +}