From e0af43ddea3b48652135ec8abebc0405f9b30605 Mon Sep 17 00:00:00 2001 From: Boris Penkov Date: Tue, 3 Sep 2019 18:56:08 +0300 Subject: [PATCH] feat(grid): row selection templating #4998 --- CHANGELOG.md | 14 + .../src/lib/core/grid-selection.ts | 6 + .../src/lib/grids/grid-base.component.ts | 109 +++-- .../src/lib/grids/grid-common.module.ts | 7 +- .../lib/grids/grid/grid-row-selection.spec.ts | 68 ++- .../lib/grids/grid/grid-row.component.html | 21 +- .../src/lib/grids/grid/grid.component.html | 27 +- .../hierarchical-grid.component.html | 27 +- .../hierarchical-grid.component.ts | 8 + .../hierarchical-grid.integration.spec.ts | 2 +- .../hierarchical-grid.selection.spec.ts | 177 +++++++- .../hierarchical-row.component.html | 22 +- .../hierarchical-row.component.ts | 16 + .../src/lib/grids/igx-row-selectors.module.ts | 31 ++ .../src/lib/grids/row.component.ts | 5 +- .../tree-grid/tree-grid-row.component.html | 17 +- .../tree-grid/tree-grid-selection.spec.ts | 68 ++- .../grids/tree-grid/tree-grid.component.html | 28 +- .../src/lib/test-utils/grid-samples.spec.ts | 40 +- .../src/lib/test-utils/helper-utils.spec.ts | 390 +++++++++++++++++- .../hierarhical-grid-components.spec.ts | 70 +++- .../test-utils/tree-grid-components.spec.ts | 45 ++ 22 files changed, 1123 insertions(+), 75 deletions(-) create mode 100644 projects/igniteui-angular/src/lib/grids/igx-row-selectors.module.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 979564eb691..b1de5bb6a92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,20 @@ For more information about the theming please read our [documentation](https://w - `uniqueColumnValuesStrategy` input is added. This property provides a callback for loading unique column values on demand. If this property is provided, the unique values it generates will be used by the Excel Style Filtering (instead of using the unique values from the data that is bound to the grid). - `igxExcelStyleLoading` directive is added, which can be used to provide a custom loading template for the Excel Style Filtering. If this property is not provided, a default loading template will be used instead. - introduced new properties `cellSelection` and `rowSelection` which accept GridSelection mode enumeration. Grid selection mode could be none, single or multiple. Also `hideRowSelectors` property is added, which allows you to show and hide row selectors when row selection is enabled. + - introduced functionality for templating row and header selectors - [spec](https://github.com/IgniteUI/igniteui-angular/wiki/Row-Selection-Templating-(Grid-feature)) + ```html + + + + + + done_all + + + + + + ``` - `IgxHierarchicalGrid` - Row Islands now emit child grid events with an additional argument - `owner`, which holds reference to the related child grid component instance. - `IgxDrag` diff --git a/projects/igniteui-angular/src/lib/core/grid-selection.ts b/projects/igniteui-angular/src/lib/core/grid-selection.ts index 33bcaabeebd..244b6d42657 100644 --- a/projects/igniteui-angular/src/lib/core/grid-selection.ts +++ b/projects/igniteui-angular/src/lib/core/grid-selection.ts @@ -635,6 +635,12 @@ export class IgxGridSelectionService { return this.rowSelection.size > 0 && filteredData && !this.areAllRowSelected(); } + public get filteredSelectedRowIds(): any[] { + return this.isFilteringApplied() ? + this.getRowIDs(this.allData).filter(rowID => this.isRowSelected(rowID)) : + this.getSelectedRows().filter(rowID => !this.isRowDeleted(rowID)); + } + public emitRowSelectionEvent(newSelection, added, removed, event?): boolean { const currSelection = this.getSelectedRows(); if (this.areEqualCollections(currSelection, newSelection)) { return; } diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.component.ts b/projects/igniteui-angular/src/lib/grids/grid-base.component.ts index ef25e379a40..9c633a8edde 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.component.ts @@ -44,7 +44,6 @@ import { VerticalAlignment, IgxOverlayService } from '../services/index'; -import { IgxCheckboxComponent } from './../checkbox/checkbox.component'; import { GridBaseAPIService } from './api.service'; import { IgxGridCellComponent } from './cell.component'; import { IColumnVisibilityChangedEventArgs } from './column-hiding-item.directive'; @@ -89,8 +88,9 @@ import { import { IgxGridColumnResizerComponent } from './grid-column-resizer.component'; import { IgxGridFilteringRowComponent } from './filtering/grid-filtering-row.component'; import { IgxDragDirective } from '../directives/drag-drop/drag-drop.directive'; -import { DeprecateProperty } from '../core/deprecateDecorators'; import { CharSeparatedValueData } from '../services/csv/char-separated-value-data'; +import { IgxHeadSelectorDirective, IgxRowSelectorDirective } from './igx-row-selectors.module'; +import { DeprecateProperty } from '../core/deprecateDecorators'; const MINIMUM_COLUMN_WIDTH = 136; const FILTER_ROW_HEIGHT = 50; @@ -259,6 +259,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements private _locale = null; public _destroyed = false; private overlayIDs = []; + private _hostWidth; /** * An accessor that sets the resource strings. @@ -1707,9 +1708,51 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements return this.toolbarCustomContentTemplates.first; } + /** + * @hidden + * @internal + */ @ContentChildren(IgxGridToolbarCustomContentDirective, { read: IgxGridToolbarCustomContentDirective, descendants: false }) public toolbarCustomContentTemplates: QueryList; + /** + * @hidden + * @internal + */ + public get headSelectorTemplate(): TemplateRef { + if (this.headSelectorsTemplates && this.headSelectorsTemplates.first) { + return this.headSelectorsTemplates.first.templateRef; + } + + return null; + } + + /** + * @hidden + * @internal + */ + @ContentChildren(IgxHeadSelectorDirective, { read: IgxHeadSelectorDirective, descendants: false }) + public headSelectorsTemplates: QueryList; + + /** + * @hidden + * @internal + */ + public get rowSelectorTemplate(): TemplateRef { + if (this.rowSelectorsTemplates && this.rowSelectorsTemplates.first) { + return this.rowSelectorsTemplates.first.templateRef; + } + + return null; + } + + /** + * @hidden + * @internal + */ + @ContentChildren(IgxRowSelectorDirective, { read: IgxRowSelectorDirective, descendants: false }) + public rowSelectorsTemplates: QueryList; + /** * @hidden */ @@ -1743,8 +1786,8 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements /** * @hidden */ - @ViewChild('headerCheckboxContainer', { static: false }) - public headerCheckboxContainer: ElementRef; + @ViewChild('headerSelectorContainer', { static: false }) + public headerSelectorContainer: ElementRef; /** * @hidden @@ -1758,12 +1801,6 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements @ViewChild('headerGroupContainer', { static: false }) public headerGroupContainer: ElementRef; - /** - * @hidden - */ - @ViewChild('headerCheckbox', { read: IgxCheckboxComponent, static: false }) - public headerCheckbox: IgxCheckboxComponent; - /** * @hidden */ @@ -1841,6 +1878,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements */ @ViewChild('defaultRowEditTemplate', { read: TemplateRef, static: true }) private defaultRowEditTemplate: TemplateRef; + /** * @hidden */ @@ -3337,10 +3375,18 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements return totalWidth; } - get showRowCheckboxes(): boolean { + /** + * @hidden + * @internal + */ + get showRowSelectors(): boolean { return this.isRowSelectable && this.hasVisibleColumns && !this.hideRowSelectors; } + /** + * @hidden + * @internal + */ get showDragIcons(): boolean { return this.rowDraggable && this.columns.length > this.hiddenColumnsCount; } @@ -4167,7 +4213,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements const pagingHeight = this.getPagingHeight(); const groupAreaHeight = this.getGroupAreaHeight(); const renderedHeight = toolbarHeight + this.theadRow.nativeElement.offsetHeight + - footerHeight + pagingHeight + groupAreaHeight + + footerHeight + pagingHeight + groupAreaHeight + this.scr.nativeElement.clientHeight; const computed = this.document.defaultView.getComputedStyle(this.nativeElement).getPropertyValue('height'); @@ -4476,7 +4522,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements let width = 0; if (this.isRowSelectable) { - width += this.headerCheckboxContainer ? this.headerCheckboxContainer.nativeElement.getBoundingClientRect().width : 0; + width += this.headerSelectorContainer ? this.headerSelectorContainer.nativeElement.getBoundingClientRect().width : 0; } if (this.rowDraggable) { width += this.headerDragContainer ? this.headerDragContainer.nativeElement.getBoundingClientRect().width : 0; @@ -4685,10 +4731,24 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements /** * @hidden */ - get headerCheckboxAriaLabel() { - return this._filteringExpressionsTree.filteringOperands.length > 0 ? - this.headerCheckbox && this.selectionService.areAllRowSelected() ? 'Deselect all filtered' : 'Select all filtered' : - this.headerCheckbox && this.selectionService.areAllRowSelected() ? 'Deselect all' : 'Select all'; + get headSelectorBaseAriaLabel() { + if (this._filteringExpressionsTree.filteringOperands.length > 0) { + return this.selectionService.areAllRowSelected() ? 'Deselect all filtered' : 'Select all filtered'; + } + + return this.selectionService.areAllRowSelected() ? 'Deselect all' : 'Select all'; + } + + /** + * @hidden + * @internal + */ + public get totalRowsCountAfterFilter() { + if (this.data) { + return this.selectionService.allData.length; + } + + return 0; } /** @@ -4875,7 +4935,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements if (col) { const key = headers ? col.header || col.field : col.field; record[key] = formatters && col.formatter ? col.formatter(source[row][col.field]) - : source[row][col.field]; + : source[row][col.field]; } }); } @@ -4890,15 +4950,15 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements protected getSelectableColumnsAt(index) { if (this.hasColumnLayouts) { const visibleLayoutColumns = this.visibleColumns - .filter(col => col.columnLayout) - .sort((a, b) => a.visibleIndex - b.visibleIndex); + .filter(col => col.columnLayout) + .sort((a, b) => a.visibleIndex - b.visibleIndex); const colLayout = visibleLayoutColumns[index]; return colLayout ? colLayout.children.toArray() : []; } else { const visibleColumns = this.visibleColumns - .filter(col => !col.columnGroup) - .sort((a, b) => a.visibleIndex - b.visibleIndex); - return [ visibleColumns[index] ]; + .filter(col => !col.columnGroup) + .sort((a, b) => a.visibleIndex - b.visibleIndex); + return [visibleColumns[index]]; } } @@ -4913,7 +4973,6 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements return this.extractDataFromSelection(source, formatters, headers); } - /** * @hidden */ @@ -4944,7 +5003,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.onGridCopy.emit(ev); if (ev.cancel) { - return; + return; } const transformer = new CharSeparatedValueData(ev.data, this.clipboardOptions.separator); diff --git a/projects/igniteui-angular/src/lib/grids/grid-common.module.ts b/projects/igniteui-angular/src/lib/grids/grid-common.module.ts index 3b0ef657634..52e388356f8 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-common.module.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-common.module.ts @@ -67,6 +67,7 @@ import { IgxGridExcelStyleFilteringModule } from './filtering/excel-style/grid.e import { IgxGridDragSelectDirective } from './drag-select.directive'; import { IgxGridColumnResizerComponent } from './grid-column-resizer.component'; import { IgxRowDragModule } from './row-drag.directive'; +import { IgxRowSelectorsModule } from './igx-row-selectors.module'; /** * @hidden */ @@ -164,7 +165,8 @@ import { IgxRowDragModule } from './row-drag.directive'; IgxFilterCellTemplateDirective, IgxRowDragModule, IgxPaginatorModule, - IgxGridFooterComponent + IgxGridFooterComponent, + IgxRowSelectorsModule ], imports: [ CommonModule, @@ -193,7 +195,8 @@ import { IgxRowDragModule } from './row-drag.directive'; IgxGridPipesModule, IgxGridExcelStyleFilteringModule, IgxRowDragModule, - IgxPaginatorModule + IgxPaginatorModule, + IgxRowSelectorsModule ], providers: [ IgxGridSelectionService, diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row-selection.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-row-selection.spec.ts index 0235bafb712..59880782ca5 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row-selection.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row-selection.spec.ts @@ -12,11 +12,12 @@ import { SelectionWithScrollsComponent, SingleRowSelectionComponent, RowSelectionWithoutPrimaryKeyComponent, - SelectionWithTransactionsComponent + SelectionWithTransactionsComponent, + GridCustomSelectorsComponent } from '../../test-utils/grid-samples.spec'; -import { IgxHierarchicalGridModule } from '../hierarchical-grid/hierarchical-grid.module'; import { GridFunctions, GridSelectionFunctions } from '../../test-utils/grid-functions.spec'; import { SampleTestData } from '../../test-utils/sample-test-data.spec'; +import { IgxRowSelectorsModule } from '../igx-row-selectors.module'; const DEBOUNCETIME = 30; @@ -31,12 +32,13 @@ describe('IgxGrid - Row Selection #grid', () => { SelectionWithScrollsComponent, RowSelectionWithoutPrimaryKeyComponent, SingleRowSelectionComponent, - SelectionWithTransactionsComponent + SelectionWithTransactionsComponent, + GridCustomSelectorsComponent, ], imports: [ NoopAnimationsModule, IgxGridModule, - IgxHierarchicalGridModule + IgxRowSelectorsModule ] }) .compileComponents(); @@ -1871,4 +1873,62 @@ describe('IgxGrid - Row Selection #grid', () => { GridSelectionFunctions.verifyRowSelected(addedRow); })); }); + + describe('Custom selectors', () => { + let fix; + let grid; + + beforeEach(fakeAsync(() => { + fix = TestBed.createComponent(GridCustomSelectorsComponent); + fix.detectChanges(); + grid = fix.componentInstance.grid; + grid.rowSelection = GridSelectionMode.multiple; + })); + + it('Should have the correct properties in the custom row selector template', () => { + const firstRow = grid.getRowByIndex(0); + const firstCheckbox = firstRow.nativeElement.querySelector('.igx-checkbox__composite'); + const context = { index: 0, rowID: 'ALFKI', selected: false }; + const contextUnselect = { index: 0, rowID: 'ALFKI', selected: true }; + spyOn(fix.componentInstance, 'onRowCheckboxClick').and.callThrough(); + firstCheckbox.click(); + fix.detectChanges(); + + expect(fix.componentInstance.onRowCheckboxClick).toHaveBeenCalledTimes(1); + expect(fix.componentInstance.onRowCheckboxClick).toHaveBeenCalledWith(new MouseEvent('click'), context); + + // Verify correct properties when unselecting a row + firstCheckbox.click(); + fix.detectChanges(); + + expect(fix.componentInstance.onRowCheckboxClick).toHaveBeenCalledTimes(2); + expect(fix.componentInstance.onRowCheckboxClick).toHaveBeenCalledWith(new MouseEvent('click'), contextUnselect); + }); + + it('Should have the correct properties in the custom row selector header template', () => { + const context = { selectedCount: 0, totalCount: 27 }; + const contextUnselect = { selectedCount: 27, totalCount: 27 }; + const headerCheckbox = fix.nativeElement.querySelector('.igx-grid__thead').querySelector('.igx-checkbox__composite'); + spyOn(fix.componentInstance, 'onHeaderCheckboxClick').and.callThrough(); + headerCheckbox.click(); + fix.detectChanges(); + + expect(fix.componentInstance.onHeaderCheckboxClick).toHaveBeenCalledTimes(1); + expect(fix.componentInstance.onHeaderCheckboxClick).toHaveBeenCalledWith(new MouseEvent('click'), context); + + headerCheckbox.click(); + fix.detectChanges(); + + expect(fix.componentInstance.onHeaderCheckboxClick).toHaveBeenCalledTimes(2); + expect(fix.componentInstance.onHeaderCheckboxClick).toHaveBeenCalledWith(new MouseEvent('click'), contextUnselect); + }); + + it('Should have correct indices on all pages', () => { + grid.nextPage(); + fix.detectChanges(); + + const firstRootRow = grid.getRowByIndex(0); + expect(firstRootRow.nativeElement.querySelector('.rowNumber').textContent).toEqual('30'); + }); + }); }); diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index 58c296c29a4..91564690f4c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -6,9 +6,15 @@ - +
- + +
@@ -104,3 +110,14 @@
+ + + + + diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html index 1704ae78f20..cc3e5ffaee9 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -58,17 +58,22 @@ }" #headerDragContainer>
+ *ngTemplateOutlet="this.dragIndicatorIconTemplate ? this.dragIndicatorIconTemplate : dragIndicatorIconBase">
- -
+
- + +
@@ -237,5 +242,17 @@ drag_indicator + + + + +
diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html index c1d1855b109..4546352898d 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html @@ -30,12 +30,19 @@
- -
+
- + +
@@ -203,5 +210,17 @@ drag_indicator + + + + +
diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts index 34437f2087a..bbfe0629d42 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts @@ -372,6 +372,14 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseCompone this.toolbarCustomContentTemplates = this.parentIsland ? this.parentIsland.toolbarCustomContentTemplates : this.toolbarCustomContentTemplates; + + this.headSelectorsTemplates = this.parentIsland ? + this.parentIsland.headSelectorsTemplates : + this.headSelectorsTemplates; + + this.rowSelectorsTemplates = this.parentIsland ? + this.parentIsland.rowSelectorsTemplates : + this.rowSelectorsTemplates; } private updateSizes() { diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.integration.spec.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.integration.spec.ts index cb00da86e90..c6bc8d5ef10 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.integration.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.integration.spec.ts @@ -473,7 +473,7 @@ describe('IgxHierarchicalGrid Integration #hGrid', () => { fixture.detectChanges(); const rootExpander = (hierarchicalGrid.dataRowList.toArray()[0] as IgxHierarchicalRowComponent).expander; - const rootCheckbox = hierarchicalGrid.headerCheckboxContainer; + const rootCheckbox = hierarchicalGrid.headerSelectorContainer; const rootSummaryRow = hierarchicalGrid.summariesRowList.first.nativeElement; const rootSummaryIndentation = rootSummaryRow.querySelector(SUMMARIES_MARGIN_CLASS); diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.selection.spec.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.selection.spec.ts index 5f3339fe8d3..0ca81c22631 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.selection.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.selection.spec.ts @@ -1,15 +1,20 @@ import { configureTestSuite } from '../../test-utils/configure-suite'; import { async, TestBed, fakeAsync } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { GridSelectionMode, IgxHierarchicalGridModule } from './index'; +import { IgxHierarchicalGridModule, GridSelectionMode } from './index'; import { IgxHierarchicalGridComponent } from './hierarchical-grid.component'; import { wait, UIInteractions } from '../../test-utils/ui-interactions.spec'; import { IgxHierarchicalRowComponent } from './hierarchical-row.component'; import { IgxStringFilteringOperand } from '../../data-operations/filtering-condition'; import { IgxIconModule } from '../../icon'; -import { IgxHierarchicalGridTestBaseComponent, - IgxHierarchicalGridRowSelectionComponent, - IgxHierarchicalGridRowSelectionNoTransactionsComponent } from '../../test-utils/hierarhical-grid-components.spec'; +import { + IgxHierarchicalGridTestBaseComponent, + IgxHierarchicalGridRowSelectionComponent, + IgxHierarchicalGridCustomSelectorsComponent, + IgxHierarchicalGridRowSelectionNoTransactionsComponent +} from '../../test-utils/hierarhical-grid-components.spec'; +import { IgxRowSelectorsModule } from '../igx-row-selectors.module'; +import { HelperUtils } from '../../test-utils/helper-utils.spec'; import { GridSelectionFunctions } from '../../test-utils/grid-functions.spec'; describe('IgxHierarchicalGrid selection #hGrid', () => { @@ -24,10 +29,14 @@ describe('IgxHierarchicalGrid selection #hGrid', () => { declarations: [ IgxHierarchicalGridTestBaseComponent, IgxHierarchicalGridRowSelectionComponent, + IgxHierarchicalGridCustomSelectorsComponent, IgxHierarchicalGridRowSelectionNoTransactionsComponent ], imports: [ - NoopAnimationsModule, IgxHierarchicalGridModule, IgxIconModule] + NoopAnimationsModule, + IgxHierarchicalGridModule, + IgxIconModule, + IgxRowSelectorsModule] }).compileComponents(); })); @@ -731,4 +740,162 @@ describe('IgxHierarchicalGrid selection #hGrid', () => { GridSelectionFunctions.verifyHeaderRowCheckboxState(childGrid, true); }); }); + + describe('Custom selectors', () => { + let hGrid; + let firstLevelChild; + + beforeEach(fakeAsync(() => { + fix = TestBed.createComponent(IgxHierarchicalGridCustomSelectorsComponent); + fix.detectChanges(); + hGrid = fix.componentInstance.hGrid; + hGrid.rowSelection = GridSelectionMode.multiple; + firstLevelChild = fix.componentInstance.firstLevelChild; + })); + + it('Row context `select` method selects a single row', () => { + // root grid + const firstRootRow = hGrid.getRowByIndex(0); + HelperUtils.rowCheckboxClick(firstRootRow); + fix.detectChanges(); + HelperUtils.verifyRowSelected(hGrid.getRowByIndex(0)); + HelperUtils.verifyHeaderRowCheckboxState(fix, false, true); + + // child grid + HelperUtils.expandRowIsland(2); + fix.detectChanges(); + + const childGrid = hGrid.hgridAPI.getChildGrids(false)[0]; + const childRow = childGrid.getRowByIndex(0); + HelperUtils.rowCheckboxClick(childRow); + fix.detectChanges(); + + HelperUtils.verifyRowSelected(childRow); + HelperUtils.verifyHeaderRowCheckboxState(childGrid, false, true); + }); + + it('Row context `deselect` method deselects an already selected row', () => { + // root grid + const firstRootRow = hGrid.getRowByIndex(1); + HelperUtils.rowCheckboxClick(firstRootRow); + fix.detectChanges(); + HelperUtils.verifyRowSelected(firstRootRow); + HelperUtils.verifyHeaderRowCheckboxState(hGrid, false, true); + + HelperUtils.rowCheckboxClick(firstRootRow); + fix.detectChanges(); + HelperUtils.verifyRowSelected(firstRootRow, false); + HelperUtils.verifyHeaderRowCheckboxState(hGrid, false, false); + + // child grid + HelperUtils.expandRowIsland(2); + fix.detectChanges(); + + const childGrid = hGrid.hgridAPI.getChildGrids(false)[0]; + const childRow = childGrid.getRowByIndex(0); + + HelperUtils.rowCheckboxClick(childRow); + fix.detectChanges(); + HelperUtils.verifyRowSelected(childRow); + HelperUtils.verifyHeaderRowCheckboxState(childGrid, false, true); + + HelperUtils.rowCheckboxClick(childRow); + fix.detectChanges(); + HelperUtils.verifyRowSelected(childRow, false); + HelperUtils.verifyHeaderRowCheckboxState(childGrid, false, false); + }); + + it('Header context `selectAll` method selects all rows', () => { + // root grid + HelperUtils.clickHeaderRowCheckbox(hGrid); + fix.detectChanges(); + HelperUtils.verifyHeaderRowCheckboxState(hGrid, true, false); + expect(hGrid.selectionService.areAllRowSelected()).toBeTruthy(); + + // child grid + HelperUtils.expandRowIsland(2); + fix.detectChanges(); + + const childGrid = hGrid.hgridAPI.getChildGrids(false)[0]; + HelperUtils.clickHeaderRowCheckbox(childGrid); + fix.detectChanges(); + + expect(childGrid.selectionService.areAllRowSelected()).toBeTruthy(); + }); + + it('Header context `deselectAll` method deselects all rows', () => { + // root grid + HelperUtils.clickHeaderRowCheckbox(hGrid); + fix.detectChanges(); + HelperUtils.verifyHeaderRowCheckboxState(hGrid, true, false); + expect(hGrid.selectionService.areAllRowSelected()).toBeTruthy(); + + HelperUtils.clickHeaderRowCheckbox(hGrid); + fix.detectChanges(); + HelperUtils.verifyHeaderRowCheckboxState(hGrid, false, false); + expect(hGrid.selectionService.areAllRowSelected()).toBeFalsy(); + + // child grid + HelperUtils.expandRowIsland(2); + fix.detectChanges(); + + const childGrid = hGrid.hgridAPI.getChildGrids(false)[0]; + + HelperUtils.clickHeaderRowCheckbox(childGrid); + fix.detectChanges(); + HelperUtils.verifyHeaderRowCheckboxState(childGrid, true, false); + expect(childGrid.selectionService.areAllRowSelected()).toBeTruthy(); + + HelperUtils.clickHeaderRowCheckbox(childGrid); + fix.detectChanges(); + HelperUtils.verifyHeaderRowCheckboxState(childGrid, false, false); + expect(childGrid.selectionService.areAllRowSelected()).toBeFalsy(); + }); + + it('Should have the correct properties in the custom row selector header template context', () => { + spyOn(fix.componentInstance, 'handleHeadSelectorClick').and.callThrough(); + + HelperUtils.headerCheckboxClick(hGrid); + fix.detectChanges(); + + expect(fix.componentInstance.handleHeadSelectorClick).toHaveBeenCalledWith(new MouseEvent('click'), { + selectedCount: 0, + totalCount: hGrid.data.length, + selectAll: jasmine.anything(), + deselectAll: jasmine.anything() + }); + }); + + it('Should have the correct properties in the custom row selector template context', () => { + spyOn(fix.componentInstance, 'handleRowSelectorClick').and.callThrough(); + + HelperUtils.rowCheckboxClick(hGrid.getRowByIndex(1)); + fix.detectChanges(); + + expect(fix.componentInstance.handleRowSelectorClick).toHaveBeenCalledWith(new MouseEvent('click'), { + index: 1, + rowID: '1', + selected: false, + select: jasmine.anything(), + deselect: jasmine.anything() + }); + }); + + it('Should have correct indices on all pages', () => { + // root grid + hGrid.nextPage(); + fix.detectChanges(); + expect(hGrid.getRowByIndex(0).nativeElement.querySelector('.rowNumber').textContent).toEqual('15'); + + // child grid + HelperUtils.expandRowIsland(3); + fix.detectChanges(); + + const childGrid = hGrid.hgridAPI.getChildGrids(false)[0]; + + childGrid.nextPage(); + fix.detectChanges(); + expect(childGrid.getRowByIndex(2).nativeElement.querySelector('.rowNumberChild').textContent).toEqual('17'); + }); + }); }); diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html index 58ed0210a26..b38f3c43677 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html @@ -7,9 +7,17 @@
- +
- + +
@@ -56,3 +64,13 @@ + + + + diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.ts index 6c2fa439748..20679cb3f06 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.ts +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.ts @@ -90,6 +90,22 @@ export class IgxHierarchicalRowComponent extends IgxRowComponent { + this.grid.selectRows([this.rowID]); + } + + /** + * @hidden + * @internal + */ + public deselect = () => { + this.grid.deselectRows([this.rowID]); + } + private endEdit(grid: IgxHierarchicalGridComponent) { if (grid.crudService.inEditMode) { grid.endEdit(); diff --git a/projects/igniteui-angular/src/lib/grids/igx-row-selectors.module.ts b/projects/igniteui-angular/src/lib/grids/igx-row-selectors.module.ts new file mode 100644 index 00000000000..d7d8ac0cc56 --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/igx-row-selectors.module.ts @@ -0,0 +1,31 @@ +import { Directive, NgModule, TemplateRef } from '@angular/core'; + +/** + * @hidden + */ +@Directive({ + selector: '[igxRowSelector]' +}) +export class IgxRowSelectorDirective { + constructor(public templateRef: TemplateRef) { } +} + +/** + * @hidden + */ +@Directive({ + selector: '[igxHeadSelector]' +}) +export class IgxHeadSelectorDirective { + constructor(public templateRef: TemplateRef) { } +} + +/** + * @hidden + */ +@NgModule({ + declarations: [IgxRowSelectorDirective, IgxHeadSelectorDirective], + exports: [IgxRowSelectorDirective, IgxHeadSelectorDirective] +}) +export class IgxRowSelectorsModule { +} diff --git a/projects/igniteui-angular/src/lib/grids/row.component.ts b/projects/igniteui-angular/src/lib/grids/row.component.ts index 5cb1f2164d0..9244f4e1e18 100644 --- a/projects/igniteui-angular/src/lib/grids/row.component.ts +++ b/projects/igniteui-angular/src/lib/grids/row.component.ts @@ -148,8 +148,8 @@ export class IgxRowComponent /** * @hidden */ - public get showRowCheckboxes(): boolean { - return this.grid.showRowCheckboxes; + public get showRowSelectors(): boolean { + return this.grid.showRowSelectors; } /** @hidden */ @@ -270,7 +270,6 @@ export class IgxRowComponent public element: ElementRef, public cdr: ChangeDetectorRef) { } - /** * @hidden * @internal diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html index 0cb896f6ac5..203d599b93c 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html @@ -3,9 +3,12 @@
- +
- + +
@@ -106,3 +109,13 @@ + + + + diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-selection.spec.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-selection.spec.ts index 7f0e9465a82..864fabf8906 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-selection.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-selection.spec.ts @@ -8,7 +8,8 @@ import { IgxTreeGridCellSelectionComponent, IgxTreeGridSelectionRowEditingComponent, IgxTreeGridSelectionWithTransactionComponent, - IgxTreeGridRowEditingTransactionComponent + IgxTreeGridRowEditingTransactionComponent, + IgxTreeGridCustomRowSelectorsComponent } from '../../test-utils/tree-grid-components.spec'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { @@ -20,6 +21,7 @@ import { import { IgxStringFilteringOperand, IgxNumberFilteringOperand } from '../../data-operations/filtering-condition'; import { configureTestSuite } from '../../test-utils/configure-suite'; import { wait, UIInteractions } from '../../test-utils/ui-interactions.spec'; +import { IgxRowSelectorsModule } from '../igx-row-selectors.module'; import { GridFunctions } from '../../test-utils/grid-functions.spec'; describe('IgxTreeGrid - Selection #tGrid', () => { @@ -34,9 +36,10 @@ describe('IgxTreeGrid - Selection #tGrid', () => { IgxTreeGridCellSelectionComponent, IgxTreeGridSelectionRowEditingComponent, IgxTreeGridSelectionWithTransactionComponent, - IgxTreeGridRowEditingTransactionComponent + IgxTreeGridRowEditingTransactionComponent, + IgxTreeGridCustomRowSelectorsComponent ], - imports: [IgxTreeGridModule, NoopAnimationsModule] + imports: [IgxTreeGridModule, NoopAnimationsModule, IgxRowSelectorsModule] }) .compileComponents(); })); @@ -609,7 +612,7 @@ describe('IgxTreeGrid - Selection #tGrid', () => { treeGrid.selectRows([475]); fix.detectChanges(); expect(treeGrid.selectedRows()).toEqual([663, 475]); - TreeGridFunctions.verifyHeaderCheckboxSelection(fix, null); + TreeGridFunctions.verifyHeaderCheckboxSelection(fix, false); }); it('Should not be able to select deleted rows through API with selectAllRows - Hierarchical DS', () => { @@ -654,7 +657,7 @@ describe('IgxTreeGrid - Selection #tGrid', () => { treeGrid.selectRows([9]); fix.detectChanges(); expect(treeGrid.selectedRows()).toEqual([6, 9]); - TreeGridFunctions.verifyHeaderCheckboxSelection(fix, null); + TreeGridFunctions.verifyHeaderCheckboxSelection(fix, false); }); it('Should not be able to select deleted rows through API with selectAllRows', () => { @@ -929,8 +932,63 @@ describe('IgxTreeGrid - Selection #tGrid', () => { expect(banner[0]).toBeTruthy(); })); }); + + describe('Custom row selectors', () => { + beforeEach(fakeAsync(() => { + fix = TestBed.createComponent(IgxTreeGridCustomRowSelectorsComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid; + })); + + it('Should have the correct properties in the custom row selector template', () => { + const firstRow = treeGrid.getRowByIndex(0); + const firstCheckbox = firstRow.nativeElement.querySelector('.igx-checkbox__composite'); + const context = { index: 0, rowID: 1, selected: false }; + const contextUnselect = { index: 0, rowID: 1, selected: true }; + spyOn(fix.componentInstance, 'onRowCheckboxClick').and.callThrough(); + firstCheckbox.click(); + fix.detectChanges(); + + expect(fix.componentInstance.onRowCheckboxClick).toHaveBeenCalledTimes(1); + expect(fix.componentInstance.onRowCheckboxClick).toHaveBeenCalledWith(new MouseEvent('click'), context); + + // Verify correct properties when unselecting a row + firstCheckbox.click(); + fix.detectChanges(); + + expect(fix.componentInstance.onRowCheckboxClick).toHaveBeenCalledTimes(2); + expect(fix.componentInstance.onRowCheckboxClick).toHaveBeenCalledWith(new MouseEvent('click'), contextUnselect); + }); + + it('Should have the correct properties in the custom row selector header template', () => { + const context = { selectedCount: 0, totalCount: 8 }; + const contextUnselect = { selectedCount: 8, totalCount: 8 }; + const headerCheckbox = fix.nativeElement.querySelector('.igx-grid__thead').querySelector('.igx-checkbox__composite'); + spyOn(fix.componentInstance, 'onHeaderCheckboxClick').and.callThrough(); + headerCheckbox.click(); + fix.detectChanges(); + + expect(fix.componentInstance.onHeaderCheckboxClick).toHaveBeenCalledTimes(1); + expect(fix.componentInstance.onHeaderCheckboxClick).toHaveBeenCalledWith(new MouseEvent('click'), context); + + headerCheckbox.click(); + fix.detectChanges(); + + expect(fix.componentInstance.onHeaderCheckboxClick).toHaveBeenCalledTimes(2); + expect(fix.componentInstance.onHeaderCheckboxClick).toHaveBeenCalledWith(new MouseEvent('click'), contextUnselect); + }); + + it('Should have correct indices on all pages', () => { + treeGrid.nextPage(); + fix.detectChanges(); + + const firstRootRow = treeGrid.getRowByIndex(0); + expect(firstRootRow.nativeElement.querySelector('.rowNumber').textContent).toEqual('5'); + }); + }); }); + function getVisibleSelectedRows(fix) { return TreeGridFunctions.getAllRows(fix).filter( (row) => row.nativeElement.classList.contains(TREE_ROW_SELECTION_CSS_CLASS)); diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html index d8bc1db4251..75a738a9b98 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html @@ -20,12 +20,16 @@ - -
- + +
+ +
@@ -187,5 +191,17 @@ drag_indicator + + + + +
diff --git a/projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts b/projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts index 0c582ad0a17..58e80753343 100644 --- a/projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts @@ -1,4 +1,4 @@ -import { Component, TemplateRef, ViewChild, Input, AfterViewInit, ChangeDetectorRef, QueryList, ViewChildren } from '@angular/core'; +import { Component, TemplateRef, ViewChild, Input, AfterViewInit, ChangeDetectorRef, QueryList, ViewChildren, OnInit } from '@angular/core'; import { IgxGridCellComponent } from '../grids/cell.component'; import { IgxDateSummaryOperand, IgxNumberSummaryOperand, IgxSummaryResult } from '../grids/summaries/grid-summary'; import { IGridCellEventArgs, IGridEditEventArgs, IgxGridTransaction } from '../grids/grid-base.component'; @@ -1171,6 +1171,44 @@ export class DynamicColumnsComponent extends GridWithSizeComponent { height = '800px'; } +@Component({ + template: ` + + + + + + + + {{ rowContext.index }} + + + + + + ` +}) +export class GridCustomSelectorsComponent extends BasicGridComponent implements OnInit { + @ViewChild('gridCustomSelectors', { static: true }) + public grid: IgxGridComponent; + public ngOnInit(): void { + this.data = SampleTestData.contactInfoDataFull(); + } + + public onRowCheckboxClick(event, rowContext) { + event.stopPropagation(); + event.preventDefault(); + rowContext.selected ? this.grid.deselectRows([rowContext.rowID]) : this.grid.selectRows([rowContext.rowID]); + } + + public onHeaderCheckboxClick(event, headContext) { + event.stopPropagation(); + event.preventDefault(); + headContext.selected ? this.grid.deselectAllRows() : this.grid.selectAllRows(); + } +} + @Component({ template: ` { @@ -134,4 +149,377 @@ export class HelperUtils { document.documentElement.scrollTop = 0; document.documentElement.scrollLeft = 0; } + + public static navigateVerticallyToIndex = ( + grid: IgxGridComponent, + rowStartIndex: number, + rowEndIndex: number, + colIndex?: number, + shift = false) => new Promise(async (resolve, reject) => { + const dir = rowStartIndex > rowEndIndex ? 'ArrowUp' : 'ArrowDown'; + const row = grid.getRowByIndex(rowStartIndex); + const cIndx = colIndex || 0; + const colKey = grid.columnList.toArray()[cIndx].field; + const nextIndex = dir === 'ArrowUp' ? rowStartIndex - 1 : rowStartIndex + 1; + let elem; + if (row) { + elem = row instanceof IgxGridGroupByRowComponent ? + row : grid.getCellByColumn(row.index, colKey); + } else { + const summariRow = grid.summariesRowList.find(s => s.index === rowStartIndex); + if (summariRow) { + elem = summariRow.summaryCells.find(cell => cell.visibleColumnIndex === cIndx); + } + } + + if (rowStartIndex === rowEndIndex) { + resolve(); + return; + } + + UIInteractions.triggerKeyDownWithBlur(dir, elem.nativeElement, true, false, shift); + + await wait(40); + HelperUtils.navigateVerticallyToIndex(grid, nextIndex, rowEndIndex, colIndex, shift) + .then(() => { resolve(); }); + }) + + public static navigateHorizontallyToIndex = ( + grid: IgxGridComponent, + cell: IgxGridCellComponent, + index: number, + shift = false) => new Promise(async (resolve) => { + // grid - the grid in which to navigate. + // cell - current cell from which the navigation will start. + // index - the index to which to navigate + // shift - if the Shift key should be pressed on keydown event + + const currIndex = cell.visibleColumnIndex; + const dir = currIndex < index ? 'ArrowRight' : 'ArrowLeft'; + const nextIndex = dir === 'ArrowRight' ? currIndex + 1 : currIndex - 1; + const visibleColumns = grid.visibleColumns.sort((c1, c2) => c1.visibleIndex - c2.visibleIndex); + const nextCol = visibleColumns[nextIndex]; + let nextCell = nextCol ? grid.getCellByColumn(cell.rowIndex, nextCol.field) : null; + + // if index reached return + if (currIndex === index) { resolve(); return; } + // else call arrow up/down + UIInteractions.triggerKeyDownWithBlur(dir, cell.nativeElement, true, false, shift); + + grid.cdr.detectChanges(); + // if next row exists navigate next + if (nextCell) { + await wait(10); + grid.cdr.detectChanges(); + HelperUtils.navigateHorizontallyToIndex(grid, nextCell, index, shift).then(() => { resolve(); }); + } else { + // else wait for chunk to load. + grid.parentVirtDir.onChunkLoad.pipe(take(1)).subscribe({ + next: () => { + grid.cdr.detectChanges(); + nextCell = nextCol ? grid.getCellByColumn(cell.rowIndex, nextCol.field) : null; + HelperUtils.navigateHorizontallyToIndex(grid, nextCell, index, shift).then(() => { resolve(); }); + } + }); + } + }) + + public static expandCollapceGroupRow = + (fix, groupRow: IgxGridGroupByRowComponent, + cell: IgxGridCellComponent) => new Promise(async (resolve, reject) => { + expect(groupRow.focused).toBe(true); + expect(groupRow.nativeElement.classList.contains('igx-grid__group-row--active')).toBe(true); + if (cell != null) { + expect(cell.selected).toBe(true); + } + + groupRow.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'arrowleft', code: 'arrowleft', altKey: true })); + await wait(300); + fix.detectChanges(); + + expect(groupRow.expanded).toBe(false); + expect(groupRow.focused).toBe(true); + expect(groupRow.nativeElement.classList.contains('igx-grid__group-row--active')).toBe(true); + if (cell != null) { + expect(cell.selected).toBe(true); + } + + groupRow.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'arrowright', code: 'arrowright', altKey: true })); + await wait(100); + fix.detectChanges(); + + expect(groupRow.expanded).toBe(true); + expect(groupRow.focused).toBe(true); + expect(groupRow.nativeElement.classList.contains('igx-grid__group-row--active')).toBe(true); + if (cell != null) { + expect(cell.selected).toBe(true); + } + resolve(); + }) + + public static verifyColumnSummariesBySummaryRowIndex(fix, rowIndex: number, summaryIndex: number, summaryLabels, summaryResults) { + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, rowIndex); + HelperUtils.verifyColumnSummaries(summaryRow, summaryIndex, summaryLabels, summaryResults); + } + + public static verifyColumnSummaries(summaryRow: DebugElement, summaryIndex: number, summaryLabels, summaryResults) { + // const summary = summaryRow.query(By.css('igx-grid-summary-cell[data-visibleindex="' + summaryIndex + '"]')); + const summary = HelperUtils.getSummaryCellByVisibleIndex(summaryRow, summaryIndex); + expect(summary).toBeDefined(); + const summaryItems = summary.queryAll(By.css('.igx-grid-summary__item')); + if (summaryLabels.length === 0) { + expect(summary.nativeElement.classList.contains('igx-grid-summary--empty')).toBeTruthy(); + expect(summaryItems.length).toBe(0); + } else { + expect(summary.nativeElement.classList.contains('igx-grid-summary--empty')).toBeFalsy(); + expect(summaryItems.length).toEqual(summaryLabels.length); + if (summaryItems.length === summaryLabels.length) { + for (let i = 0; i < summaryLabels.length; i++) { + const summaryItem = summaryItems[i]; + const summaryLabel = summaryItem.query(By.css('.igx-grid-summary__label')); + expect(summaryLabels[i]).toEqual(summaryLabel.nativeElement.textContent.trim()); + if (summaryResults.length > 0) { + const summaryResult = summaryItem.query(By.css('.igx-grid-summary__result')); + expect(summaryResults[i]).toEqual(summaryResult.nativeElement.textContent.trim()); + } + } + } + } + } + + public static getSummaryRowByDataRowIndex(fix, rowIndex: number) { + return fix.debugElement.query(By.css('igx-grid-summary-row[data-rowindex="' + rowIndex + '"]')); + } + + public static getSummaryCellByVisibleIndex(summaryRow: DebugElement, summaryIndex: number) { + return summaryRow.query(By.css('igx-grid-summary-cell[data-visibleindex="' + summaryIndex + '"]')); + } + + public static getAllVisibleSummariesLength(fix) { + return HelperUtils.getAllVisibleSummaries(fix).length; + } + + public static getAllVisibleSummariesRowIndexes(fix) { + const summaries = HelperUtils.getAllVisibleSummaries(fix); + const rowIndexes = []; + summaries.forEach(summary => { + rowIndexes.push(Number(summary.attributes['data-rowIndex'])); + }); + return rowIndexes.sort((a: number, b: number) => a - b); + } + + public static getAllVisibleSummaries(fix) { + return fix.debugElement.queryAll(By.css('igx-grid-summary-row')); + } + + public static verifyVisibleSummariesHeight(fix, summariesRows, rowHeight = 36) { + const visibleSummaries = HelperUtils.getAllVisibleSummaries(fix); + visibleSummaries.forEach(summary => { + expect(summary.nativeElement.getBoundingClientRect().height).toBeGreaterThanOrEqual(summariesRows * rowHeight - 1); + expect(summary.nativeElement.getBoundingClientRect().height).toBeLessThanOrEqual(summariesRows * rowHeight + 1); + }); + } + + public static verifySummaryCellActive(fix, rowIndex, cellIndex, active: boolean = true) { + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, rowIndex); + const summ = HelperUtils.getSummaryCellByVisibleIndex(summaryRow, cellIndex); + const hasClass = summ.nativeElement.classList.contains(CELL_ACTIVE_CSS_CLASS); + expect(hasClass === active).toBeTruthy(); + } + + public static moveSummaryCell = + (fix, rowIndex, cellIndex, key, shift = false, ctrl = false) => new Promise(async (resolve, reject) => { + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, rowIndex); + const summaryCell = HelperUtils.getSummaryCellByVisibleIndex(summaryRow, cellIndex); + UIInteractions.triggerKeyDownEvtUponElem(key, summaryCell.nativeElement, true, false, shift, ctrl); + await wait(DEBOUNCETIME); + fix.detectChanges(); + resolve(); + }) + + public static focusSummaryCell = + (fix, rowIndex, cellIndex) => new Promise(async (resolve, reject) => { + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, rowIndex); + const summaryCell = HelperUtils.getSummaryCellByVisibleIndex(summaryRow, cellIndex); + summaryCell.nativeElement.dispatchEvent(new Event('focus')); + fix.detectChanges(); + await wait(DEBOUNCETIME); + resolve(); + }) + + public static selectCellsRange = + (fix, startCell, endCell, ctrl = false, shift = false) => new Promise(async (resolve, reject) => { + UIInteractions.simulatePointerOverCellEvent('pointerdown', startCell.nativeElement, shift, ctrl); + startCell.nativeElement.dispatchEvent(new Event('focus')); + fix.detectChanges(); + await wait(); + fix.detectChanges(); + + UIInteractions.simulatePointerOverCellEvent('pointerenter', endCell.nativeElement, shift, ctrl); + UIInteractions.simulatePointerOverCellEvent('pointerup', endCell.nativeElement, shift, ctrl); + await wait(); + fix.detectChanges(); + resolve(); + }) + + public static selectCellsRangeNoWait(fix, startCell, endCell, ctrl = false, shift = false) { + UIInteractions.simulatePointerOverCellEvent('pointerdown', startCell.nativeElement, shift, ctrl); + startCell.nativeElement.dispatchEvent(new Event('focus')); + fix.detectChanges(); + + UIInteractions.simulatePointerOverCellEvent('pointerenter', endCell.nativeElement, shift, ctrl); + UIInteractions.simulatePointerOverCellEvent('pointerup', endCell.nativeElement, shift, ctrl); + fix.detectChanges(); + } + + public static selectCellsRangeWithShiftKey = + (fix, startCell, endCell) => new Promise(async (resolve, reject) => { + UIInteractions.simulateClickAndSelectCellEvent(startCell); + await wait(); + fix.detectChanges(); + + UIInteractions.simulateClickAndSelectCellEvent(endCell, true); + await wait(); + fix.detectChanges(); + resolve(); + }) + + public static selectCellsRangeWithShiftKeyNoWait(fix, startCell, endCell) { + UIInteractions.simulateClickAndSelectCellEvent(startCell); + fix.detectChanges(); + + UIInteractions.simulateClickAndSelectCellEvent(endCell, true); + fix.detectChanges(); + } + + public static verifyCellsRegionSelected(grid, startRowIndex, endRowIndex, startColumnIndex, endColumnIndex, selected = true) { + const startRow = startRowIndex < endRowIndex ? startRowIndex : endRowIndex; + const endRow = startRowIndex < endRowIndex ? endRowIndex : startRowIndex; + const startCol = startColumnIndex < endColumnIndex ? startColumnIndex : endColumnIndex; + const endCol = startColumnIndex < endColumnIndex ? endColumnIndex : startColumnIndex; + for (let i = startCol; i <= endCol; i++) { + for (let j = startRow; j <= endRow; j++) { + const cell = grid.getCellByColumn(j, grid.columnList.find(col => col.visibleIndex === i).field); + if (cell) { + HelperUtils.verifyCellSelected(cell, selected); + } + } + } + } + + public static verifySelectedRange(grid, rowStart, rowEnd, columnStart, columnEnd, rangeIndex = 0, selectedRanges = 1) { + const range = grid.getSelectedRanges(); + expect(range).toBeDefined(); + expect(range.length).toBe(selectedRanges); + expect(range[rangeIndex].columnStart).toBe(columnStart); + expect(range[rangeIndex].columnEnd).toBe(columnEnd); + expect(range[rangeIndex].rowStart).toBe(rowStart); + expect(range[rangeIndex].rowEnd).toBe(rowEnd); + } + + public static verifyCellSelected(cell, selected = true) { + expect(cell.selected).toBe(selected); + expect(cell.nativeElement.classList.contains(CELL_SELECTED_CSS_CLASS)).toBe(selected); + } + + public static verifyRowSelected(row, selected = true, hasCheckbox = true) { + expect(row.selected).toBe(selected); + expect(row.nativeElement.classList.contains(ROW_SELECTION_CSS_CLASS)).toBe(selected); + if (hasCheckbox) { + HelperUtils.verifyRowHasCheckbox(row.nativeElement); + expect(HelperUtils.getRowCheckboxInput(row.nativeElement).checked).toBe(selected); + } + } + + public static verifyRowsArraySelected(rows, selected = true, hasCheckbox = true) { + rows.forEach(row => { + HelperUtils.verifyRowSelected(row, selected, hasCheckbox); + }); + } + + public static verifyHeaderRowCheckboxState(parent, checked = false, indeterminate = false) { + const header = HelperUtils.getHeaderRow(parent); + const headerCheckboxElement = HelperUtils.getRowCheckboxInput(header); + expect(headerCheckboxElement.checked).toBe(checked); + expect(headerCheckboxElement.indeterminate).toBe(indeterminate); + } + + public static verifyHeaderAndRowCheckBoxesAlignment(grid) { + const headerDiv = HelperUtils.getRowCheckboxDiv(HelperUtils.getHeaderRow(grid)); + const firstRowDiv = HelperUtils.getRowCheckboxDiv(grid.rowList.first.nativeElement); + const scrollStartElement = grid.nativeElement.querySelector(SCROLL_START_CSS_CLASS); + const hScrollbar = grid.parentVirtDir.getHorizontalScroll(); + + expect(headerDiv.offsetWidth).toEqual(firstRowDiv.offsetWidth); + expect(headerDiv.offsetLeft).toEqual(firstRowDiv.offsetLeft); + if (hScrollbar.scrollWidth) { + expect(scrollStartElement.offsetWidth).toEqual(firstRowDiv.offsetWidth); + expect(hScrollbar.offsetLeft).toEqual(firstRowDiv.offsetWidth); + } + } + + public static verifyRowHasCheckbox(rowDOM, hasCheckbox = true, hasCheckboxDiv = true, verifyHeader = false) { + const checkboxDiv = HelperUtils.getRowCheckboxDiv(rowDOM); + if (!hasCheckbox && !hasCheckboxDiv) { + expect(HelperUtils.getRowCheckboxDiv(rowDOM)).toBeNull(); + } else { + expect(checkboxDiv).toBeDefined(); + const rowCheckbox = HelperUtils.getRowCheckbox(rowDOM); + expect(rowCheckbox).toBeDefined(); + if (!hasCheckbox) { + expect(rowCheckbox.style.visibility).toEqual('hidden'); + } else if (verifyHeader) { + expect(rowCheckbox.style.visibility).toEqual('visible'); + } else { + expect(rowCheckbox.style.visibility).toEqual(''); + } + } + } + + public static verifyHeaderRowHasCheckbox(parent, hasCheckbox = true, hasCheckboxDiv = true) { + HelperUtils.verifyRowHasCheckbox(HelperUtils.getHeaderRow(parent), hasCheckbox, hasCheckboxDiv, true); + } + + public static getHeaderRow(parent): HTMLElement { + return parent.nativeElement.querySelector(HEADER_ROW_CSS_CLASS); + } + + public static getRowCheckboxDiv(rowDOM): HTMLElement { + return rowDOM.querySelector(ROW_DIV_SELECTION_CHECKBOX_CSS_CLASS); + } + + public static getRowCheckboxInput(rowDOM): HTMLInputElement { + return HelperUtils.getRowCheckboxDiv(rowDOM).querySelector(CHECKBOX_INPUT_CSS_CLASS); + } + + public static getRowCheckbox(rowDOM): HTMLElement { + return HelperUtils.getRowCheckboxDiv(rowDOM).querySelector(CHECKBOX_ELEMENT); + } + + public static clickRowCheckbox(row) { + const checkboxElement = HelperUtils.getRowCheckboxDiv(row.nativeElement); + checkboxElement.dispatchEvent(new Event('click', {})); + } + + public static clickHeaderRowCheckbox(parent) { + const checkboxElement = HelperUtils.getRowCheckboxDiv(HelperUtils.getHeaderRow(parent)); + checkboxElement.dispatchEvent(new Event('click', {})); + } + + // select - deselect a checkbox without a handler + public static rowCheckboxClick(row) { + const checkboxElement = row.nativeElement ? + row.nativeElement.querySelector(CHECKBOX_LBL_CSS_CLASS) : + row.querySelector(CHECKBOX_LBL_CSS_CLASS); + checkboxElement.click(); + } + + public static headerCheckboxClick(parent) { + HelperUtils.rowCheckboxClick(HelperUtils.getHeaderRow(parent)); + } + // + + public static expandRowIsland(rowNumber = 1) { + (document.getElementsByClassName(ICON_CSS_CLASS)[rowNumber]).click(); + } } diff --git a/projects/igniteui-angular/src/lib/test-utils/hierarhical-grid-components.spec.ts b/projects/igniteui-angular/src/lib/test-utils/hierarhical-grid-components.spec.ts index 56830ab6b0b..fd3d6c8fcb1 100644 --- a/projects/igniteui-angular/src/lib/test-utils/hierarhical-grid-components.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/hierarhical-grid-components.spec.ts @@ -1,11 +1,6 @@ import { Component, ViewChild, OnInit } from '@angular/core'; -import { IgxTreeGridComponent } from '../grids/tree-grid/tree-grid.component'; import { SampleTestData } from './sample-test-data.spec'; -import { IgxNumberSummaryOperand, IgxSummaryResult, IgxColumnComponent } from '../grids'; -import { IgxGridTransaction } from '../grids/grid-base.component'; -import { IgxTransactionService } from '../services/transaction/igx-transaction'; -import { IgxHierarchicalTransactionService } from '../services/transaction/igx-hierarchical-transaction'; -import { DisplayDensity } from '../core/displayDensity'; +import { IgxColumnComponent } from '../grids'; import { IgxHierarchicalTransactionServiceFactory, IgxHierarchicalGridComponent, IgxRowIslandComponent } from 'igniteui-angular'; @Component({ @@ -90,7 +85,6 @@ export class IgxHierarchicalGridRowSelectionComponent { } } - @Component({ template: ` + + + + + + + + + + + {{ rowContext.index }} + + + + + + + + + + {{ rowContext.index }} + + + + ` +}) +export class IgxHierarchicalGridCustomSelectorsComponent implements OnInit { + public data = []; + + @ViewChild('hGridCustomSelectors', { read: IgxHierarchicalGridComponent, static: true }) + public hGrid: IgxHierarchicalGridComponent; + + @ViewChild('rowIsland1', { read: IgxRowIslandComponent, static: true }) + public firstLevelChild: IgxRowIslandComponent; + + public ngOnInit(): void { + // 2 level hierarchy + this.data = SampleTestData.generateHGridData(40, 2); + } + + public handleHeadSelectorClick(event, headContext) { + event.stopPropagation(); + event.preventDefault(); + headContext.totalCount !== headContext.selectedCount ? headContext.selectAll() : headContext.deselectAll(); + } + + public handleRowSelectorClick(event, rowContext) { + event.stopPropagation(); + event.preventDefault(); + rowContext.selected ? rowContext.deselect() : rowContext.select(); + } +} diff --git a/projects/igniteui-angular/src/lib/test-utils/tree-grid-components.spec.ts b/projects/igniteui-angular/src/lib/test-utils/tree-grid-components.spec.ts index 5fb1ccb1d42..0f29d53dc7e 100644 --- a/projects/igniteui-angular/src/lib/test-utils/tree-grid-components.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/tree-grid-components.spec.ts @@ -729,3 +729,48 @@ export class IgxTreeGridDefaultLoadingComponent implements OnInit { }, 1000); } } + +@Component({ + template: ` + + + + + + + + {{ rowContext.index }} + + + + + + + ` +}) +export class IgxTreeGridCustomRowSelectorsComponent implements OnInit { + @ViewChild(IgxTreeGridComponent, { static: true }) + public treeGrid: IgxTreeGridComponent; + public data = []; + + public ngOnInit(): void { + this.data = SampleTestData.employeePrimaryForeignKeyTreeData(); + } + + public onRowCheckboxClick(event, rowContext) { + event.stopPropagation(); + event.preventDefault(); + rowContext.selected ? this.treeGrid.deselectRows([rowContext.rowID]) : this.treeGrid.selectRows([rowContext.rowID]); + } + + public onHeaderCheckboxClick(event, headContext) { + event.stopPropagation(); + event.preventDefault(); + headContext.selected ? this.treeGrid.deselectAllRows() : this.treeGrid.selectAllRows(); + } +}