Skip to content

Commit d37853d

Browse files
authored
Merge branch 'master' into patch-2
2 parents c52f5c8 + e492fab commit d37853d

19 files changed

+391
-72
lines changed

.github/workflows/nodejs.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,6 @@ jobs:
5454
npm run test:i18n:dist
5555
- name: Publish to coveralls.io
5656
if: github.repository == 'IgniteUI/igniteui-angular' && matrix.node-version == '16.x'
57-
uses: coverallsapp/github-action@v1.1.2
57+
uses: coverallsapp/github-action@v1.2.1
5858
with:
5959
github-token: ${{ github.token }}

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ All notable changes for each version of this project will be documented in this
2424

2525
- `IgxGrid`
2626
- Added new auto-sizing API `recalculateAutoSizes` that recalculates widths of columns that have size set to `auto`. Can be used in scenarios where you want to auto-size the columns again post initialization.
27+
- Clicking with the Left Mouse key while holding `Ctrl` on selected cell will deselect the cell.
2728
- `igxPivotGrid`
2829
- Adding `aggregatorName` for pivot value configuration as an alternative to setting `aggregator` function. If both are set `aggregatorName` takes precedent. If none are set an error is thrown.
2930
- `IgxSimpleCombo`

projects/igniteui-angular/src/lib/badge/badge.component.ts

