Skip to content

Commit a76a5f2

Browse files
authored
Merge pull request #356 from UiPath/feat/grid_radio_btn
Feat(grid): add radio button selection
2 parents 13b1ed3 + ef3c5c5 commit a76a5f2

File tree

11 files changed

+178
-54
lines changed

11 files changed

+178
-54
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# v14.8.1 (2023-05-29)
2+
* **grid** add tooltips & disable state for radio selection
3+
* **grid** add tests for radio selection
4+
* **grid** add singleSelectable input
5+
* **grid** add radio btn select
6+
17
# v14.8.0 (2023-05-29)
28
* **suggest** changed in EventEmitter
39
* **suggest** added item selected output

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "angular-components",
3-
"version": "14.8.0",
3+
"version": "14.8.1",
44
"author": {
55
"name": "UiPath Inc",
66
"url": "https://uipath.com"

projects/angular/components/ui-grid/src/ui-grid.component.html

Lines changed: 63 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@
122122
<div *ngIf="showHeaderRow"
123123
class="ui-grid-header-row"
124124
role="row">
125-
<div *ngIf="selectable"
125+
<div *ngIf="selectable && !singleSelectable"
126126
class="ui-grid-header-cell ui-grid-checkbox-cell ui-grid-feature-cell"
127127
role="columnheader">
128128
<mat-checkbox #selectAvailableRowsCheckbox
@@ -165,7 +165,8 @@
165165
[matTooltipDisabled]="resizeManager.isResizing"
166166
[attr.aria-label]="column.title + (column.description ? ('. ' + column.description) : '') + (column.sortable && intl.sortableMessage ? '. ' + intl.sortableMessage : '')"
167167
matTooltipClass="preserve-whitespace">
168-
{{ column.title }}</p>
168+
{{ column.title }}
169+
</p>
169170

170171
<mat-icon *ngIf="column.description"
171172
[matTooltip]="column.description"
@@ -258,7 +259,7 @@
258259
let last = last;
259260
let index = index;
260261
trackBy: dataManager.hashTrack">
261-
<ng-container *ngTemplateOutlet="rowTemplate; context: {
262+
<ng-container *ngTemplateOutlet="rowTemplate; context: {
262263
data: row,
263264
index: index,
264265
expanded: expandedEntries,
@@ -290,7 +291,7 @@
290291
<ng-container *ngFor="let row of dataManager.data$ | async;
291292
let index = index;
292293
let last = last;">
293-
<ng-container *ngTemplateOutlet="rowTemplate; context: {
294+
<ng-container *ngTemplateOutlet="rowTemplate; context: {
294295
data: row,
295296
index: index,
296297
expanded: expandedEntries,
@@ -332,21 +333,30 @@
332333
<div *ngIf="isProjected"
333334
class="ui-grid-cell ui-grid-feature-cell ui-grid-mobile-feature-cell"
334335
role="gridcell">
335-
<div *ngIf="selectable"
336-
class="ui-grid-mobile-feature-container ui-grid-mobile-refresh-container">
337-
<mat-checkbox *ngLet="disableSelectionByEntry(row) as disabledReason"
338-
(click)="checkShift($event)"
339-
(keyup.shift.space)="checkShift($event)"
340-
(keyup.space)="checkShift($event)"
341-
(change)="handleSelection(index, row)"
342-
[checked]="selectionManager.isSelected(row)"
343-
[indeterminate]="selectionManager.isIndeterminate(row)"
344-
[matTooltip]="disabledReason || checkboxTooltip(row)"
345-
[aria-label]="disabledReason || checkboxTooltip(row)"
346-
[disabled]="!!disabledReason"
347-
tabindex="0">
348-
</mat-checkbox>
349-
</div>
336+
<ng-container *ngLet="disableSelectionByEntry(row) as disabledReason">
337+
<div *ngIf="selectable && !singleSelectable"
338+
class="ui-grid-mobile-feature-container ui-grid-mobile-refresh-container">
339+
<mat-checkbox (click)="checkShift($event)"
340+
(keyup.shift.space)="checkShift($event)"
341+
(keyup.space)="checkShift($event)"
342+
(change)="handleSelection(index, row)"
343+
[checked]="selectionManager.isSelected(row)"
344+
[indeterminate]="selectionManager.isIndeterminate(row)"
345+
[matTooltip]="disabledReason || checkboxTooltip(row)"
346+
[aria-label]="disabledReason || checkboxTooltip(row)"
347+
[disabled]="!!disabledReason"
348+
tabindex="0">
349+
</mat-checkbox>
350+
</div>
351+
<div *ngIf="singleSelectable"
352+
class="ui-grid-mobile-feature-container ui-grid-mobile-refresh-container">
353+
<mat-radio-button [checked]="selectionManager.isSelected(row)"
354+
[disabled]="disabledReason"
355+
[matTooltip]="disabledReason || checkboxTooltip(row)"
356+
[aria-label]="disabledReason || checkboxTooltip(row)"
357+
(change)="rowSelected(row)"></mat-radio-button>
358+
</div>
359+
</ng-container>
350360
<div *ngIf="!!actions"
351361
role="gridcell"
352362
class="ui-grid-mobile-feature-container ui-grid-mobile-action-container">
@@ -358,22 +368,30 @@
358368
</div>
359369
</div>
360370

361-
<div *ngIf="!isProjected &&
362-
selectable"
371+
<div *ngIf="!isProjected && (selectable || singleSelectable)"
363372
class="ui-grid-cell ui-grid-checkbox-cell ui-grid-feature-cell"
364373
role="gridcell">
365-
<mat-checkbox *ngLet="disableSelectionByEntry(row) as disabledReason"
366-
(click)="checkShift($event)"
367-
(keyup.shift.space)="checkShift($event)"
368-
(keyup.space)="checkShift($event)"
369-
(change)="handleSelection(index, row)"
370-
[checked]="selectionManager.isSelected(row)"
371-
[indeterminate]="selectionManager.isIndeterminate(row)"
372-
[matTooltip]="disabledReason || checkboxTooltip(row)"
373-
[aria-label]="disabledReason || checkboxTooltip(row)"
374-
[disabled]="disabledReason"
375-
tabindex="0">
376-
</mat-checkbox>
374+
<ng-container *ngLet="disableSelectionByEntry(row) as disabledReason">
375+
<mat-radio-button *ngIf="singleSelectable; else multiSelectable"
376+
[checked]="selectionManager.isSelected(row)"
377+
[disabled]="disabledReason"
378+
[matTooltip]="disabledReason || checkboxTooltip(row)"
379+
[aria-label]="disabledReason || checkboxTooltip(row)"
380+
(change)="rowSelected(row)"></mat-radio-button>
381+
<ng-template #multiSelectable>
382+
<mat-checkbox (click)="checkShift($event)"
383+
(keyup.shift.space)="checkShift($event)"
384+
(keyup.space)="checkShift($event)"
385+
(change)="handleSelection(index, row)"
386+
[checked]="selectionManager.isSelected(row)"
387+
[indeterminate]="selectionManager.isIndeterminate(row)"
388+
[matTooltip]="disabledReason || checkboxTooltip(row)"
389+
[aria-label]="disabledReason || checkboxTooltip(row)"
390+
[disabled]="disabledReason"
391+
tabindex="0">
392+
</mat-checkbox>
393+
</ng-template>
394+
</ng-container>
377395
</div>
378396

379397
<ng-container *ngFor="let column of renderedColumns$ | async">
@@ -444,18 +462,17 @@
444462
</ng-template>
445463

446464
<ng-template #rowCardTemplate
447-
let-row="data"
448-
let-expanded="expanded"
449-
let-last="last"
450-
let-index="index">
465+
let-row="data"
466+
let-expanded="expanded"
467+
let-last="last"
468+
let-index="index">
451469
<div cdkMonitorSubtreeFocus
452-
class="ui-grid-card-wrapper"
453-
[ngClass]="rowConfig?.ngClassFn(row) ?? ''"
454-
[tabIndex]="0"
455-
[attr.data-row-index]="index"
456-
(click)="onRowClick($event, row)"
457-
(keyup.enter)="onRowClick($event, row)"
458-
>
470+
class="ui-grid-card-wrapper"
471+
[ngClass]="rowConfig?.ngClassFn(row) ?? ''"
472+
[tabIndex]="0"
473+
[attr.data-row-index]="index"
474+
(click)="onRowClick($event, row)"
475+
(keyup.enter)="onRowClick($event, row)">
459476
<ng-container *ngIf="cardTemplate?.html; else defaultCardTemplate">
460477
<ng-container *ngTemplateOutlet="cardTemplate?.html || defaultCardTemplate;context: {
461478
data: row,
@@ -468,8 +485,8 @@
468485
</ng-template>
469486

470487
<ng-template #defaultCardTemplate
471-
let-row="data"
472-
let-index="index">
488+
let-row="data"
489+
let-index="index">
473490
<div class="ui-grid-card-default">
474491
<ng-container *ngFor="let column of renderedColumns$ | async">
475492
<div [class.ui-grid-primary]="column.directive.primary"

projects/angular/components/ui-grid/src/ui-grid.component.spec.ts

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ describe('Component: UiGrid', () => {
7070
[disableSelectionByEntry]="disableSelectionByEntry"
7171
[refreshable]="refreshable"
7272
[selectable]="selectable"
73+
[singleSelectable]="singleSelectable"
7374
[showHeaderRow]="showHeaderRow"
7475
[virtualScroll]="virtualScroll">
7576
<ui-grid-column [property]="'myNumber'"
@@ -111,6 +112,7 @@ describe('Component: UiGrid', () => {
111112
isColumnVisible = true;
112113
isFilterVisible = true;
113114
selectable?: boolean;
115+
singleSelectable?: boolean;
114116
refreshable?: boolean;
115117
showHeaderRow = true;
116118
virtualScroll = false;
@@ -483,6 +485,89 @@ describe('Component: UiGrid', () => {
483485
});
484486
});
485487

488+
describe('Feature: radio button selection', () => {
489+
beforeEach(() => {
490+
component.selectable = false;
491+
component.singleSelectable = true;
492+
fixture.detectChanges();
493+
});
494+
495+
it('should have correct selection', () => {
496+
const radioBtns = fixture.debugElement.queryAll(By.css('.mat-radio-label'));
497+
expect(radioBtns.length).toEqual(50);
498+
499+
const firstRadioBtn = radioBtns[0];
500+
firstRadioBtn.nativeElement.dispatchEvent(EventGenerator.click);
501+
fixture.detectChanges();
502+
503+
expect(component.grid.selectionManager.selected[0].id).toEqual(component.data[0].id);
504+
});
505+
506+
it('should have only one selection', () => {
507+
const radioBtns = fixture.debugElement.queryAll(By.css('.mat-radio-label'));
508+
509+
const firstRadioBtn = radioBtns[0];
510+
firstRadioBtn.nativeElement.dispatchEvent(EventGenerator.click);
511+
fixture.detectChanges();
512+
513+
const secondRadioBtn = radioBtns[1];
514+
secondRadioBtn.nativeElement.dispatchEvent(EventGenerator.click);
515+
fixture.detectChanges();
516+
517+
expect(component.grid.selectionManager.selected[0].id).toEqual(component.data[1].id);
518+
expect(component.grid.selectionManager.selected.length).toEqual(1);
519+
});
520+
521+
it('should have preselected option displayed', () => {
522+
const randomIdx = faker.random.number({ max: 49 });
523+
grid.selectionManager.select(data[randomIdx]);
524+
fixture.detectChanges();
525+
const radioBtns = fixture.debugElement.queryAll(By.css('[role="gridcell"] mat-radio-button'));
526+
const checkedRadioBtn = radioBtns[randomIdx];
527+
expect(checkedRadioBtn.nativeElement.classList.contains('mat-radio-checked')).toBeTruthy();
528+
});
529+
530+
it('should display Select / Deselect according to button state', () => {
531+
const checkedBtnIdx = faker.random.number({ max: 25 });
532+
const uncheckedBtnIdx = faker.random.number({
533+
min: 26,
534+
max: 49,
535+
});
536+
grid.selectionManager.select(data[checkedBtnIdx]);
537+
fixture.detectChanges();
538+
const radioBtns = fixture.debugElement.queryAll(By.css('[role="gridcell"] mat-radio-button'));
539+
const checkedRadioBtn = radioBtns[checkedBtnIdx].nativeElement;
540+
const uncheckedRadioBtn = radioBtns[uncheckedBtnIdx].nativeElement;
541+
expect(checkedRadioBtn.getAttribute('ng-reflect-message')).toEqual(`Deselect row ${checkedBtnIdx}`);
542+
expect(uncheckedRadioBtn.getAttribute('ng-reflect-message')).toEqual(`Select row ${uncheckedBtnIdx}`);
543+
});
544+
545+
it('should disable radio btn according to disableSelectionByEntry', () => {
546+
const selectableBtnIdx = faker.random.number({ max: 25 });
547+
const disabledBtnIdx = faker.random.number({
548+
min: 26,
549+
max: 49,
550+
});
551+
const disableSelectionByEntry = (entry?: ITestEntity) => entry && entry.id === data[disabledBtnIdx].id
552+
? 'unselectable'
553+
: null;
554+
555+
component.disableSelectionByEntry = disableSelectionByEntry;
556+
grid.selectionManager.disableSelectionByEntry = disableSelectionByEntry;
557+
fixture.detectChanges();
558+
559+
const radioBtns = fixture.debugElement.queryAll(By.css('[role="gridcell"] mat-radio-button'));
560+
const selectableRadioBtn = radioBtns[selectableBtnIdx].nativeElement;
561+
const disabledRadioBtn = radioBtns[disabledBtnIdx].nativeElement;
562+
563+
expect(selectableRadioBtn.getAttribute('ng-reflect-message')).toEqual(`Select row ${selectableBtnIdx}`);
564+
expect(selectableRadioBtn.classList.contains('mat-radio-disabled')).toBeFalsy();
565+
566+
expect(disabledRadioBtn.getAttribute('ng-reflect-message')).toEqual(`unselectable`);
567+
expect(disabledRadioBtn.classList.contains('mat-radio-disabled')).toBeTruthy();
568+
});
569+
});
570+
486571
describe('Feature: checkbox', () => {
487572
it('should have ariaLabel set correctly for toggle selection', () => {
488573
const checkboxHeader = fixture.debugElement.query(By.css('.ui-grid-header-cell.ui-grid-checkbox-cell'));
@@ -795,7 +880,7 @@ describe('Component: UiGrid', () => {
795880
expect(matCheckbox.checked).toEqual(false);
796881
});
797882

798-
it('should unselect heade checkbox if all grid rows are unselected', () => {
883+
it('should unselect header checkbox if all grid rows are unselected', () => {
799884
const disableSelectionByEntry = (entry?: ITestEntity) => entry && entry.id % 2 === 1 ? 'unselectable' : null;
800885

801886
component.disableSelectionByEntry = disableSelectionByEntry;
@@ -4338,10 +4423,8 @@ describe('Component: UiGrid', () => {
43384423
});
43394424

43404425
it('should render provided card template', () => {
4341-
43424426
const cardContainer = fixture.debugElement.query(By.css('.expanded-row'));
43434427

4344-
console.log(cardContainer);
43454428
expect(cardContainer).toBeDefined();
43464429
expect(cardContainer.nativeElement.querySelector('[data-property="myNumber"]')).toBeDefined();
43474430
expect(cardContainer.nativeElement.querySelector('[data-property="myBool"]')).toBeDefined();

projects/angular/components/ui-grid/src/ui-grid.component.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,13 @@ export class UiGridComponent<T extends IGridDataEntry> extends ResizableGrid<T>
251251
@Input()
252252
selectable = true;
253253

254+
/**
255+
* Configure if the grid allows radio button selection for its items.
256+
*
257+
*/
258+
@Input()
259+
singleSelectable = false;
260+
254261
/**
255262
* Option to select an alternate layout for footer pagination.
256263
*
@@ -1068,6 +1075,11 @@ export class UiGridComponent<T extends IGridDataEntry> extends ResizableGrid<T>
10681075
activeItem?.focus();
10691076
}
10701077

1078+
rowSelected(row: T) {
1079+
this.selectionManager.clear();
1080+
this.selectionManager.select(row);
1081+
}
1082+
10711083
private _announceGridHeaderActions() {
10721084
this._queuedAnnouncer.enqueue(this.intl.gridHeaderActionsNotice);
10731085
}

projects/angular/components/ui-grid/src/ui-grid.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { MatIconModule } from '@angular/material/icon';
88
import { MatMenuModule } from '@angular/material/menu';
99
import { MatPaginatorModule } from '@angular/material/paginator';
1010
import { MatProgressBarModule } from '@angular/material/progress-bar';
11+
import { MatRadioModule } from '@angular/material/radio';
1112
import { MatSelectModule } from '@angular/material/select';
1213
import { MatTooltipModule } from '@angular/material/tooltip';
1314
import { UiAutoAccessibleLabelModule } from '@uipath/angular/a11y';
@@ -21,8 +22,8 @@ import { UiGridExpandedRowDirective } from './body/ui-grid-expanded-row.directiv
2122
import { UiGridLoadingDirective } from './body/ui-grid-loading.directive';
2223
import { UiGridNoContentDirective } from './body/ui-grid-no-content.directive';
2324
import { UiGridRowActionDirective } from './body/ui-grid-row-action.directive';
24-
import { UiGridRowConfigDirective } from './body/ui-grid-row-config.directive';
2525
import { UiGridRowCardViewDirective } from './body/ui-grid-row-card-view.directive';
26+
import { UiGridRowConfigDirective } from './body/ui-grid-row-config.directive';
2627
import { UiGridCustomPaginatorModule } from './components/ui-grid-custom-paginator/ui-grid-custom-paginator.module';
2728
import { UiGridSearchModule } from './components/ui-grid-search/ui-grid-search.module';
2829
import { UiGridToggleColumnsModule } from './components/ui-grid-toggle-columns/ui-grid-toggle-columns.module';
@@ -44,6 +45,7 @@ import { UiGridComponent } from './ui-grid.component';
4445
MatSelectModule,
4546
MatTooltipModule,
4647
MatProgressBarModule,
48+
MatRadioModule,
4749
ScrollingModule,
4850
UiGridSearchModule,
4951
UiGridToggleColumnsModule,

projects/angular/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@uipath/angular",
3-
"version": "14.8.0",
3+
"version": "14.8.1",
44
"license": "MIT",
55
"author": {
66
"name": "UiPath Inc",

projects/playground/src/app/pages/grid/component/grid.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
[loading]="inputs.loading"
77
[disabled]="inputs.disabled"
88
[selectable]="inputs.selectable"
9+
[singleSelectable]="inputs.singleSelectable"
910
[toggleColumns]="inputs.toggleColumns"
1011
[multiPageSelect]="inputs.multiPageSelect"
1112
[refreshable]="inputs.refreshable"

0 commit comments

Comments
 (0)