Skip to content

Commit 509c313

Browse files
authored
feat: adding all option and fixing some styling (#514)
* feat: adding all option and fixing some styling * refactor: addressing review comments
1 parent 2bde24e commit 509c313

File tree

5 files changed

+114
-23
lines changed

5 files changed

+114
-23
lines changed

projects/common/src/color/color.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const enum Color {
77
BlueGray4 = '#4b5f77',
88
Gray1 = '#f4f5f5',
99
Gray2 = '#e1e4e5',
10+
Gray4 = '#889499',
1011
Gray5 = '#657277',
1112
Gray7 = '#272c2e',
1213
Gray9 = '#080909',

projects/components/src/multi-select/multi-select.component.scss

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
.multi-select-content {
5555
@include dropdown();
5656
min-width: 120px;
57-
max-height: 204px;
5857

5958
.multi-select-option {
6059
display: flex;
@@ -65,15 +64,15 @@
6564
align-items: center;
6665

6766
.checkbox {
68-
margin-right: 8px;
67+
margin: 0px;
6968
}
7069

7170
.icon {
72-
margin-right: 8px;
71+
margin-left: 8px;
7372
}
7473

7574
.label {
76-
margin-right: 8px;
75+
margin-left: 8px;
7776
}
7877

7978
&:hover {

projects/components/src/multi-select/multi-select.component.test.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { fakeAsync, flush } from '@angular/core/testing';
22
import { IconType } from '@hypertrace/assets-library';
33
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
44
import { MockComponent } from 'ng-mocks';
5+
import { DividerComponent } from '../divider/divider.component';
56
import { LabelComponent } from '../label/label.component';
67
import { LetAsyncModule } from '../let-async/let-async.module';
78
import { SelectJustify } from '../select/select-justify';
@@ -13,7 +14,7 @@ describe('Multi Select Component', () => {
1314
component: MultiSelectComponent,
1415
imports: [LetAsyncModule],
1516
entryComponents: [SelectOptionComponent],
16-
declarations: [MockComponent(LabelComponent)],
17+
declarations: [MockComponent(LabelComponent), MockComponent(DividerComponent)],
1718
shallow: true
1819
});
1920

@@ -75,7 +76,7 @@ describe('Multi Select Component', () => {
7576
spectator.tick();
7677

7778
spectator.click('.trigger-content');
78-
const optionElements = spectator.queryAll('.multi-select-option', { root: true });
79+
const optionElements = spectator.queryAll('.multi-select-option:not(.all-options)', { root: true });
7980
expect(spectator.query('.multi-select-content', { root: true })).toExist();
8081
expect(optionElements.length).toBe(3);
8182

@@ -109,15 +110,79 @@ describe('Multi Select Component', () => {
109110
spectator.tick();
110111
spectator.click('.trigger-content');
111112

112-
const optionElements = spectator.queryAll('.multi-select-option', { root: true });
113+
const optionElements = spectator.queryAll('.multi-select-option:not(.all-options)', { root: true });
113114
spectator.click(optionElements[2]);
115+
spectator.tick();
114116

115117
expect(onChange).toHaveBeenCalledTimes(1);
116118
expect(onChange).toHaveBeenCalledWith([selectionOptions[1].value, selectionOptions[2].value]);
117119
expect(spectator.query(LabelComponent)?.label).toEqual('second and 1 more');
118120
flush();
119121
}));
120122

123+
test('should show all checkbox only when enabled by input property', fakeAsync(() => {
124+
spectator = hostFactory(
125+
`
126+
<ht-multi-select>
127+
<ht-select-option *ngFor="let option of options" [label]="option.label" [value]="option.value">
128+
</ht-select-option>
129+
</ht-multi-select>`,
130+
{
131+
hostProps: {
132+
options: selectionOptions
133+
}
134+
}
135+
);
136+
137+
spectator.tick();
138+
spectator.click('.trigger-content');
139+
140+
const allOptionElement = spectator.query('.all-options', { root: true });
141+
expect(allOptionElement).not.toExist();
142+
143+
flush();
144+
}));
145+
146+
test('should notify and update selection when all checkbox is selected', fakeAsync(() => {
147+
const onChange = jest.fn();
148+
149+
spectator = hostFactory(
150+
`
151+
<ht-multi-select [selected]="selected" (selectedChange)="onChange($event)" [placeholder]="placeholder" [showAllOptionControl]="showAllOptionControl">
152+
<ht-select-option *ngFor="let option of options" [label]="option.label" [value]="option.value">
153+
</ht-select-option>
154+
</ht-multi-select>`,
155+
{
156+
hostProps: {
157+
options: selectionOptions,
158+
selected: [selectionOptions[1].value],
159+
placeholder: 'Select options',
160+
showAllOptionControl: true,
161+
onChange: onChange
162+
}
163+
}
164+
);
165+
166+
spectator.tick();
167+
spectator.click('.trigger-content');
168+
169+
const allOptionElement = spectator.query('.all-options', { root: true });
170+
expect(allOptionElement).toExist();
171+
spectator.click(allOptionElement!);
172+
173+
expect(onChange).toHaveBeenCalledTimes(1);
174+
expect(onChange).toHaveBeenCalledWith(selectionOptions.map(option => option.value));
175+
expect(spectator.query(LabelComponent)?.label).toEqual('first and 2 more');
176+
177+
// De select all
178+
spectator.click(allOptionElement!);
179+
expect(onChange).toHaveBeenCalledTimes(2);
180+
expect(onChange).toHaveBeenLastCalledWith([]);
181+
expect(spectator.query(LabelComponent)?.label).toEqual('Select options');
182+
183+
flush();
184+
}));
185+
121186
test('should notify but not change trigger label if triggerLabelDisplayMode is placeholder', fakeAsync(() => {
122187
const onChange = jest.fn();
123188

@@ -141,7 +206,7 @@ describe('Multi Select Component', () => {
141206
expect(spectator.query(LabelComponent)?.label).toEqual('Placeholder');
142207
spectator.click('.trigger-content');
143208

144-
const optionElements = spectator.queryAll('.multi-select-option', { root: true });
209+
const optionElements = spectator.queryAll('.multi-select-option:not(.all-options)', { root: true });
145210
spectator.click(optionElements[2]);
146211

147212
expect(onChange).toHaveBeenCalledTimes(1);

projects/components/src/multi-select/multi-select.component.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ import { SelectSize } from '../select/select-size';
4040
</ht-popover-trigger>
4141
<ht-popover-content>
4242
<div class="multi-select-content">
43+
<ng-container *ngIf="this.showAllOptionControl">
44+
<div class="multi-select-option all-options" (click)="this.onAllSelectionChange()">
45+
<input class="checkbox" type="checkbox" [checked]="this.areAllOptionsSelected()" />
46+
<span class="label">All</span>
47+
</div>
48+
49+
<ht-divider></ht-divider>
50+
</ng-container>
51+
4352
<div *ngFor="let item of items" (click)="this.onSelectionChange(item)" class="multi-select-option">
4453
<input class="checkbox" type="checkbox" [checked]="this.isSelectedItem(item)" />
4554
<ht-icon
@@ -79,6 +88,9 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
7988
@Input()
8089
public justify?: SelectJustify;
8190

91+
@Input()
92+
public showAllOptionControl?: boolean = false;
93+
8294
@Input()
8395
public triggerLabelDisplayMode: TriggerLabelDisplayMode = TriggerLabelDisplayMode.Selection;
8496

@@ -113,6 +125,33 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
113125
this.setTriggerLabel();
114126
}
115127

128+
public onAllSelectionChange(): void {
129+
this.selected = this.areAllOptionsSelected() ? [] : this.items!.map(item => item.value); // Select All or none
130+
this.setSelection();
131+
}
132+
133+
public areAllOptionsSelected(): boolean {
134+
return this.selected !== undefined && this.items !== undefined && this.selected.length === this.items.length;
135+
}
136+
137+
public onSelectionChange(item: SelectOptionComponent<V>): void {
138+
this.selected = this.isSelectedItem(item)
139+
? this.selected?.filter(value => value !== item.value)
140+
: (this.selected ?? []).concat(item.value);
141+
142+
this.setSelection();
143+
}
144+
145+
public isSelectedItem(item: SelectOptionComponent<V>): boolean {
146+
return this.selected !== undefined && this.selected.filter(value => value === item.value).length > 0;
147+
}
148+
149+
private setSelection(): void {
150+
this.setTriggerLabel();
151+
this.selected$ = this.buildObservableOfSelected();
152+
this.selectedChange.emit(this.selected);
153+
}
154+
116155
private setTriggerLabel(): void {
117156
if (this.triggerLabelDisplayMode === TriggerLabelDisplayMode.Placeholder) {
118157
this.triggerLabel = this.placeholder;
@@ -130,10 +169,6 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
130169
}
131170
}
132171

133-
public isSelectedItem(item: SelectOptionComponent<V>): boolean {
134-
return this.selected !== undefined && this.selected.filter(value => value === item.value).length > 0;
135-
}
136-
137172
private buildObservableOfSelected(): Observable<SelectOption<V>[] | undefined> {
138173
if (!this.items) {
139174
return EMPTY;
@@ -145,16 +180,6 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
145180
);
146181
}
147182

148-
public onSelectionChange(item: SelectOptionComponent<V>): void {
149-
this.selected = this.isSelectedItem(item)
150-
? this.selected?.filter(value => value !== item.value)
151-
: (this.selected ?? []).concat(item.value);
152-
153-
this.setTriggerLabel();
154-
this.selected$ = this.buildObservableOfSelected();
155-
this.selectedChange.emit(this.selected);
156-
}
157-
158183
// Find the select option object for a value
159184
private findItems(value: V[] | undefined): SelectOption<V>[] | undefined {
160185
if (this.items === undefined) {

projects/components/src/multi-select/multi-select.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { CommonModule } from '@angular/common';
22
import { NgModule } from '@angular/core';
33
import { FormsModule } from '@angular/forms';
4+
import { DividerModule } from '../divider/divider.module';
45
import { IconModule } from '../icon/icon.module';
56
import { LabelModule } from '../label/label.module';
67
import { LetAsyncModule } from '../let-async/let-async.module';
78
import { PopoverModule } from '../popover/popover.module';
89
import { MultiSelectComponent } from './multi-select.component';
910

1011
@NgModule({
11-
imports: [FormsModule, CommonModule, IconModule, LabelModule, LetAsyncModule, PopoverModule],
12+
imports: [FormsModule, CommonModule, IconModule, LabelModule, LetAsyncModule, PopoverModule, DividerModule],
1213
declarations: [MultiSelectComponent],
1314
exports: [MultiSelectComponent]
1415
})

0 commit comments

Comments
 (0)