Skip to content

Commit 1d80a59

Browse files
authored
Update sort icons and adding checkbox in header cell for multi select (#1477)
* feat: adding checkbox to header for multi selection * refactor: fix tests * refactor: revert checkbox change * refactor: addressing review comments
1 parent 52a1832 commit 1d80a59

8 files changed

+110
-45
lines changed

projects/components/src/checkbox/checkbox.component.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
::ng-deep .mat-checkbox-checked .mat-checkbox-background,
55
.mat-checkbox-indeterminate {
66
background-color: $blue-4;
7+
8+
.mat-checkbox-mixedmark {
9+
background-color: white;
10+
}
711
}
812

913
::ng-deep .mat-checkbox-disabled {

projects/components/src/checkbox/checkbox.component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { MatCheckboxChange } from '@angular/material/checkbox';
1111
labelPosition="after"
1212
[checked]="this.isChecked"
1313
[disabled]="this.isDisabled"
14+
[(indeterminate)]="this.indeterminate"
1415
(change)="this.onCheckboxChange($event)"
1516
class="ht-checkbox"
1617
[ngClass]="{ disabled: this.isDisabled }"
@@ -35,10 +36,13 @@ export class CheckboxComponent implements ControlValueAccessor {
3536
this.isChecked = checked ?? false;
3637
}
3738

38-
public get checked(): boolean {
39+
public get checked(): boolean | undefined {
3940
return this.isChecked;
4041
}
4142

43+
@Input()
44+
public indeterminate?: boolean;
45+
4246
@Input()
4347
public set disabled(disabled: boolean | undefined) {
4448
this.isDisabled = disabled ?? false;

projects/components/src/table/data/table-cdk-data-source.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ export class TableCdkDataSource implements DataSource<TableRow> {
154154
this.rowsChange$.next(TableCdkRowUtil.mergeRowStates(this.cachedRows, unselectedRows));
155155
}
156156

157+
public getAllRows(): StatefulTableRow[] {
158+
return this.cachedRows;
159+
}
160+
157161
/****************************
158162
* Change Detection
159163
****************************/

projects/components/src/table/header/table-header-cell-renderer.component.scss

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
@import 'mixins';
22

3-
:host {
4-
width: 100%; // Sorry...
5-
}
6-
73
.table-header-cell-renderer {
84
@include ellipsis-overflow();
9-
@include overline($gray-5);
105
display: flex;
6+
align-items: center;
7+
width: 100%;
8+
height: 32px;
119

1210
&.sortable {
1311
cursor: pointer;
@@ -17,18 +15,6 @@
1715
color: $gray-9;
1816
}
1917

20-
&.left {
21-
text-align: left;
22-
}
23-
24-
&.center {
25-
text-align: center;
26-
}
27-
28-
&.right {
29-
text-align: right;
30-
}
31-
3218
.options-button {
3319
display: none;
3420
color: $gray-7;
@@ -46,29 +32,33 @@
4632
}
4733

4834
.title {
35+
@include overline($gray-5);
36+
4937
min-width: 0;
50-
width: 100%;
38+
flex: 1 1 auto;
39+
display: flex;
5140

52-
&.asc,
53-
&.desc {
54-
color: $gray-9;
41+
&.left {
42+
justify-content: flex-start;
5543
}
5644

57-
&:after {
58-
display: inline-block;
45+
&.center {
46+
justify-content: center;
5947
}
6048

61-
&.desc:after {
62-
content: '';
63-
font-size: 9px;
49+
&.right {
50+
justify-content: flex-end;
6451
}
6552

66-
&.asc:after {
67-
content: '';
68-
font-size: 10px;
69-
transform: scale(1, -1) translateY(1.5px); // That's right! Half pixels!
53+
.sort-icon {
54+
margin-left: 4px;
55+
color: $gray-9;
7056
}
7157
}
58+
59+
.state-checkbox {
60+
margin-left: 12px;
61+
}
7262
}
7363

7464
.popover-content {

projects/components/src/table/header/table-header-cell-renderer.component.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe('Table Header Cell Renderer', () => {
3535
}
3636
});
3737

38-
expect(spectator.query('.table-header-cell-renderer')).toHaveClass('sortable');
38+
expect(spectator.query('.table-header-cell-renderer > .title')).toHaveClass('sortable');
3939
});
4040

4141
test('should not have sortable class, if column cannot be sorted', () => {
@@ -53,7 +53,7 @@ describe('Table Header Cell Renderer', () => {
5353
}
5454
});
5555

56-
expect(spectator.query('.table-header-cell-renderer')).not.toHaveClass('sortable');
56+
expect(spectator.query('.table-header-cell-renderer > .title')).not.toHaveClass('sortable');
5757
});
5858

5959
test('should sort column when header title is clicked', fakeAsync(() => {

projects/components/src/table/header/table-header-cell-renderer.component.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,42 @@ import { TableColumnConfigExtended } from '../table.service';
3131
template: `
3232
<div
3333
*ngIf="this.columnConfig"
34-
[ngClass]="this.classes"
3534
[htTooltip]="this.getTooltip(this.columnConfig.titleTooltip, this.columnConfig.title)"
3635
class="table-header-cell-renderer"
3736
>
38-
<ng-container *ngIf="this.isShowOptionButton && this.leftAlignFilterButton">
39-
<ng-container *ngTemplateOutlet="optionsButton"></ng-container>
40-
</ng-container>
41-
<div class="title" [ngClass]="this.classes" (click)="this.onSortChange()">{{ this.columnConfig.title }}</div>
42-
<ng-container *ngIf="this.isShowOptionButton && !this.leftAlignFilterButton">
43-
<ng-container *ngTemplateOutlet="optionsButton"></ng-container>
37+
<ng-container *ngIf="!this.isStateColumn; else stateColumnTemplate">
38+
<ng-container *ngIf="this.isShowOptionButton && this.leftAlignFilterButton">
39+
<ng-container *ngTemplateOutlet="optionsButton"></ng-container>
40+
</ng-container>
41+
<div class="title" [ngClass]="this.classes" (click)="this.onSortChange()">
42+
<span>{{ this.columnConfig.title }}</span>
43+
<ng-container *ngIf="this.sort">
44+
<ht-icon
45+
class="sort-icon"
46+
[icon]="
47+
this.sort === '${TableSortDirection.Descending}' ? '${IconType.ArrowDown}' : '${IconType.ArrowUp}'
48+
"
49+
size="${IconSize.ExtraSmall}"
50+
></ht-icon>
51+
</ng-container>
52+
</div>
53+
54+
<ng-container *ngIf="this.isShowOptionButton && !this.leftAlignFilterButton">
55+
<ng-container *ngTemplateOutlet="optionsButton"></ng-container>
56+
</ng-container>
4457
</ng-container>
4558
59+
<ng-template #stateColumnTemplate>
60+
<ng-container *ngIf="this.isMultipleSelectionStateColumn">
61+
<ht-checkbox
62+
class="state-checkbox"
63+
[htTooltip]="this.getHeaderCheckboxTooltip()"
64+
[indeterminate]="this.indeterminateRowsSelected"
65+
(checkedChange)="this.onToggleAllSelectedChange($event)"
66+
></ht-checkbox>
67+
</ng-container>
68+
</ng-template>
69+
4670
<ng-template #htmlTooltip>
4771
<div [innerHTML]="this.columnConfig?.titleTooltip"></div>
4872
</ng-template>
@@ -104,19 +128,28 @@ export class TableHeaderCellRendererComponent implements OnInit, OnChanges {
104128
@Input()
105129
public sort?: TableSortDirection;
106130

131+
@Input()
132+
public indeterminateRowsSelected?: boolean;
133+
107134
@Output()
108135
public readonly sortChange: EventEmitter<TableSortDirection | undefined> = new EventEmitter();
109136

110137
@Output()
111138
public readonly columnsChange: EventEmitter<TableColumnConfigExtended[]> = new EventEmitter();
112139

140+
@Output()
141+
public readonly allRowsSelectionChange: EventEmitter<boolean> = new EventEmitter();
142+
113143
public alignment?: TableCellAlignmentType;
114144
public leftAlignFilterButton: boolean = false;
115145
public classes: string[] = [];
116146

117147
public isFilterable: boolean = false;
118148
public isEditableAvailableColumns: boolean = false;
119149
public isShowOptionButton: boolean = false;
150+
public isStateColumn: boolean = false;
151+
public isMultipleSelectionStateColumn: boolean = false;
152+
private allRowsSelected: boolean = false;
120153

121154
@ViewChild('htmlTooltip')
122155
public htmlTooltipTemplate?: TemplateRef<unknown>;
@@ -137,6 +170,8 @@ export class TableHeaderCellRendererComponent implements OnInit, OnChanges {
137170
this.isEditableAvailableColumns = this.areAnyAvailableColumnsEditable();
138171
this.isShowOptionButton =
139172
this.isFilterable || this.isEditableAvailableColumns || this.columnConfig?.sortable === true;
173+
this.isStateColumn = this.columnConfig?.id === '$$selected' || this.columnConfig?.id === '$$expanded';
174+
this.isMultipleSelectionStateColumn = this.columnConfig?.id === '$$selected';
140175
}
141176
}
142177

@@ -155,6 +190,19 @@ export class TableHeaderCellRendererComponent implements OnInit, OnChanges {
155190
this.classes = this.buildClasses();
156191
}
157192

193+
public onToggleAllSelectedChange(allSelected: boolean): void {
194+
this.allRowsSelected = allSelected;
195+
this.allRowsSelectionChange.emit(allSelected);
196+
}
197+
198+
public getHeaderCheckboxTooltip(): string {
199+
return this.indeterminateRowsSelected
200+
? 'Some rows are selected'
201+
: this.allRowsSelected
202+
? 'All rows in the current page are selected'
203+
: 'None of the rows in the current page are selected';
204+
}
205+
158206
private buildClasses(): string[] {
159207
return [
160208
...(this.alignment !== undefined ? [this.alignment.toLowerCase()] : []),

projects/components/src/table/table.component.scss

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,6 @@ $header-height: 32px;
7171

7272
.header-cell-renderer {
7373
width: 100%;
74-
padding: 10px 12px 10px 7px;
75-
}
76-
77-
.header-cell-renderer:first-child {
78-
padding-left: 12px;
7974
}
8075

8176
.header-column-resize-handle {

projects/components/src/table/table.component.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,10 @@ import { TableColumnConfigExtended, TableService } from './table.service';
9494
[availableColumns]="this.columnConfigs$ | async"
9595
[index]="index"
9696
[sort]="columnDef.sort"
97+
[indeterminateRowsSelected]="this.indeterminateRowsSelected"
9798
(sortChange)="this.onSortChange($event, columnDef)"
9899
(columnsChange)="this.onColumnsEdit($event)"
100+
(allRowsSelectionChange)="this.onHeaderAllRowsSelectionChange($event)"
99101
>
100102
</ht-table-header-cell-renderer>
101103
</cdk-header-cell>
@@ -380,6 +382,7 @@ export class TableComponent
380382
private resizeHeaderOffsetLeft: number = 0;
381383
private resizeStartX: number = 0;
382384
private resizeColumns?: ResizeColumns;
385+
public indeterminateRowsSelected?: boolean;
383386

384387
public constructor(
385388
private readonly elementRef: ElementRef,
@@ -564,6 +567,22 @@ export class TableComponent
564567
this.columnConfigsChange.emit(columnConfigs);
565568
}
566569

570+
public onHeaderAllRowsSelectionChange(allRowsSelected: boolean): void {
571+
if (this.hasMultiSelect()) {
572+
if (allRowsSelected) {
573+
this.dataSource?.selectAllRows();
574+
this.selections = this.dataSource?.getAllRows();
575+
} else {
576+
this.dataSource?.unselectAllRows();
577+
this.selections = [];
578+
}
579+
580+
this.selectionsChange.emit(this.selections);
581+
this.indeterminateRowsSelected = false;
582+
this.changeDetector.markForCheck();
583+
}
584+
}
585+
567586
public onDataCellClick(row: StatefulTableRow): void {
568587
// NOTE: Cell Renderers generally handle their own clicks. We should only perform table actions here.
569588
// Propagate the cell click to the row
@@ -654,6 +673,7 @@ export class TableComponent
654673
this.selections = [toggledRow];
655674
}
656675
this.selectionsChange.emit(this.selections);
676+
this.indeterminateRowsSelected = this.selections?.length !== this.dataSource?.getAllRows().length;
657677
this.changeDetector.markForCheck();
658678
}
659679

0 commit comments

Comments
 (0)