+18
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,24 @@ export class IgxBadgeComponent {
133133
@HostBinding('class.igx-badge')
134134
public cssClass = 'igx-badge';
135135

136+
/**
137+
* Sets a square shape to the badge, if `shape` is set to `square`.
138+
* By default the shape of the badge is rounded.
139+
*
140+
* @example
141+
* ```html
142+
* <igx-badge shape="square"></igx-badge>
143+
* ```
144+
*/
145+
@Input()
146+
public shape: 'rounded' | 'square' = 'rounded';
147+
148+
/** @hidden @internal */
149+
@HostBinding('class.igx-badge--square')
150+
public get _squareShape(): boolean {
151+
return this.shape === 'square';
152+
}
153+
136154
/**
137155
* Sets/gets the aria-label attribute value.
138156
*

projects/igniteui-angular/src/lib/core/styles/components/badge/_badge-component.scss

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
@extend %igx-badge--error !optional;
4040
}
4141

42+
@include m(square) {
43+
@extend %igx-badge--square !optional;
44+
}
45+
4246
@include m(hidden) {
4347
@extend %igx-badge--hidden !optional;
4448
}

projects/igniteui-angular/src/lib/core/styles/components/badge/_badge-theme.scss

+4
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@
127127
}
128128
}
129129

130+
%igx-badge--square {
131+
border-radius: 0;
132+
}
133+
130134
%igx-badge-value {
131135
white-space: nowrap;
132136
padding: $badge-value-padding;

projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.spec.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -230,16 +230,14 @@ describe('IgxRadioGroupDirective', () => {
230230

231231
const domRadio = fixture.debugElement.query(By.css('igx-radio')).nativeElement;
232232
expect(domRadio.classList.contains('igx-radio--invalid')).toBe(false);
233-
expect(radioGroup.invalid).toBe(false);
234233
expect(radioGroup.selected).toBeUndefined;
235234

236235
dispatchRadioEvent('keyup', domRadio, fixture);
237236
expect(domRadio.classList.contains('igx-radio--focused')).toBe(true);
238237
dispatchRadioEvent('blur', domRadio, fixture);
239238
fixture.detectChanges();
240239
tick();
241-
242-
//expect(radioGroup.invalid).toBe(true);
240+
243241
expect(domRadio.classList.contains('igx-radio--invalid')).toBe(true);
244242

245243
dispatchRadioEvent('keyup', domRadio, fixture);
@@ -251,7 +249,6 @@ describe('IgxRadioGroupDirective', () => {
251249

252250
expect(domRadio.classList.contains('igx-radio--checked')).toBe(true);
253251
expect(radioGroup.radioButtons.first.checked).toEqual(true);
254-
expect(radioGroup.invalid).toBe(false);
255252
expect(domRadio.classList.contains('igx-radio--invalid')).toBe(false);
256253
}));
257254
});

projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.ts

+55-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import {
22
AfterContentInit,
33
AfterViewInit,
4-
ContentChildren, Directive, EventEmitter, HostBinding, Input, NgModule, OnDestroy, Optional, Output, QueryList, Self
4+
ContentChildren, Directive, EventEmitter, HostBinding, HostListener, Input, NgModule, OnDestroy, Optional, Output, QueryList, Self
55
} from '@angular/core';
66
import { ControlValueAccessor, NgControl, Validators } from '@angular/forms';
77
import { fromEvent, noop, Subject } from 'rxjs';
88
import { startWith, takeUntil } from 'rxjs/operators';
99
import { mkenum } from '../../core/utils';
1010
import { IChangeRadioEventArgs, IgxRadioComponent } from '../../radio/radio.component';
11+
import { IgxDirectionality } from '../../services/direction/directionality';
1112
import { IgxRippleModule } from '../ripple/ripple.directive';
1213

1314
/**
@@ -194,6 +195,51 @@ export class IgxRadioGroupDirective implements AfterContentInit, AfterViewInit,
194195
@HostBinding('class.igx-radio-group--vertical')
195196
private vertical = false;
196197

198+
@HostListener('click', ['$event'])
199+
protected handleClick(event: MouseEvent) {
200+
event.stopPropagation();
201+
this.selected.nativeElement.focus();
202+
}
203+
204+
@HostListener('keydown', ['$event'])
205+
protected handleKeyDown(event: KeyboardEvent) {
206+
const { key } = event;
207+
208+
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)) {
209+
const buttons = this.radioButtons.filter(radio => !radio.disabled);
210+
const checked = buttons.find((radio) => radio.checked);
211+
let index = checked ? buttons.indexOf(checked!) : -1;
212+
const ltr = this._directionality.value === 'ltr';
213+
214+
switch (key) {
215+
case 'ArrowUp':
216+
index += -1;
217+
break;
218+
case 'ArrowLeft':
219+
index += ltr ? -1 : 1;
220+
break;
221+
case 'ArrowRight':
222+
index += ltr ? 1 : -1;
223+
break;
224+
default:
225+
index += 1;
226+
}
227+
228+
if (index < 0) index = buttons.length - 1;
229+
if (index > buttons.length - 1) index = 0;
230+
231+
buttons.forEach((radio) => {
232+
radio.deselect();
233+
radio.nativeElement.blur();
234+
});
235+
236+
buttons[index].focused = true;
237+
buttons[index].nativeElement.focus();
238+
buttons[index].select();
239+
event.preventDefault();
240+
}
241+
}
242+
197243
/**
198244
* Returns the alignment of the `igx-radio-group`.
199245
* ```typescript
@@ -284,8 +330,8 @@ export class IgxRadioGroupDirective implements AfterContentInit, AfterViewInit,
284330
});
285331
}
286332

287-
/**
288-
* @hidden
333+
/**
334+
* @hidden
289335
* @internal
290336
*/
291337
public ngAfterViewInit() {
@@ -315,6 +361,10 @@ export class IgxRadioGroupDirective implements AfterContentInit, AfterViewInit,
315361
* @internal
316362
*/
317363
private updateValidityOnBlur() {
364+
this.radioButtons.forEach((button) => {
365+
button.focused = false;
366+
});
367+
318368
if (this.required) {
319369
const checked = this.radioButtons.find(x => x.checked);
320370
this.invalid = !checked;
@@ -372,7 +422,8 @@ export class IgxRadioGroupDirective implements AfterContentInit, AfterViewInit,
372422

373423
constructor(
374424
@Optional() @Self() public ngControl: NgControl,
375-
) {
425+
private _directionality: IgxDirectionality,
426+
) {
376427
if (this.ngControl !== null) {
377428
this.ngControl.valueAccessor = this;
378429
}

projects/igniteui-angular/src/lib/grids/cell.component.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,8 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT
973973
this.grid.crudService.updateCell(true, event);
974974
}
975975
return;
976+
} else {
977+
this.selectionService.primaryButton = true;
976978
}
977979
this.selectionService.pointerDown(this.selectionNode, event.shiftKey, event.ctrlKey);
978980
this.activate(event);
@@ -1030,11 +1032,16 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT
10301032
*/
10311033
public activate(event: FocusEvent | KeyboardEvent) {
10321034
const node = this.selectionNode;
1033-
const shouldEmitSelection = !this.selectionService.isActiveNode(node);
1034-
1035+
let shouldEmitSelection = !this.selectionService.isActiveNode(node);
1036+
10351037
if (this.selectionService.primaryButton) {
10361038
const currentActive = this.selectionService.activeElement;
1037-
this.selectionService.activeElement = node;
1039+
if (this.cellSelectionMode === GridSelectionMode.single && (event as any)?.ctrlKey && this.selected) {
1040+
this.selectionService.activeElement = null;
1041+
shouldEmitSelection = true;
1042+
} else {
1043+
this.selectionService.activeElement = node;
1044+
}
10381045
const cancel = this._updateCRUDStatus(event);
10391046
if (cancel) {
10401047
this.selectionService.activeElement = currentActive;
@@ -1066,8 +1073,13 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT
10661073
}
10671074
this.selectionService.primaryButton = true;
10681075
if (this.cellSelectionMode === GridSelectionMode.multiple && this.selectionService.activeElement) {
1069-
this.selectionService.add(this.selectionService.activeElement, false); // pointer events handle range generation
1070-
this.selectionService.keyboardStateOnFocus(node, this.grid.rangeSelected, this.nativeElement);
1076+
if (this.selectionService.isInMap(this.selectionService.activeElement) && (event as any)?.ctrlKey && !(event as any)?.shiftKey) {
1077+
this.selectionService.remove(this.selectionService.activeElement);
1078+
shouldEmitSelection = true;
1079+
} else {
1080+
this.selectionService.add(this.selectionService.activeElement, false); // pointer events handle range generation
1081+
this.selectionService.keyboardStateOnFocus(node, this.grid.rangeSelected, this.nativeElement);
1082+
}
10711083
}
10721084
if (this.grid.isCellSelectable && shouldEmitSelection) {
10731085
this.zone.run(() => this.grid.selected.emit({ cell: this.getCellType(), event }));

projects/igniteui-angular/src/lib/grids/grid-base.directive.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4758,8 +4758,8 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements
47584758
this.crudService.endEdit(true);
47594759
this.gridAPI.addRowToData(data);
47604760

4761-
this.rowAddedNotifier.next({ data: data, owner: this });
47624761
this.pipeTrigger++;
4762+
this.rowAddedNotifier.next({ data: data, owner: this });
47634763
this.notifyChanges();
47644764
}
47654765

@@ -5065,6 +5065,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements
50655065
this.crudService.endEdit(true);
50665066
this.selectionService.clearHeaderCBState();
50675067
this.summaryService.clearSummaryCache();
5068+
this.summaryPipeTrigger++;
50685069
this.cdr.detectChanges();
50695070
}
50705071

projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts

+3
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ export class IgxGridNavigationService {
148148
this.grid.cdr.detectChanges();
149149
});
150150
} else {
151+
if (hasLastActiveNode && !this.grid.selectionService.selected(this.lastActiveNode)) {
152+
return;
153+
}
151154
const range = {
152155
rowStart: this.activeNode.row, rowEnd: this.activeNode.row,
153156
columnStart: this.activeNode.column, columnEnd: this.activeNode.column

projects/igniteui-angular/src/lib/grids/grid/grid-cell-selection.spec.ts

+53-7
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,34 @@ describe('IgxGrid - Cell selection #grid', () => {
225225
GridSelectionFunctions.verifySelectedRange(grid, 0, 0, 0, 0, 2, 3);
226226
});
227227

228+
it('Should be able to select cells correctly when focus is returned to the grid', async() => {
229+
const firstCell = grid.gridAPI.get_cell_by_index(1, 'ParentID');
230+
const secondCell = grid.gridAPI.get_cell_by_index(2, 'Name');
231+
232+
UIInteractions.simulateClickAndSelectEvent(firstCell);
233+
fix.detectChanges();
234+
235+
GridSelectionFunctions.verifyCellSelected(firstCell);
236+
expect(grid.selectedCells.length).toBe(1);
237+
238+
UIInteractions.simulateClickAndSelectEvent(firstCell, false, true);
239+
fix.detectChanges();
240+
241+
expect(grid.selectedCells.length).toBe(0);
242+
243+
grid.navigation.lastActiveNode = grid.navigation.activeNode;
244+
grid.navigation.activeNode = null;
245+
fix.detectChanges();
246+
grid.tbody.nativeElement.focus();
247+
fix.detectChanges();
248+
249+
UIInteractions.simulateClickAndSelectEvent(secondCell, false, true);
250+
fix.detectChanges();
251+
GridSelectionFunctions.verifyCellSelected(firstCell, false);
252+
GridSelectionFunctions.verifyCellSelected(secondCell, true);
253+
expect(grid.selectedCells.length).toBe(1);
254+
});
255+
228256
it('Should be able to select range when click on a cell and hold Shift key and click on another Cell', () => {
229257
const firstCell = grid.gridAPI.get_cell_by_index(3, 'HireDate');
230258
const secondCell = grid.gridAPI.get_cell_by_index(1, 'ID');
@@ -341,7 +369,7 @@ describe('IgxGrid - Cell selection #grid', () => {
341369
];
342370
const expectedData2 = [
343371
{ ID: 475, ParentID: 147, Name: 'Michael Langdon' },
344-
{ ID: 957, ParentID: 147 },
372+
{ ID: 957 },
345373
{ ID: 317, ParentID: 147 }
346374
];
347375

@@ -362,18 +390,17 @@ describe('IgxGrid - Cell selection #grid', () => {
362390
GridSelectionFunctions.verifyCellsRegionSelected(grid, 0, 2, 0, 1);
363391
GridSelectionFunctions.verifyCellSelected(thirdCell);
364392

365-
// Click on a cell in the region and verify it is not changed
393+
// Click on a cell in the region and verify it is deselected
366394
let cell = grid.gridAPI.get_cell_by_index(1, 'ParentID');
367395
UIInteractions.simulateClickAndSelectEvent(cell, false, true);
368396
fix.detectChanges();
369397

370-
GridSelectionFunctions.verifyCellsRegionSelected(grid, 0, 2, 0, 1);
371-
GridSelectionFunctions.verifyCellSelected(thirdCell);
398+
GridSelectionFunctions.verifyCellsRegionSelected(grid, 0, 0, 0, 2);
399+
GridSelectionFunctions.verifyCellsRegionSelected(grid, 2, 2, 0, 1);
400+
GridSelectionFunctions.verifyCellSelected(cell, false);
401+
GridSelectionFunctions.verifyCellSelected(grid.gridAPI.get_cell_by_index(1, 'ID'), true);
372402
expect(selectionChangeSpy).toHaveBeenCalledTimes(1);
373403
expect(grid.getSelectedData()).toEqual(expectedData2);
374-
GridSelectionFunctions.verifySelectedRange(grid, 0, 2, 0, 1, 0, 3);
375-
GridSelectionFunctions.verifySelectedRange(grid, 0, 0, 2, 2, 1, 3);
376-
GridSelectionFunctions.verifySelectedRange(grid, 1, 1, 1, 1, 2, 3);
377404

378405
// Click on a cell without holding Ctrl
379406
cell = grid.gridAPI.get_cell_by_index(0, 'ID');
@@ -3155,6 +3182,25 @@ describe('IgxGrid - Cell selection #grid', () => {
31553182
GridSelectionFunctions.verifySelectedRange(grid, 0, 0, 0, 0);
31563183
});
31573184

3185+
it('Should deselect a selected cell with Ctrl + click', () => {
3186+
const selectionChangeSpy = spyOn<any>(grid.selected, 'emit').and.callThrough();
3187+
const firstCell = grid.gridAPI.get_cell_by_index(1, 'ParentID');
3188+
3189+
// Click on a cell
3190+
UIInteractions.simulateClickAndSelectEvent(firstCell);
3191+
fix.detectChanges();
3192+
GridSelectionFunctions.verifyCellSelected(firstCell);
3193+
expect(grid.selectedCells.length).toBe(1);
3194+
expect(selectionChangeSpy).toHaveBeenCalledTimes(1);
3195+
3196+
// Click on same cell holding Ctrl
3197+
UIInteractions.simulateClickAndSelectEvent(firstCell, false, true);
3198+
fix.detectChanges();
3199+
GridSelectionFunctions.verifyCellSelected(firstCell, false);
3200+
expect(grid.selectedCells.length).toBe(0);
3201+
expect(selectionChangeSpy).toHaveBeenCalledTimes(2);
3202+
});
3203+
31583204
it('When when navigate with arrow keys cell selection should be changed', () => {
31593205
const selectionChangeSpy = spyOn<any>(grid.selected, 'emit').and.callThrough();
31603206
let cell = grid.gridAPI.get_cell_by_index(1, 'Name');

projects/igniteui-angular/src/lib/grids/grid/grid-row-editing.spec.ts

+34
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,40 @@ describe('IgxGrid - Row Editing #grid', () => {
851851
expect(GridFunctions.getRowEditingBannerText(fix)).toBe('You have 2 changes in this row and 1 hidden columns');
852852
}));
853853

854+
it(`Should show no row changes when changing the cell value to the original one`, () => {
855+
targetCell = grid.gridAPI.get_cell_by_index(0, 'Downloads');
856+
fix.detectChanges();
857+
858+
const originalValue = targetCell.value;
859+
860+
UIInteractions.simulateDoubleClickAndSelectEvent(targetCell);
861+
fix.detectChanges();
862+
863+
// change first editable cell value
864+
targetCell.editValue = '500';
865+
fix.detectChanges();
866+
867+
// go to next cell
868+
UIInteractions.triggerEventHandlerKeyDown('tab', gridContent);
869+
fix.detectChanges();
870+
871+
expect(GridFunctions.getRowEditingBannerText(fix)).toBe('You have 1 changes in this row and 0 hidden columns');
872+
873+
// return to first editable cell
874+
UIInteractions.triggerEventHandlerKeyDown('tab', gridContent, false, true);
875+
fix.detectChanges();
876+
877+
// change cell value to the original one
878+
targetCell.editValue = originalValue;
879+
fix.detectChanges();
880+
881+
// go to next cell
882+
UIInteractions.triggerEventHandlerKeyDown('tab', gridContent);
883+
fix.detectChanges();
884+
885+
expect(GridFunctions.getRowEditingBannerText(fix)).toBe('You have 0 changes in this row and 0 hidden columns');
886+
});
887+
854888
it(`Should focus last edited cell after click on editable buttons`, (async () => {
855889
targetCell = grid.gridAPI.get_cell_by_index(0, 'Downloads');
856890
UIInteractions.simulateDoubleClickAndSelectEvent(targetCell);

0 commit comments

Comments
 (0)