diff --git a/CHANGELOG.md b/CHANGELOG.md index a04155ae708..e788efb0fa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes for each version of this project will be documented in this ## 9.1.0 +### General +- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid` + - **Behavioral Change** - When a column is sortable sort indicator is always visible. The column is sorted when click on it. + ### Themes - **Breaking Change** Change the default `$legacy-support` value to false in the `igx-theme` function. @@ -17,6 +21,17 @@ All notable changes for each version of this project will be documented in this ```typescript public pinningConfiguration: IPinningConfig = { columns: ColumnPinningPosition.End }; ``` + - Added functionality for column selection. + - `selected` property has been added to the IgxColumnComponent; Allows you to set whether the column is selected. + - `selectable` property has been added to the IgxColumnComponent; Allows you to set whether the column is selectable. + - `onColumnSelectionChange` event is added for the `IgxGrid`. It is emitted when the column selection is changed. + - `excelStyleSelectingTemplate` property is introduced to IgxGrid, which allows you to set a custom template for the selecting a column in the Excel Style Filter. + - `selectedColumns` API method is added for the `IgxGrid`. It allows to get all selected columns. + - `selectColumns` API method is added for the `IgxGrid`. It allows to select columns by passing array of IgxColumnComponent or column fields. + - `deselectColumns` API method is added for the `IgxGrid`. It allows to deselect columns by passing array of IgxColumnComponent or column fields. + - `deselectAllColumns` API method is added for the `IgxGrid`. It allows to deselect all columns. + - `getSelectedColumnsData` API method is added for the `IgxGrid`. It allows to get the selected columns data. + - `IgxCombo`: - Added `autoFocusSearch` input that allows to manipulate the combo's opening behavior. When the property is `true` (by default), the combo's search input is focused on open. When set to `false`, the focus goes to the combo items container, which can be used to prevent the software keyboard from activating on mobile devices when opening the combo. @@ -24,7 +39,7 @@ All notable changes for each version of this project will be documented in this - `igxSlider` have full right-to-left (RTL) support. ## 9.0.1 -- **Breaking Changes** +- **Breaking Changes** - Remove `$base-color` from igx-typography. The igx-typography class now inherits the parent color. ## 9.0.0 diff --git a/projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts b/projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts index a681d93f00b..e46583edb1f 100644 --- a/projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts +++ b/projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts @@ -69,6 +69,7 @@ export interface IGridResourceStrings { igx_grid_excel_show?: string; igx_grid_excel_pin?: string; igx_grid_excel_unpin?: string; + igx_grid_excel_select?: string; igx_grid_excel_text_filter?: string; igx_grid_excel_number_filter?: string; igx_grid_excel_date_filter?: string; @@ -162,6 +163,7 @@ export const GridResourceStringsEN: IGridResourceStrings = { igx_grid_excel_show: 'Show column', igx_grid_excel_pin: 'Pin column', igx_grid_excel_unpin: 'Unpin column', + igx_grid_excel_select: 'Select column', igx_grid_excel_text_filter: 'Text filter', igx_grid_excel_number_filter: 'Number filter', igx_grid_excel_date_filter: 'Date filter', diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-component.scss index 2d70865c70d..ab4151ae5d5 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-component.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-component.scss @@ -85,6 +85,15 @@ @extend %grid-excel-actions__action !optional; } + @include e(actions-select) { + @extend %grid-excel-actions__action !optional; + } + + @include e(actions-selected) { + @extend %grid-excel-actions__action !optional; + @extend %grid-excel-actions--selected !optional; + } + @include e(actions-filter) { @extend %grid-excel-actions__action !optional; } diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-theme.scss index 4c8eadc8466..c3b8ce59fd1 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-theme.scss @@ -144,6 +144,12 @@ padding: rem(8px) rem(16px); } + %grid-excel-actions--selected { + igx-icon { + color: igx-color($palette, 'secondary'); + } + } + %grid-excel-move { margin-bottom: rem(8px); diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss index ff94ac3e3e6..e63e1533227 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss @@ -233,6 +233,10 @@ @extend %igx-grid__th--sortable !optional; } + @include e(th, $m: selectable) { + @extend %igx-grid__th--selectable !optional; + } + @include e(th, $m: filtrable) { @extend %igx-grid__th--filtrable !optional; } @@ -245,6 +249,10 @@ @extend %igx-grid__th--sorted !optional; } + @include e(th, $m: selected) { + @extend %igx-grid__th--selected !optional; + } + @include e(th, $m: number) { @extend %grid-cell-number !optional; } @@ -301,6 +309,14 @@ @extend %grid-cell--selected !optional; } + @include e(td, $m: column-selected) { + @extend %grid-cell--column-selected !optional; + } + + @include e(td, $mods: (selected, column-selected)) { + @extend %grid-cell--cross-selected !optional; + } + @include e(tr, $mods: (selected, filtered)) { @extend %grid-row--selected--filtered !optional; } @@ -372,6 +388,10 @@ @extend %grid-cell--pinned-selected !optional; } + @include e(td, $mods: (pinned, column-selected)) { + @extend %grid-cell--pinned--column-selected !optional; + } + @include e(td-text) { @extend %grid-cell-text !optional; } diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index 514fccdc8b1..040a5d689fc 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1286,6 +1286,20 @@ } } + %grid-cell--column-selected { + color: --var($theme, 'row-selected-text-color'); + background: --var($theme, 'row-selected-background'); + + &:hover { + background: --var($theme, 'row-selected-hover-background'); + color: --var($theme, 'row-selected-text-color'); + } + } + + %grid-cell--cross-selected { + background: --var($theme, 'row-selected-cell-background'); + } + %igx-grid__td--edited { %grid-cell-text { font-style: italic; @@ -1331,6 +1345,16 @@ // border-bottom: 0; } + %grid-cell--pinned--column-selected { + color: --var($theme, 'row-selected-text-color'); + background: --var($theme, 'row-selected-background'); + + &:hover { + background: --var($theme, 'row-selected-hover-background'); + color: --var($theme, 'row-selected-text-color'); + } + } + %grid-cell--pinned-last { border-#{$right}: map-get($cell-pin, 'style') map-get($cell-pin, 'color') !important; @@ -1447,36 +1471,29 @@ justify-content: normal; } + %igx-grid__th--selectable { + color: --var($theme, 'row-selected-text-color'); + background: --var($theme, 'row-selected-background'); + opacity: .7; + } + + %igx-grid__th--selected { + color: --var($theme, 'row-selected-text-color'); + background: --var($theme, 'row-selected-cell-background'); + } + %igx-grid__th--sortable { - &:hover { + .sort-icon { cursor: pointer; + opacity: .7; - %grid-cell-header-title { - cursor: pointer; + &:hover { opacity: 1; } - - .sort-icon { - opacity: .7; - - &:hover { - opacity: 1; - } - } } } %igx-grid__th--sorted { - %grid-cell-header-title { - opacity: 1; - } - - &:hover{ - .sort-icon { - opacity: 1; - } - } - .sort-icon { opacity: 1; color: --var($theme, 'sorted-header-icon-color'); diff --git a/projects/igniteui-angular/src/lib/grids/README.md b/projects/igniteui-angular/src/lib/grids/README.md index a8d427d3fdd..c845f858d3c 100644 --- a/projects/igniteui-angular/src/lib/grids/README.md +++ b/projects/igniteui-angular/src/lib/grids/README.md @@ -232,6 +232,7 @@ A list of the events emitted by the **igx-grid**: |`onColumnMovingStart`|Emitted when a column moving starts. Returns the moved column object.| |`onSelection`|Emitted when a cell is selected. Returns the cell object.| |`onRowSelectionChange`|Emitted when a row selection has changed. Returns array with old and new selected rows' IDs and the target row, if available.| +|`onColumnSelectionChange`|Emitted when a column selection has changed. Returns array with old and new selected column' fields| |`onColumnInit`|Emitted when the grid columns are initialized. Returns the column object.| |`onSortingDone`|Emitted when sorting is performed through the UI. Returns the sorting expression.| |`onFilteringDone`|Emitted when filtering is performed through the UI. Returns the filtering expressions tree of the column for which the filtering was performed.| @@ -290,6 +291,11 @@ Here is a list of all public methods exposed by **igx-grid**: |`deselectRows(rowIDs: any[])`|Removes the specified row(s) from the grid's selection in the `selectionAPI`.| |`selectAllRows()`|Marks all rows as selected in the grid `selectionAPI`.| |`deselectAllRows()`|Sets the grid's row selection in the `selectionAPI` to `[]`.| +|`selectedColumns()`|Returns array of the currently selected columns| +|`selectColumns(columns: string[] | IgxColumnComponent[], clearCurrentSelection?: boolean)`|Marks the specified columns as selected in the grid `selectionAPI`. `clearCurrentSelection` first empties the grid's selection array.| +|`deselectColumns(columns: string[] | IgxColumnComponent[])`|Removes the specified columns from the grid's selection in the `selectionAPI`.| +|`deselectAllColumns()`|Sets the grid's column selection in the `selectionAPI` to `[]`.| +|`getSelectedColumnsData()`|Gets the the data form current selected columns.| |`findNext(text: string, caseSensitive?: boolean, exactMatch?: boolean)`|Highlights all occurrences of the specified text and marks the next occurrence as active.| |`findPrev(text: string, caseSensitive?: boolean, exactMatch?: boolean)`|Highlights all occurrences of the specified text and marks the previous occurrence as active.| |`clearSearch(text: string, caseSensitive?: boolean)`|Removes all search highlights from the grid.| @@ -324,6 +330,8 @@ Inputs available on the **IgxGridColumnComponent** to define columns: |`hidden`|boolean|Visibility of the column| |`movable`|boolean|Set column to be movable| |`resizable`|boolean|Set column to be resizable| +|`selectable`|boolean|Set column to be selectable| +|`selected`|boolean|Set column to be selected| |`width`|string|Columns width| |`minWidth`|string|Columns minimal width| |`maxWidth`|string|Columns miximum width| diff --git a/projects/igniteui-angular/src/lib/grids/cell.component.ts b/projects/igniteui-angular/src/lib/grids/cell.component.ts index 73fd2e8e9de..3ed25765e3a 100644 --- a/projects/igniteui-angular/src/lib/grids/cell.component.ts +++ b/projects/igniteui-angular/src/lib/grids/cell.component.ts @@ -432,6 +432,11 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy { @Input() width = ''; + @HostBinding('attr.aria-selected') + get ariaSelected() { + return this.selected || this.column.selected || this.row.selected; + } + /** * Gets whether the cell is selected. * ```typescript @@ -439,7 +444,6 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy { * ``` * @memberof IgxGridCellComponent */ - @HostBinding('attr.aria-selected') @HostBinding('class.igx-grid__td--selected') get selected() { return this.selectionService.selected(this.selectionNode); @@ -458,6 +462,18 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy { this.grid.notifyChanges(); } + /** + * Gets whether the cell column is selected. + * ```typescript + * let isCellColumnSelected = this.cell.columnSelected; + * ``` + * @memberof IgxGridCellComponent + */ + @HostBinding('class.igx-grid__td--column-selected') + get columnSelected() { + return this.selectionService.isColumnSelected(this.column.field); + } + @HostBinding('class.igx-grid__td--edited') get dirty() { if (this.grid.rowEditable) { diff --git a/projects/igniteui-angular/src/lib/grids/columns/column-group.component.ts b/projects/igniteui-angular/src/lib/grids/columns/column-group.component.ts index fae82e1c331..7fcb2f38e82 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/column-group.component.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/column-group.component.ts @@ -126,6 +126,19 @@ export class IgxColumnGroupComponent extends IgxColumnComponent implements After */ public set filters(classRef: any) { } + /** + * Returns if the column group is selectable + * ```typescript + * let columnGroupSelectable = this.columnGroup.selectable; + * ``` + * @memberof IgxColumnGroupComponent + */ + public get selectable(): boolean { + return this.children && this.children.some(child => child.selectable); + } + + public set selectable(value: boolean) {} + /** * Returns a reference to the body template. * ```typescript @@ -206,13 +219,40 @@ export class IgxColumnGroupComponent extends IgxColumnComponent implements After if (this._hidden || !this.collapsible) { this.children.forEach(child => child.hidden = this._hidden); } else { - this.children.forEach(c => { - if (c.visibleWhenCollapsed === undefined) {c.hidden = false; return; } + this.children.forEach(c => { + if (c.visibleWhenCollapsed === undefined) { c.hidden = false; return; } c.hidden = this.expanded ? c.visibleWhenCollapsed : !c.visibleWhenCollapsed; }); } } + /** + * Returns if the column group is selected. + * ```typescript + * let isSelected = this.columnGroup.selected; + * ``` + * @memberof IgxColumnGroupComponent + */ + get selected(): boolean { + const selectableChildren = this.allChildren.filter(c => !c.columnGroup && c.selectable && !c.hidden); + return selectableChildren.length > 0 && selectableChildren.every(c => c.selected); + } + + /** + * Enables/Disables the column group selection. + * ```html + * + * ``` + * @memberof IgxColumnGroupComponent + */ + set selected(value: boolean) { + if (this.selectable) { + this.children.forEach(c => { + c.selected = value; + }); + } + } + /** *@hidden */ @@ -299,6 +339,24 @@ export class IgxColumnGroupComponent extends IgxColumnComponent implements After set width(val) { } + /** + *@hidden + */ + public get applySelectableClass(): boolean { + return this._applySelectableClass; + } + + /** + *@hidden + */ + public set applySelectableClass(value: boolean) { + if (this.selectable) { + this._applySelectableClass = value; + this.children.forEach(c => { + c.applySelectableClass = value; + }); + } + } // constructor(public gridAPI: GridBaseAPIService, public cdr: ChangeDetectorRef) { // // D.P. constructor duplication due to es6 compilation, might be obsolete in the future // super(gridAPI, cdr); diff --git a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts index 8468f15ccb1..264fa6cb7b8 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts @@ -44,7 +44,7 @@ import { IgxFilterCellTemplateDirective } from './templates.directive'; import { MRLResizeColumnInfo, MRLColumnSizeInfo } from './interfaces'; -import { ColumnPinningPosition } from '../common/enums'; +import { IgxGridSelectionService } from '../selection/selection.service'; /** * **Ignite UI for Angular Column** - @@ -102,6 +102,31 @@ export class IgxColumnComponent implements AfterContentInit { @WatchColumnChanges() @Input() public sortable = false; + /** + * Returns if the column is selectable. + * ```typescript + * let columnSelectable = this.column.selectable; + * ``` + * @memberof IgxColumnComponent + */ + @WatchColumnChanges() + @Input() + get selectable(): boolean { + return this._selectable; + } + + /** + * Sets if the column is selectable. + * Default value is `true`. + * ```html + * + * ``` + * @memberof IgxColumnComponent + */ + set selectable(value: boolean) { + this._selectable = value; + } + /** * Sets/gets whether the column is groupable. * Default value is `false`. @@ -256,6 +281,33 @@ export class IgxColumnComponent implements AfterContentInit { } } + /** + * Returns if the column is selected. + * ```typescript + * let isSelected = this.column.selected; + * ``` + *@memberof IgxColumnComponent + */ + get selected(): boolean { + return this.grid.selectionService.isColumnSelected(this.field); + } + + /** + * Enables/Disables column selection. + * Default value is `false`. + * ```html + * + * ``` + * @memberof IgxColumnComponent + */ + set selected(value: boolean) { + if (this.selectable && value !== this.selected) { + value ? this.grid.selectionService.selectColumnsWithNoEvent([this.field]) : + this.grid.selectionService.deselectColumnsWithNoEvent([this.field]); + this.grid.notifyChanges(); + } + } + /** *@hidden */ @@ -368,6 +420,10 @@ export class IgxColumnComponent implements AfterContentInit { private _calcWidth = null; public calcPixelWidth: number; + /** + * @hidden + */ + protected _applySelectableClass = false; /** * Sets/gets the maximum `width` of the column. @@ -1224,6 +1280,10 @@ export class IgxColumnComponent implements AfterContentInit { * @hidden */ protected _expanded = true; + /** + * @hidden + */ + protected _selectable = true; /** * @hidden */ @@ -1829,6 +1889,7 @@ export class IgxColumnComponent implements AfterContentInit { return (cols.some(c => c === true) && cols.some(c => c === false)); } + /** *@hidden */ @@ -1840,4 +1901,20 @@ export class IgxColumnComponent implements AfterContentInit { * @hidden */ public populateVisibleIndexes() { } + + /** + *@hidden + */ + public get applySelectableClass(): boolean { + return this._applySelectableClass; + } + + /** + *@hidden + */ + public set applySelectableClass(value: boolean) { + if (this.selectable) { + this._applySelectableClass = value; + } + } } diff --git a/projects/igniteui-angular/src/lib/grids/common/column.interface.ts b/projects/igniteui-angular/src/lib/grids/common/column.interface.ts index 5521af5cd9f..1566340f519 100644 --- a/projects/igniteui-angular/src/lib/grids/common/column.interface.ts +++ b/projects/igniteui-angular/src/lib/grids/common/column.interface.ts @@ -29,6 +29,8 @@ export interface ColumnType { hasSummary: boolean; summaries: any; pinned: boolean; + selected: boolean; + selectable: boolean; level: number; rowStart: number; rowEnd: number; diff --git a/projects/igniteui-angular/src/lib/grids/common/events.ts b/projects/igniteui-angular/src/lib/grids/common/events.ts index 62db0914a37..5d2c35966a8 100644 --- a/projects/igniteui-angular/src/lib/grids/common/events.ts +++ b/projects/igniteui-angular/src/lib/grids/common/events.ts @@ -60,6 +60,14 @@ export interface IRowSelectionEventArgs extends CancelableEventArgs, IBaseEventA event?: Event; } +export interface IColumnSelectionEventArgs extends CancelableEventArgs, IBaseEventArgs { + oldSelection: string[]; + newSelection: string[]; + added: string[]; + removed: string[]; + event?: Event; +} + export interface ISearchInfo { searchText: string; caseSensitive: boolean; diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.html b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.html index 44d1654b360..b7bec015503 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.html +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.html @@ -44,6 +44,15 @@ + +
+ {{grid.resourceStrings.igx_grid_excel_select }} + done +
+
+

{{ column.header || column.field }}

+
+ {{ column.header || column.field }} (onClosed)="onSubMenuClosed()">
+ *ngFor="let condition of conditions" + [value]="condition"> {{ translateCondition(condition) }} diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.ts index 809dfb715a4..9477860a1f9 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.ts @@ -73,6 +73,13 @@ export class IgxExcelStyleHidingTemplateDirective { constructor(public template: TemplateRef) {} } +@Directive({ + selector: '[igxExcelStyleSelecting]' +}) +export class IgxExcelStyleSelectingTemplateDirective { + constructor(public template: TemplateRef) {} +} + @Directive({ selector: '[igxExcelStylePinning]' }) @@ -208,7 +215,7 @@ export class IgxGridExcelStyleFilteringComponent implements OnDestroy { */ get minHeight() { if (!this.inline) { - let minHeight = 600; + let minHeight = 645; switch (this.grid.displayDensity) { case DisplayDensity.cosy: minHeight = 465; break; case DisplayDensity.compact: minHeight = 330; break; @@ -223,7 +230,7 @@ export class IgxGridExcelStyleFilteringComponent implements OnDestroy { */ @HostBinding('style.max-height') get maxHeight() { if (!this.inline) { - let maxHeight = 730; + let maxHeight = 775; switch (this.grid.displayDensity) { case DisplayDensity.cosy: maxHeight = 565; break; case DisplayDensity.compact: maxHeight = 405; break; @@ -287,6 +294,12 @@ export class IgxGridExcelStyleFilteringComponent implements OnDestroy { @ViewChild('defaultExcelStyleHidingTemplate', { read: TemplateRef, static: true }) protected defaultExcelStyleHidingTemplate: TemplateRef; + /** + * @hidden @internal + */ + @ViewChild('defaultExcelStyleSelectingTemplate', { read: TemplateRef, static: true }) + protected defaultExcelStyleSelectingTemplate: TemplateRef; + /** * @hidden @internal */ @@ -368,6 +381,13 @@ export class IgxGridExcelStyleFilteringComponent implements OnDestroy { return this.isColumnPinnable ? 'igx-excel-filter__actions-pin' : 'igx-excel-filter__actions-pin--disabled'; } + /** + * @hidden @internal + */ + public selectedClass() { + return this.column.selected ? 'igx-excel-filter__actions-selected' : 'igx-excel-filter__actions-select'; + } + /** * @hidden @internal */ @@ -411,6 +431,18 @@ export class IgxGridExcelStyleFilteringComponent implements OnDestroy { this.closeDropdown(); } + /** + * @hidden @internal + */ + public onSelect() { + if (!this.column.selected) { + this.grid.selectionService.selectColumn(this.column.field); + } else { + this.grid.selectionService.deselectColumn(this.column.field); + } + this.grid.notifyChanges(); + } + /** * @hidden @internal */ @@ -805,6 +837,17 @@ export class IgxGridExcelStyleFilteringComponent implements OnDestroy { } } + /** + * @hidden @internal + */ + get selectingTemplate() { + if (this.grid.excelStyleSelectingTemplateDirective) { + return this.grid.excelStyleSelectingTemplateDirective.template; + } else { + return this.defaultExcelStyleSelectingTemplate; + } + } + /** * @hidden @internal */ diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.module.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.module.ts index 5c62b9f5a78..60f5b513fad 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.module.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.module.ts @@ -6,7 +6,8 @@ import { IgxExcelStyleSortingTemplateDirective, IgxExcelStyleHidingTemplateDirective, IgxExcelStyleMovingTemplateDirective, - IgxExcelStylePinningTemplateDirective + IgxExcelStylePinningTemplateDirective, + IgxExcelStyleSelectingTemplateDirective } from './grid.excel-style-filtering.component'; import { IgxExcelStyleSortingComponent } from './excel-style-sorting.component'; import { IgxExcelStyleColumnMovingComponent } from './excel-style-column-moving.component'; @@ -48,6 +49,7 @@ import { IgxProgressBarModule } from '../../../progressbar/progressbar.component IgxExcelStyleHidingTemplateDirective, IgxExcelStyleMovingTemplateDirective, IgxExcelStylePinningTemplateDirective, + IgxExcelStyleSelectingTemplateDirective, IgxExcelStyleLoadingValuesTemplateDirective, IgxExcelStyleSearchFilterPipe ], @@ -57,6 +59,7 @@ import { IgxProgressBarModule } from '../../../progressbar/progressbar.component IgxExcelStyleHidingTemplateDirective, IgxExcelStyleMovingTemplateDirective, IgxExcelStylePinningTemplateDirective, + IgxExcelStyleSelectingTemplateDirective, IgxExcelStyleLoadingValuesTemplateDirective, IgxExcelStyleDateExpressionComponent ], diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts index ec80dd58f7f..d0d19f1597c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -93,7 +93,8 @@ import { IgxExcelStyleSortingTemplateDirective, IgxExcelStylePinningTemplateDirective, IgxExcelStyleHidingTemplateDirective, - IgxExcelStyleMovingTemplateDirective + IgxExcelStyleMovingTemplateDirective, + IgxExcelStyleSelectingTemplateDirective } from './filtering/excel-style/grid.excel-style-filtering.component'; import { IgxGridColumnResizerComponent } from './resizing/resizer.component'; import { IgxGridFilteringRowComponent } from './filtering/base/grid-filtering-row.component'; @@ -101,8 +102,10 @@ import { CharSeparatedValueData } from '../services/csv/char-separated-value-dat import { IgxColumnResizingService } from './resizing/resizing.service'; import { DeprecateProperty } from '../core/deprecateDecorators'; import { IFilteringStrategy } from '../data-operations/filtering-strategy'; -import { IgxRowExpandedIndicatorDirective, IgxRowCollapsedIndicatorDirective, - IgxHeaderExpandIndicatorDirective, IgxHeaderCollapseIndicatorDirective } from './grid/grid.directives'; +import { + IgxRowExpandedIndicatorDirective, IgxRowCollapsedIndicatorDirective, + IgxHeaderExpandIndicatorDirective, IgxHeaderCollapseIndicatorDirective +} from './grid/grid.directives'; import { GridKeydownTargetType, GridSelectionMode, @@ -131,6 +134,7 @@ import { ISearchInfo, ICellPosition, IRowToggleEventArgs, + IColumnSelectionEventArgs, IPinRowEventArgs } from './common/events'; import { IgxAdvancedFilteringDialogComponent } from './filtering/advanced-filtering/advanced-filtering-dialog.component'; @@ -142,7 +146,7 @@ import { IgxGridToolbarCustomContentDirective } from './toolbar/toolbar.directiv import { IgxColumnComponent } from './columns/column.component'; import { IgxColumnGroupComponent } from './columns/column-group.component'; import { IGridSortingStrategy } from '../data-operations/sorting-strategy'; -import { IgxRowDragGhostDirective, IgxDragIndicatorIconDirective } from './row-drag.directive'; +import { IgxRowDragGhostDirective, IgxDragIndicatorIconDirective } from './row-drag.directive'; const MINIMUM_COLUMN_WIDTH = 136; const FILTER_ROW_HEIGHT = 50; @@ -1017,8 +1021,8 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements */ @Input() public uniqueColumnValuesStrategy: (column: IgxColumnComponent, - filteringExpressionsTree: IFilteringExpressionsTree, - done: (values: any[]) => void) => void; + filteringExpressionsTree: IFilteringExpressionsTree, + done: (values: any[]) => void) => void; /** * Emitted when `IgxGridCellComponent` is clicked. @@ -1054,6 +1058,16 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements @Output() public onRowSelectionChange = new EventEmitter(); + /** + * Emitted when `IgxColumnComponent` is selected. + * @example + * ```html + * + * ``` + */ + @Output() + public onColumnSelectionChange = new EventEmitter(); + /** * Emitted when `IgxColumnComponent` is pinned. * @remarks @@ -1428,6 +1442,12 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements @ContentChild(IgxExcelStyleHidingTemplateDirective, { read: IgxExcelStyleHidingTemplateDirective }) public excelStyleHidingTemplateDirective: IgxExcelStyleHidingTemplateDirective; + /** + *@hidden @internal + */ + @ContentChild(IgxExcelStyleSelectingTemplateDirective, { read: IgxExcelStyleSelectingTemplateDirective }) + public excelStyleSelectingTemplateDirective: IgxExcelStyleSelectingTemplateDirective; + /** *@hidden @internal */ @@ -1818,26 +1838,26 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements /** * The custom template, if any, that should be used when rendering a row expand indicator. */ - @ContentChild(IgxRowExpandedIndicatorDirective, { read: TemplateRef }) - public rowExpandedIndicatorTemplate: TemplateRef = null; + @ContentChild(IgxRowExpandedIndicatorDirective, { read: TemplateRef }) + public rowExpandedIndicatorTemplate: TemplateRef = null; - /** - * The custom template, if any, that should be used when rendering a row collapse indicator. - */ - @ContentChild(IgxRowCollapsedIndicatorDirective, { read: TemplateRef }) - public rowCollapsedIndicatorTemplate: TemplateRef = null; + /** + * The custom template, if any, that should be used when rendering a row collapse indicator. + */ + @ContentChild(IgxRowCollapsedIndicatorDirective, { read: TemplateRef }) + public rowCollapsedIndicatorTemplate: TemplateRef = null; /** * The custom template, if any, that should be used when rendering a header expand indicator. */ - @ContentChild(IgxHeaderExpandIndicatorDirective, { read: TemplateRef }) - public headerExpandIndicatorTemplate: TemplateRef = null; + @ContentChild(IgxHeaderExpandIndicatorDirective, { read: TemplateRef }) + public headerExpandIndicatorTemplate: TemplateRef = null; - /** - * The custom template, if any, that should be used when rendering a header collapse indicator. - */ - @ContentChild(IgxHeaderCollapseIndicatorDirective, { read: TemplateRef }) - public headerCollapseIndicatorTemplate: TemplateRef = null; + /** + * The custom template, if any, that should be used when rendering a header collapse indicator. + */ + @ContentChild(IgxHeaderCollapseIndicatorDirective, { read: TemplateRef }) + public headerCollapseIndicatorTemplate: TemplateRef = null; /** * @hidden @@ -2274,7 +2294,7 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements return this._cellSelectionMode; } - set cellSelection(selectionMode: GridSelectionMode) { + set cellSelection(selectionMode: GridSelectionMode) { this._cellSelectionMode = selectionMode; if (this.gridAPI.grid) { this.selectionService.clear(true); @@ -2295,7 +2315,7 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements } - set rowSelection(selectionMode: GridSelectionMode) { + set rowSelection(selectionMode: GridSelectionMode) { this._rowSelectionMode = selectionMode; if (this.gridAPI.grid && this.columnList) { this.selectionService.clearAllSelectedRows(); @@ -2477,7 +2497,7 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements protected _allowAdvancedFiltering = false; protected _filterMode = FilterMode.quickFilter; - protected observer: ResizeObserver = new ResizeObserver(() => {}); + protected observer: ResizeObserver = new ResizeObserver(() => { }); protected resizeNotify = new Subject(); @@ -2664,8 +2684,8 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements @Inject(IgxOverlayService) protected overlayService: IgxOverlayService, public summaryService: IgxGridSummaryService, @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions: IDisplayDensityOptions) { - super(_displayDensityOptions); - this.cdr.detach(); + super(_displayDensityOptions); + this.cdr.detach(); } _setupServices() { @@ -2753,8 +2773,8 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements this.verticalScrollContainer.onDataChanging.pipe(destructor, filter(() => !this._init)).subscribe(($event) => { const shouldRecalcSize = this.isPercentHeight && - ( !this.calcHeight || this.calcHeight === this.getDataBasedBodyHeight() || - this.calcHeight === this.renderedRowHeight * this._defaultTargetRecordNumber); + (!this.calcHeight || this.calcHeight === this.getDataBasedBodyHeight() || + this.calcHeight === this.renderedRowHeight * this._defaultTargetRecordNumber); if (shouldRecalcSize) { this.calculateGridHeight(); $event.containerSize = this.calcHeight; @@ -3047,25 +3067,25 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements } } - /** - * Expands all rows. - * @example - * ```typescript - * this.grid.expandAll(); - * ``` - */ + /** + * Expands all rows. + * @example + * ```typescript + * this.grid.expandAll(); + * ``` + */ public expandAll() { this._defaultExpandState = true; this.expansionStates = new Map(); } - /** - * Collapses all rows. - * @example - * ```typescript - * this.grid.collapseAll(); - * ``` - */ + /** + * Collapses all rows. + * @example + * ```typescript + * this.grid.collapseAll(); + * ``` + */ public collapseAll() { this._defaultExpandState = false; this.expansionStates = new Map(); @@ -3520,7 +3540,7 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements * @internal */ get showRowSelectors(): boolean { - return this.isRowSelectable && this.hasVisibleColumns && !this.hideRowSelectors; + return this.isRowSelectable && this.hasVisibleColumns && !this.hideRowSelectors; } /** @@ -4461,9 +4481,9 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements */ protected getFilterCellHeight(): number { const headerGroupNativeEl = (this.headerGroupsList.length !== 0) ? - this.headerGroupsList[0].element.nativeElement : null; + this.headerGroupsList[0].element.nativeElement : null; const filterCellNativeEl = (headerGroupNativeEl) ? - headerGroupNativeEl.querySelector('igx-grid-filtering-cell') : null; + headerGroupNativeEl.querySelector('igx-grid-filtering-cell') : null; return (filterCellNativeEl) ? filterCellNativeEl.offsetHeight : 0; } @@ -4476,8 +4496,8 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements } const actualTheadRow = (!this.allowFiltering || (this.allowFiltering && this.filterMode !== FilterMode.quickFilter)) ? - this.theadRow.nativeElement.offsetHeight - this.getFilterCellHeight() : - this.theadRow.nativeElement.offsetHeight; + this.theadRow.nativeElement.offsetHeight - this.getFilterCellHeight() : + this.theadRow.nativeElement.offsetHeight; const footerHeight = this.summariesHeight || this.tfoot.nativeElement.offsetHeight - this.tfoot.nativeElement.clientHeight; const toolbarHeight = this.getToolbarHeight(); const pagingHeight = this.getPagingHeight(); @@ -4486,10 +4506,10 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements footerHeight + pagingHeight + groupAreaHeight + this.scr.nativeElement.clientHeight; - const computed = this.document.defaultView.getComputedStyle(this.nativeElement).getPropertyValue('height'); let gridHeight = 0; if (this.isPercentHeight) { + const computed = this.document.defaultView.getComputedStyle(this.nativeElement).getPropertyValue('height'); const autoSize = this._shouldAutoSize(renderedHeight); if (autoSize || computed.indexOf('%') !== -1) { const bodyHeight = this.getDataBasedBodyHeight(); @@ -4519,12 +4539,12 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements protected _shouldAutoSize(renderedHeight) { this.tbody.nativeElement.style.display = 'none'; let res = !this.nativeElement.parentElement || - this.nativeElement.parentElement.clientHeight === 0 || - this.nativeElement.parentElement.clientHeight === renderedHeight; + this.nativeElement.parentElement.clientHeight === 0 || + this.nativeElement.parentElement.clientHeight === renderedHeight; if (!isChromium()) { // If grid causes the parent container to extend (for example when container is flex) // we should always auto-size since the actual size of the container will continuously change as the grid renders elements. - res = this.checkContainerSizeChange(); + res = this.checkContainerSizeChange(); } this.tbody.nativeElement.style.display = ''; return res; @@ -4600,8 +4620,8 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements Math.max(computedWidth / columnsToSize, MINIMUM_COLUMN_WIDTH) : Math.max((computedWidth - sumExistingWidths) / columnsToSize, MINIMUM_COLUMN_WIDTH)); - return columnWidth + 'px'; - } + return columnWidth + 'px'; + } /** * @hidden @@ -4609,10 +4629,10 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements */ protected calculateGridWidth() { let width; - const computed = this.document.defaultView.getComputedStyle(this.nativeElement).getPropertyValue('width'); if (this.isPercentWidth) { /* width in %*/ + const computed = this.document.defaultView.getComputedStyle(this.nativeElement).getPropertyValue('width'); width = computed.indexOf('%') === -1 ? parseInt(computed, 10) : null; } else { width = parseInt(this.width, 10); @@ -4638,14 +4658,14 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements private getColumnWidthSum(): number { let colSum = 0; - const cols = this.hasColumnLayouts ? - this.visibleColumns.filter(x => x.columnLayout) : this.visibleColumns.filter(x => !x.columnGroup); + const cols = this.hasColumnLayouts ? + this.visibleColumns.filter(x => x.columnLayout) : this.visibleColumns.filter(x => !x.columnGroup); cols.forEach((item) => { const isWidthInPercent = item.width && typeof item.width === 'string' && item.width.indexOf('%') !== -1; if (isWidthInPercent) { item.width = item.calcWidth || MINIMUM_COLUMN_WIDTH + 'px'; } - colSum += parseInt((item.width || item.defaultWidth), 10) || MINIMUM_COLUMN_WIDTH; + colSum += parseInt((item.width || item.defaultWidth), 10) || MINIMUM_COLUMN_WIDTH; }); if (!colSum) { return null; @@ -4707,7 +4727,7 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements this.gridAPI.clear_filter(record.item.field); // Close filter row - if ( this.filteringService.isFilterRowVisible + if (this.filteringService.isFilterRowVisible && this.filteringService.filteredColumn && this.filteringService.filteredColumn.field === record.item.field) { this.filteringRow.close(); @@ -4715,6 +4735,9 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements // Clear Sorting this.gridAPI.clear_sort(record.item.field); + + // Remove column selection + this.selectionService.deselectColumnsWithNoEvent([record.item.field]); } removed = true; }); @@ -4826,7 +4849,7 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements protected getUnpinnedWidth(takeHidden = false) { let width = this.isPercentWidth ? this.calcWidth : - parseInt(this.width, 10) || parseInt(this.hostWidth, 10) || this.calcWidth; + parseInt(this.width, 10) || parseInt(this.hostWidth, 10) || this.calcWidth; if (this.hasVerticalSroll() && !this.isPercentWidth) { width -= this.scrollWidth; } @@ -4966,8 +4989,8 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements this._pinnedColumns = (this.hasColumnGroups) ? this.columnList.filter((c) => c.pinned) : this.columnList.filter((c) => c.pinned).sort((a, b) => this._pinnedColumns.indexOf(a) - this._pinnedColumns.indexOf(b)); this._unpinnedColumns = this.hasColumnGroups ? this.columnList.filter((c) => !c.pinned) : - this.columnList.filter((c) => !c.pinned) - .sort((a, b) => this._unpinnedColumns.indexOf(a) - this._unpinnedColumns.indexOf(b)); + this.columnList.filter((c) => !c.pinned) + .sort((a, b) => this._unpinnedColumns.indexOf(a) - this._unpinnedColumns.indexOf(b)); } /** @@ -5265,6 +5288,131 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements return this.extractDataFromSelection(source, formatters, headers); } + /** + * Get current selected columns. + * @example + * Returns an array with selected columns + * ```typescript + * const selectedColumns = this.grid.selectedColumns(); + * ``` + */ + public selectedColumns(): IgxColumnComponent[] { + const fields = this.selectionService.getSelectedColumns(); + return fields.map(field => this.getColumnByName(field)).filter(field => field); + } + + /** + * Select specified columns. + * @example + * ```typescript + * this.grid.selectColumns(['ID','Name'], true); + * ``` + * @param columns + * @param clearCurrentSelection if true clears the current selection + */ + public selectColumns(columns: string[] | IgxColumnComponent[], clearCurrentSelection?: boolean) { + let fieldToSelect: string[] = []; + if (columns.length === 0 || typeof columns[0] === 'string') { + fieldToSelect = columns as string[]; + } else { + (columns as IgxColumnComponent[]).forEach(col => { + if (col.columnGroup) { + const children = col.allChildren.filter(c => !c.columnGroup).map(c => c.field); + fieldToSelect = [...fieldToSelect, ...children]; + } else { + fieldToSelect.push(col.field); + } + }); + } + + this.selectionService.selectColumnsWithNoEvent(fieldToSelect, clearCurrentSelection); + this.notifyChanges(); + } + + /** + * Deselect specified columns by filed. + * @example + * ```typescript + * this.grid.deselectColumns(['ID','Name']); + * ``` + * @param columns + */ + public deselectColumns(columns: string[] | IgxColumnComponent[]) { + let fieldToDeselect: string[] = []; + if (columns.length === 0 || typeof columns[0] === 'string') { + fieldToDeselect = columns as string[]; + } else { + (columns as IgxColumnComponent[]).forEach(col => { + if (col.columnGroup) { + const children = col.allChildren.filter(c => !c.columnGroup).map(c => c.field); + fieldToDeselect = [...fieldToDeselect, ...children]; + } else { + fieldToDeselect.push(col.field); + } + }); + } + this.selectionService.deselectColumnsWithNoEvent(fieldToDeselect); + this.notifyChanges(); + } + + /** + * Deselects all columns + * @example + * ```typescript + * this.grid.deselectAllColumns(); + * ``` + */ + public deselectAllColumns() { + this.selectionService.clearAllSelectedColumns(); + this.notifyChanges(); + } + + /** + * Selects all columns + * @example + * ```typescript + * this.grid.deselectAllColumns(); + * ``` + */ + public selectAllColumns() { + this.selectColumns(this.columnList.filter(c => !c.columnGroup)); + } + + protected extractDataFromColumnsSelection(source: any[], formatters = false, headers = false): any[] { + let record = {}; + const selectedData = []; + const selectedColumns = this.selectedColumns(); + if (selectedColumns.length === 0) { + return []; + } + + for (let rowIndex = 0; rowIndex < source.length; rowIndex++) { + selectedColumns.forEach((col) => { + const key = headers ? col.header || col.field : col.field; + record[key] = formatters && col.formatter ? col.formatter(source[rowIndex][col.field]) + : source[rowIndex][col.field]; + }); + + if (Object.keys(record).length) { + selectedData.push(record); + } + record = {}; + } + return selectedData; + } + + /** + * + * Returns an array of the current columns selection in the form of `[{ column.field: cell.value }, ...]`. + * @remarks + * If `formatters` is enabled, the cell value will be formatted by its respective column formatter (if any). + * If `headers` is enabled, it will use the column header (if any) instead of the column field. + */ + public getSelectedColumnsData(formatters = false, headers = false) { + const source = this.filteredSortedData ? this.filteredSortedData : this.data; + return this.extractDataFromColumnsSelection(source, formatters, headers); + } + /** * @hidden @internal */ @@ -5347,7 +5495,7 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements () => { this.navigateTo(rowIndex, visibleColIndex, cb); }); } else if (shouldScrollHorizontally) { this.navigation.performHorizontalScrollToCell(rowIndex, visibleColIndex, false, - () => { this.navigateTo(rowIndex, visibleColIndex, cb); }); + () => { this.navigateTo(rowIndex, visibleColIndex, cb); }); } else { this.executeCallback(rowIndex, visibleColIndex, cb); } @@ -5481,9 +5629,9 @@ export class IgxGridBaseDirective extends DisplayDensityBase implements if (this.document.activeElement && // tslint:disable-next-line:no-bitwise (this.document.activeElement.compareDocumentPosition(this.tbody.nativeElement) & Node.DOCUMENT_POSITION_CONTAINS || - // tslint:disable-next-line:no-bitwise - (this.document.activeElement. - compareDocumentPosition(this.tfoot.nativeElement) & Node.DOCUMENT_POSITION_CONTAINS && isScroll))) { + // tslint:disable-next-line:no-bitwise + (this.document.activeElement. + compareDocumentPosition(this.tfoot.nativeElement) & Node.DOCUMENT_POSITION_CONTAINS && isScroll))) { (this.document.activeElement as HTMLElement).blur(); } } diff --git a/projects/igniteui-angular/src/lib/grids/grid/column-moving.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/column-moving.spec.ts index 4440043391a..b194edfc705 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/column-moving.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/column-moving.spec.ts @@ -18,7 +18,7 @@ import { UIInteractions, wait } from '../../test-utils/ui-interactions.spec'; import { configureTestSuite } from '../../test-utils/configure-suite'; import { IgxGridComponent } from './grid.component'; import { HelperUtils } from '../../test-utils/helper-utils.spec'; -import { GridSelectionFunctions } from '../../test-utils/grid-functions.spec'; +import { GridSelectionFunctions, GridFunctions } from '../../test-utils/grid-functions.spec'; describe('IgxGrid - Column Moving #grid', () => { configureTestSuite(); @@ -168,7 +168,6 @@ describe('IgxGrid - Column Moving #grid', () => { expect(columnsList[1].field).toEqual('ID'); expect(columnsList[2].field).toEqual('LastName'); - // step 2 - verify resizing is not broken const resizeHandle = headers[0].parent.nativeElement.children[2]; UIInteractions.simulateMouseEvent('mousedown', resizeHandle, 200, 80); @@ -183,10 +182,9 @@ describe('IgxGrid - Column Moving #grid', () => { expect(grid.columns[0].width).toEqual('250px'); - // step 3 - verify sorting is not broken - headers[0].triggerEventHandler('click', new Event('click')); - headers[0].triggerEventHandler('click', new Event('click')); + GridFunctions.clickHeaderSortIcon(headers[0]); + GridFunctions.clickHeaderSortIcon(headers[0]); fixture.detectChanges(); expect(grid.getCellByColumn(0, 'ID').value).toEqual(6); diff --git a/projects/igniteui-angular/src/lib/grids/grid/column-resizing.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/column-resizing.spec.ts index e94b49d9152..37dcdef0340 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/column-resizing.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/column-resizing.spec.ts @@ -83,8 +83,8 @@ describe('IgxGrid - Deferred Column Resizing #grid', () => { expect(grid.columns[2].cells[0].value).toEqual('Brown'); - headers[2].nativeElement.dispatchEvent(new Event('click')); - headers[2].nativeElement.dispatchEvent(new Event('click')); + GridFunctions.clickHeaderSortIcon(headers[2]); + GridFunctions.clickHeaderSortIcon(headers[2]); fixture.detectChanges(); expect(grid.columns[2].cells[0].value).toEqual('Wilson'); @@ -266,8 +266,8 @@ describe('IgxGrid - Deferred Column Resizing #grid', () => { expect(grid.columns[2].width).toEqual('250px'); expect(grid.columns[2].cells[0].value).toEqual(254); - headers[2].nativeElement.dispatchEvent(new Event('click')); - headers[2].nativeElement.dispatchEvent(new Event('click')); + GridFunctions.clickHeaderSortIcon(headers[2]); + GridFunctions.clickHeaderSortIcon(headers[2]); fixture.detectChanges(); expect(grid.columns[2].cells[0].value).toEqual(1000); diff --git a/projects/igniteui-angular/src/lib/grids/grid/column-selection.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/column-selection.spec.ts new file mode 100644 index 00000000000..3ed9de1d66f --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/grid/column-selection.spec.ts @@ -0,0 +1,929 @@ +import { async, TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; +import { configureTestSuite } from '../../test-utils/configure-suite'; +import { IgxGridModule } from './grid.module'; +import { IgxGridComponent } from './grid.component'; +import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ProductsComponent, ColumnSelectionGroupTestComponent } from '../../test-utils/grid-samples.spec'; +import { GridSelectionFunctions, GridFunctions } from '../../test-utils/grid-functions.spec'; +import { IgxColumnComponent } from '../columns/column.component'; +import { IColumnSelectionEventArgs } from '../common/events'; +import { IgxStringFilteringOperand } from '../../data-operations/filtering-condition'; +import { ɵɵsetComponentScope } from '@angular/core'; + +const SELECTED_COLUMN_CLASS = 'igx-grid__th--selected'; +const SELECTED_COLUMN_CELL_CLASS = 'igx-grid__td--column-selected'; +function selectedData() { + return [{ ProductID: 1, ProductName: 'Chai' }, + { ProductID: 2, ProductName: 'Aniseed Syrup' }, + { ProductID: 3, ProductName: 'Chef Antons Cajun Seasoning' }, + { ProductID: 4, ProductName: 'Grandmas Boysenberry Spread' }, + { ProductID: 5, ProductName: 'Uncle Bobs Dried Pears' }, + { ProductID: 6, ProductName: 'Northwoods Cranberry Sauce' }, + { ProductID: 7, ProductName: 'Queso Cabrales' }, + { ProductID: 8, ProductName: 'Tofu' }, + { ProductID: 9, ProductName: 'Teatime Chocolate Biscuits' }, + { ProductID: 10, ProductName: 'Chocolate' }]; +} + +describe('IgxGrid - Column Selection #grid', () => { + configureTestSuite(); + let fix: ComponentFixture; + let grid: IgxGridComponent; + + beforeAll(async(() => { + TestBed.configureTestingModule({ + declarations: [ + ProductsComponent, + ColumnSelectionGroupTestComponent + ], + imports: [BrowserAnimationsModule, IgxGridModule, NoopAnimationsModule] + }).compileComponents(); + })); + + describe('Base tests: ', () => { + let colProductName: IgxColumnComponent; + let colProductID: IgxColumnComponent; + let colInStock: IgxColumnComponent; + beforeEach(fakeAsync(/** height/width setter rAF */() => { + fix = TestBed.createComponent(ProductsComponent); + fix.detectChanges(); + grid = fix.componentInstance.grid; + colProductName = grid.getColumnByName('ProductName'); + colProductID = grid.getColumnByName('ProductID'); + colInStock = grid.getColumnByName('InStock'); + })); + + it('setting selected and selectable properties ', () => { + spyOn(grid.onColumnSelectionChange, 'emit').and.callThrough(); + grid.columnList.forEach(column => { + expect(column.selectable).toBeTruthy(); + expect(column.selected).toBeFalsy(); + }); + + let col = grid.getColumnByName('ProductID'); + col.selected = true; + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnAndCellsSelected(col); + + col = grid.getColumnByName('ProductName'); + col.selectable = false; + fix.detectChanges(); + + expect(col.selectable).toBeFalsy(); + + // Verify that when column is not selectable cannot select it. + col.selected = true; + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnAndCellsSelected(col, false); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(0); + }); + + it('selecting a column with mouse click', () => { + spyOn(grid.onColumnSelectionChange, 'emit').and.callThrough(); + colProductName.selectable = false; + fix.detectChanges(); + + GridFunctions.clickColumnHeaderUI('ProductID', fix); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(1); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: [], + newSelection: ['ProductID'], + added: ['ProductID'], + removed: [], + event: jasmine.anything(), + cancel: false + }); + + GridFunctions.clickColumnHeaderUI('ProductName', fix); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName, false); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(1); + + GridFunctions.clickColumnHeaderUI('InStock', fix); + GridSelectionFunctions.verifyColumnAndCellsSelected(colInStock); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID, false); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(2); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: ['ProductID'], + newSelection: ['InStock'], + added: ['InStock'], + removed: ['ProductID'], + event: jasmine.anything(), + cancel: false + }); + + GridFunctions.clickColumnHeaderUI('InStock', fix); + GridSelectionFunctions.verifyColumnAndCellsSelected(colInStock, false); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(3); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: ['InStock'], + newSelection: [], + added: [], + removed: ['InStock'], + event: jasmine.anything(), + cancel: false + }); + }); + + it('selecting a column with ctrl + mouse click', () => { + spyOn(grid.onColumnSelectionChange, 'emit').and.callThrough(); + colProductName.selectable = false; + fix.detectChanges(); + + GridSelectionFunctions.clickOnColumnToSelect(colProductID, true); + grid.cdr.detectChanges(); + + GridSelectionFunctions.verifyColumnSelected(colProductID); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(1); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: [], + newSelection: ['ProductID'], + added: ['ProductID'], + removed: [], + event: jasmine.anything(), + cancel: false + }); + + GridSelectionFunctions.clickOnColumnToSelect(colInStock, true); + grid.cdr.detectChanges(); + + GridSelectionFunctions.verifyColumnSelected(colProductID); + GridSelectionFunctions.verifyColumnSelected(colInStock); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(2); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: ['ProductID'], + newSelection: ['ProductID', 'InStock'], + added: ['InStock'], + removed: [], + event: jasmine.anything(), + cancel: false + }); + + GridSelectionFunctions.clickOnColumnToSelect(colProductName, true); + grid.cdr.detectChanges(); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(2); + + const colOrderDate = grid.getColumnByName('OrderDate'); + GridSelectionFunctions.clickOnColumnToSelect(colOrderDate, true); + grid.cdr.detectChanges(); + + GridSelectionFunctions.verifyColumnSelected(colProductID); + GridSelectionFunctions.verifyColumnSelected(colInStock); + GridSelectionFunctions.verifyColumnSelected(colOrderDate); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(3); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: ['ProductID', 'InStock'], + newSelection: ['ProductID', 'InStock', 'OrderDate'], + added: ['OrderDate'], + removed: [], + event: jasmine.anything(), + cancel: false + }); + + GridSelectionFunctions.clickOnColumnToSelect(colInStock, true); + grid.cdr.detectChanges(); + + GridSelectionFunctions.verifyColumnSelected(colProductID); + GridSelectionFunctions.verifyColumnSelected(colOrderDate); + GridSelectionFunctions.verifyColumnSelected(colInStock, false); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(4); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: ['ProductID', 'InStock', 'OrderDate'], + newSelection: ['ProductID', 'OrderDate'], + added: [], + removed: ['InStock'], + event: jasmine.anything(), + cancel: false + }); + GridSelectionFunctions.clickOnColumnToSelect(colOrderDate); + grid.cdr.detectChanges(); + + GridSelectionFunctions.verifyColumnSelected(colProductID, false); + GridSelectionFunctions.verifyColumnSelected(colOrderDate); + GridSelectionFunctions.verifyColumnSelected(colInStock, false); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(5); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: ['ProductID', 'OrderDate'], + newSelection: ['OrderDate'], + added: [], + removed: ['ProductID'], + event: jasmine.anything(), + cancel: false + }); + }); + + it('selecting a column with shift + mouse click', () => { + spyOn(grid.onColumnSelectionChange, 'emit').and.callThrough(); + const colUnits = grid.getColumnByName('UnitsInStock'); + const colOrderDate = grid.getColumnByName('OrderDate'); + colUnits.selected = true; + colProductName.selectable = false; + fix.detectChanges(); + + GridSelectionFunctions.clickOnColumnToSelect(colInStock, true, true); + grid.cdr.detectChanges(); + + GridSelectionFunctions.verifyColumnSelected(colUnits); + GridSelectionFunctions.verifyColumnSelected(colInStock); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(1); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: ['UnitsInStock'], + newSelection: ['UnitsInStock', 'InStock'], + added: ['InStock'], + removed: [], + event: jasmine.anything(), + cancel: false + }); + + GridSelectionFunctions.clickOnColumnToSelect(colOrderDate, false, true); + grid.cdr.detectChanges(); + + GridSelectionFunctions.verifyColumnSelected(colOrderDate); + GridSelectionFunctions.verifyColumnSelected(colInStock); + GridSelectionFunctions.verifyColumnSelected(colUnits); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(2); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: ['UnitsInStock', 'InStock'], + newSelection: ['UnitsInStock', 'InStock', 'OrderDate'], + added: ['OrderDate'], + removed: [], + event: jasmine.anything(), + cancel: false + }); + + GridSelectionFunctions.clickOnColumnToSelect(colProductID, false, true); + grid.cdr.detectChanges(); + + GridSelectionFunctions.verifyColumnSelected(colOrderDate, false); + GridSelectionFunctions.verifyColumnSelected(colProductName, false); + GridSelectionFunctions.verifyColumnSelected(colProductID); + GridSelectionFunctions.verifyColumnSelected(colInStock); + GridSelectionFunctions.verifyColumnSelected(colUnits); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(3); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledWith({ + oldSelection: ['UnitsInStock', 'InStock', 'OrderDate'], + newSelection: ['UnitsInStock', 'InStock', 'ProductID'], + added: ['ProductID'], + removed: ['OrderDate'], + event: jasmine.anything(), + cancel: false + }); + }); + + it('verify selectable class is applied when hover a column', () => { + colProductName.selectable = false; + fix.detectChanges(); + + const productIDHeader = GridFunctions.getColumnHeader('ProductID', fix); + productIDHeader.triggerEventHandler('pointerenter', null); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnHeaderHasSelectableClass(productIDHeader); + + productIDHeader.triggerEventHandler('pointerleave', null); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnHeaderHasSelectableClass(productIDHeader, false); + + const productNameHeader = GridFunctions.getColumnHeader('ProductName', fix); + productNameHeader.triggerEventHandler('pointerenter', null); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnHeaderHasSelectableClass(productNameHeader, false); + + productNameHeader.triggerEventHandler('pointerleave', null); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnHeaderHasSelectableClass(productIDHeader, false); + }); + + it('verify ARIA support', () => { + colProductName.selected = true; + fix.detectChanges(); + + const productIDHeader = GridFunctions.getColumnHeader('ProductID', fix); + const productNameHeader = GridFunctions.getColumnHeader('ProductName', fix); + + expect(productIDHeader.nativeElement.getAttribute('aria-selected')).toMatch('false'); + expect(productNameHeader.nativeElement.getAttribute('aria-selected')).toMatch('true'); + + colProductName.cells.forEach(cell => { + expect(cell.nativeElement.getAttribute('aria-selected')).toMatch('true'); + }); + colProductID.cells.forEach(cell => { + expect(cell.nativeElement.getAttribute('aria-selected')).toMatch('false'); + }); + }); + + it('verify canceling event onColumnSelectionChange', () => { + GridFunctions.clickColumnHeaderUI('ProductID', fix); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + + grid.onColumnSelectionChange.subscribe((e: IColumnSelectionEventArgs) => { + e.cancel = true; + }); + + // Click on same column to deselect it. + GridFunctions.clickColumnHeaderUI('ProductID', fix); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + + // Click on different column + GridFunctions.clickColumnHeaderUI('ProductName', fix); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName, false); + expect(grid.selectedColumns()).toEqual([colProductID]); + + // Click on different column holding ctrl key + GridFunctions.clickColumnHeaderUI('ProductName', fix, true); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName, false); + expect(grid.selectedColumns()).toEqual([colProductID]); + + // Click on different column holding shift key + GridFunctions.clickColumnHeaderUI('ProductName', fix, false, true); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName, false); + expect(grid.selectedColumns()).toEqual([colProductID]); + }); + + it('verify method selectColumns', () => { + spyOn(grid.onColumnSelectionChange, 'emit').and.callThrough(); + const colUnits = grid.getColumnByName('UnitsInStock'); + const colOrderDate = grid.getColumnByName('OrderDate'); + // select columns with array of fields + grid.selectColumns(['ProductID', 'InStock']); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnsSelected([colProductID, colInStock]); + expect(grid.selectedColumns()).toEqual([colProductID, colInStock]); + + // select columns with with clearCurrentSelection false + grid.selectColumns(['UnitsInStock', 'InStock']); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnsSelected([colProductID, colInStock, colUnits]); + expect(grid.selectedColumns()).toEqual([colProductID, colInStock, colUnits]); + + // select columns with with clearCurrentSelection true + grid.selectColumns(['OrderDate', 'ProductID'], true); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnsSelected([colProductID, colOrderDate]); + expect(grid.selectedColumns()).toEqual([colOrderDate, colProductID]); + + // select columns with array of columns + grid.selectColumns([colInStock, colProductName]); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnsSelected([colProductID, colOrderDate, colInStock, colProductName]); + expect(grid.selectedColumns()).toEqual([colOrderDate, colProductID, colInStock, colProductName]); + + grid.selectColumns([colProductID], true); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnSelected(colProductID); + GridSelectionFunctions.verifyColumnsSelected([colOrderDate, colInStock, colProductName], false); + expect(grid.selectedColumns()).toEqual([colProductID]); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(0); + }); + + it('verify method deselectColumns', () => { + spyOn(grid.onColumnSelectionChange, 'emit').and.callThrough(); + grid.columns.forEach(col => col.selected = true); + + const colUnits = grid.getColumnByName('UnitsInStock'); + const colOrderDate = grid.getColumnByName('OrderDate'); + + // deselect columns with array of fields + grid.deselectColumns(['ProductID']); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnSelected(colProductID, false); + GridSelectionFunctions.verifyColumnsSelected([colProductName, colInStock, colUnits, colOrderDate]); + expect(grid.selectedColumns()).toEqual([colProductName, colInStock, colUnits, colOrderDate]); + + // deselect columns with not existing field + grid.deselectColumns(['testField', 'ProductID', 'UnitsInStock']); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnsSelected([colProductID, colUnits], false); + GridSelectionFunctions.verifyColumnsSelected([colProductName, colInStock, colOrderDate]); + expect(grid.selectedColumns()).toEqual([colProductName, colInStock, colOrderDate]); + + // select columns with array of columns + grid.deselectColumns([colOrderDate, colUnits]); + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsSelected([colProductID, colUnits, colOrderDate], false); + GridSelectionFunctions.verifyColumnsSelected([colProductName, colInStock]); + expect(grid.selectedColumns()).toEqual([colProductName, colInStock]); + }); + + it('verify methods selectAllColumns and deselectAllColumns', () => { + spyOn(grid.onColumnSelectionChange, 'emit').and.callThrough(); + // select all columns + grid.selectAllColumns(); + fix.detectChanges(); + + grid.columnList.forEach(c => { + expect(c.selected).toEqual(true); + }); + + // deselect all columns + grid.deselectAllColumns(); + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsSelected(grid.columns, false); + expect(grid.selectedColumns()).toEqual([]); + + // Set selectable false to a column + colProductName.selectable = false; + fix.detectChanges(); + + // select all columns + grid.selectAllColumns(); + fix.detectChanges(); + + grid.columnList.forEach(c => { + expect(c.selected).toEqual(true); + }); + + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(0); + }); + + it('verify method getSelectedColumnsData', () => { + colProductID.selected = true; + colProductName.selected = true; + fix.detectChanges(); + + expect(grid.getSelectedColumnsData()).toEqual(selectedData()); + }); + }); + + describe('Multi column headers tests: ', () => { + beforeEach(fakeAsync(/** height/width setter rAF */() => { + fix = TestBed.createComponent(ColumnSelectionGroupTestComponent); + fix.detectChanges(); + grid = fix.componentInstance.grid; + })); + + it('setting selected on a column group', () => { + const personDetails = GridFunctions.getColGroup(grid, 'Person Details'); + const genInf = GridFunctions.getColGroup(grid, 'General Information'); + const companyName = grid.getColumnByName('CompanyName'); + const contactName = grid.getColumnByName('ContactName'); + const contactTitle = grid.getColumnByName('ContactTitle'); + spyOn(grid.onColumnSelectionChange, 'emit').and.callThrough(); + + // verify setting selected true on a column group + genInf.selected = true; + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnsSelected([companyName, contactName, contactTitle]); + GridSelectionFunctions.verifyColumnGroupSelected(fix, personDetails); + GridSelectionFunctions.verifyColumnGroupSelected(fix, genInf); + + // verify setting selected false on a column group + personDetails.selected = false; + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsSelected([contactName, contactTitle], false); + GridSelectionFunctions.verifyColumnSelected(companyName); + GridSelectionFunctions.verifyColumnGroupSelected(fix, personDetails, false); + GridSelectionFunctions.verifyColumnGroupSelected(fix, genInf, false); + + contactName.selected = true; + contactTitle.selected = true; + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsSelected([companyName, contactName, contactTitle]); + GridSelectionFunctions.verifyColumnGroupSelected(fix, personDetails); + GridSelectionFunctions.verifyColumnGroupSelected(fix, genInf); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(0); + }); + + it('setting selected on a column group with no selectable children', () => { + const countryInf = GridFunctions.getColGroup(grid, 'Country Information'); + const regInf = GridFunctions.getColGroup(grid, 'Region Information'); + const cityInf = GridFunctions.getColGroup(grid, 'City Information'); + const country = grid.getColumnByName('Country'); + const region = grid.getColumnByName('Region'); + const postalCode = grid.getColumnByName('PostalCode'); + const city = grid.getColumnByName('City'); + const address = grid.getColumnByName('Address'); + spyOn(grid.onColumnSelectionChange, 'emit').and.callThrough(); + + // verify setting selected true on a column group + countryInf.selected = true; + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnsSelected([region, postalCode]); + GridSelectionFunctions.verifyColumnsSelected([country, city, address], false); + GridSelectionFunctions.verifyColumnGroupSelected(fix, countryInf); + GridSelectionFunctions.verifyColumnGroupSelected(fix, regInf); + GridSelectionFunctions.verifyColumnGroupSelected(fix, cityInf, false); + + // Set select to false to a column group + countryInf.selected = false; + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsSelected([country, region, postalCode, city, address], false); + GridSelectionFunctions.verifyColumnGroupSelected(fix, countryInf, false); + GridSelectionFunctions.verifyColumnGroupSelected(fix, regInf, false); + GridSelectionFunctions.verifyColumnGroupSelected(fix, cityInf, false); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(0); + }); + + it('setting selectable false to group children', () => { + const genInf = GridFunctions.getColGroup(grid, 'General Information'); + const personDetails = GridFunctions.getColGroup(grid, 'Person Details'); + const companyName = grid.getColumnByName('CompanyName'); + const contactName = grid.getColumnByName('ContactName'); + const contactTitle = grid.getColumnByName('ContactTitle'); + spyOn(grid.onColumnSelectionChange, 'emit').and.callThrough(); + + // verify setting selected true on a column group + contactName.selectable = false; + contactTitle.selectable = false; + companyName.selectable = false; + fix.detectChanges(); + + expect(personDetails.selectable).toBeFalsy(); + expect(genInf.selectable).toBeFalsy(); + + // Set selected + genInf.selected = true; + fix.detectChanges(); + GridSelectionFunctions.verifyColumnGroupSelected(fix, personDetails, false); + GridSelectionFunctions.verifyColumnGroupSelected(fix, genInf, false); + expect(grid.onColumnSelectionChange.emit).toHaveBeenCalledTimes(0); + }); + + it('verify that when hover group all its selectable children have correct classes', () => { + const contactName = grid.getColumnByName('ContactName'); + const contactTitle = grid.getColumnByName('ContactTitle'); + const genInfHeader = GridFunctions.getColumnGroupHeaderCell('General Information', fix); + const personDetailsHeader = GridFunctions.getColumnGroupHeaderCell('Person Details', fix); + const companyNameHeader = GridFunctions.getColumnHeader('CompanyName', fix); + const contactNameHeader = GridFunctions.getColumnHeader('ContactName', fix); + const contactTitleHeader = GridFunctions.getColumnHeader('ContactTitle', fix); + + genInfHeader.triggerEventHandler('pointerenter', null); + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsHeadersHasSelectableClass([genInfHeader, + personDetailsHeader, companyNameHeader, contactNameHeader, contactTitleHeader]); + + genInfHeader.triggerEventHandler('pointerleave', null); + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsHeadersHasSelectableClass([genInfHeader, + personDetailsHeader, companyNameHeader, contactNameHeader, contactTitleHeader], false); + + contactName.selectable = false; + contactTitle.selectable = false; + fix.detectChanges(); + + genInfHeader.triggerEventHandler('pointerenter', null); + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsHeadersHasSelectableClass([genInfHeader, companyNameHeader]); + GridSelectionFunctions.verifyColumnsHeadersHasSelectableClass([personDetailsHeader, + contactNameHeader, contactTitleHeader], false); + + genInfHeader.triggerEventHandler('pointerleave', null); + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsHeadersHasSelectableClass([genInfHeader, companyNameHeader], false); + + // hover not selectable group + personDetailsHeader.triggerEventHandler('pointerenter', null); + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsHeadersHasSelectableClass([personDetailsHeader, + contactNameHeader, contactTitleHeader, genInfHeader, companyNameHeader], false); + personDetailsHeader.triggerEventHandler('pointerleave', null); + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsHeadersHasSelectableClass([personDetailsHeader, + contactNameHeader, contactTitleHeader, genInfHeader, companyNameHeader], false); + }); + + + it('When click on a col goup all it\'s visible and selectable child should be selected', () => { + const personDetails = GridFunctions.getColGroup(grid, 'Person Details'); + const genInfHeader = GridFunctions.getColGroup(grid, 'General Information'); + const companyName = grid.getColumnByName('CompanyName'); + const contactName = grid.getColumnByName('ContactName'); + const contactTitle = grid.getColumnByName('ContactTitle'); + + contactTitle.hidden = true; + contactName.selectable = false; + fix.detectChanges(); + + GridFunctions.clickColumnGroupHeaderUI('General Information', fix); + + GridSelectionFunctions.verifyColumnSelected(companyName); + GridSelectionFunctions.verifyColumnGroupSelected(fix, genInfHeader); + GridSelectionFunctions.verifyColumnGroupSelected(fix, personDetails, false); + GridSelectionFunctions.verifyColumnsSelected([contactTitle, contactName], false); + + GridFunctions.clickColumnGroupHeaderUI('General Information', fix); + + GridSelectionFunctions.verifyColumnSelected(companyName, false); + GridSelectionFunctions.verifyColumnGroupSelected(fix, genInfHeader, false); + GridSelectionFunctions.verifyColumnGroupSelected(fix, personDetails, false); + GridSelectionFunctions.verifyColumnsSelected([contactTitle, contactName], false); + }); + + it('Should select multiple columns when click and hold ctrl', () => { + const personDetails = GridFunctions.getColGroup(grid, 'Person Details'); + const countryInfo = GridFunctions.getColGroup(grid, 'Country Information'); + const regionInfo = GridFunctions.getColGroup(grid, 'Region Information'); + const contactName = grid.getColumnByName('ContactName'); + const contactTitle = grid.getColumnByName('ContactTitle'); + const region = grid.getColumnByName('Region'); + const postalCode = grid.getColumnByName('PostalCode'); + + GridFunctions.clickColumnGroupHeaderUI('Person Details', fix, true); + GridFunctions.clickColumnGroupHeaderUI('Region Information', fix, true); + + GridSelectionFunctions.verifyColumnGroupSelected(fix, personDetails); + GridSelectionFunctions.verifyColumnGroupSelected(fix, countryInfo); + GridSelectionFunctions.verifyColumnGroupSelected(fix, regionInfo); + GridSelectionFunctions.verifyColumnsSelected([contactTitle, contactName, region, postalCode]); + + GridFunctions.clickColumnHeaderUI('Region', fix); + GridFunctions.clickColumnHeaderUI('PostalCode', fix, true); + + GridSelectionFunctions.verifyColumnGroupSelected(fix, regionInfo); + GridSelectionFunctions.verifyColumnGroupSelected(fix, countryInfo); + GridSelectionFunctions.verifyColumnsSelected([region, postalCode]); + GridSelectionFunctions.verifyColumnGroupSelected(fix, personDetails, false); + GridSelectionFunctions.verifyColumnsSelected([contactTitle, contactName], false); + }); + + it('Should select whole range of columns when click and hold shift', () => { + const countryInfo = GridFunctions.getColGroup(grid, 'Country Information'); + const personDetails = GridFunctions.getColGroup(grid, 'Person Details'); + const regionInfo = GridFunctions.getColGroup(grid, 'Region Information'); + const id = grid.getColumnByName('ID'); + const contactName = grid.getColumnByName('ContactName'); + const contactTitle = grid.getColumnByName('ContactTitle'); + const region = grid.getColumnByName('Region'); + const postalCode = grid.getColumnByName('PostalCode'); + + GridFunctions.clickColumnHeaderUI('ID', fix, false, true); + GridFunctions.clickColumnHeaderUI('PostalCode', fix, false, true); + + GridSelectionFunctions.verifyColumnGroupSelected(fix, countryInfo); + GridSelectionFunctions.verifyColumnGroupSelected(fix, regionInfo); + GridSelectionFunctions.verifyColumnsSelected([region, postalCode, id]); + + GridFunctions.clickColumnHeaderUI('ID', fix); + GridFunctions.clickColumnHeaderUI('ContactName', fix, false, true); + + GridSelectionFunctions.verifyColumnGroupSelected(fix, personDetails); + GridSelectionFunctions.verifyColumnsSelected([contactTitle, contactName, id]); + GridSelectionFunctions.verifyColumnGroupSelected(fix, countryInfo, false); + GridSelectionFunctions.verifyColumnGroupSelected(fix, regionInfo, false); + GridSelectionFunctions.verifyColumnsSelected([region, postalCode], false); + }); + + it('Should select the group when all visible child are selected', () => { + const countryInfo = GridFunctions.getColGroup(grid, 'Country Information'); + const regionInfo = GridFunctions.getColGroup(grid, 'Region Information'); + const region = grid.getColumnByName('Region'); + const country = grid.getColumnByName('Country'); + const postalCode = grid.getColumnByName('PostalCode'); + + GridSelectionFunctions.verifyColumnGroupSelected(fix, countryInfo, false); + GridSelectionFunctions.verifyColumnGroupSelected(fix, regionInfo, false); + GridSelectionFunctions.verifyColumnSelected(region, false); + + country.hidden = true; + country.selectable = true; + postalCode.hidden = true; + fix.detectChanges(); + + GridFunctions.clickColumnHeaderUI('Region', fix); + GridSelectionFunctions.verifyColumnGroupSelected(fix, countryInfo); + GridSelectionFunctions.verifyColumnGroupSelected(fix, regionInfo); + GridSelectionFunctions.verifyColumnSelected(region); + }); + + it('group is selected if all it\'s children are selected', () => { + const regionInfo = GridFunctions.getColGroup(grid, 'Region Information'); + const region = grid.getColumnByName('Region'); + const country = grid.getColumnByName('Country'); + const postalCode = grid.getColumnByName('PostalCode'); + + country.selectable = true; + country.selected = true; + region.selected = true; + postalCode.selected = true; + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnGroupSelected(fix, regionInfo); + + postalCode.selected = false; + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnGroupSelected(fix, regionInfo, false); + }); + + it('when column(s) is/are hidden, selection should not reflect on them', () => { + const postalCode = grid.getColumnByName('PostalCode'); + const region = grid.getColumnByName('Region'); + const contactName = grid.getColumnByName('ContactName'); + const contactTitle = grid.getColumnByName('ContactTitle'); + const personDetails = GridFunctions.getColGroup(grid, 'Person Details'); + const regionInfo = GridFunctions.getColGroup(grid, 'Region Information'); + + postalCode.hidden = true; + contactTitle.hidden = true; + fix.detectChanges(); + + GridFunctions.clickColumnGroupHeaderUI('Person Details', fix, true); + GridFunctions.clickColumnGroupHeaderUI('Region Information', fix, true); + + GridSelectionFunctions.verifyColumnsSelected([postalCode, contactTitle], false); + GridSelectionFunctions.verifyColumnsSelected([contactName, region]); + + grid.deselectAllColumns(); + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsSelected([contactName, region], false); + + personDetails.selected = true; + regionInfo.selected = true; + fix.detectChanges(); + GridSelectionFunctions.verifyColumnsSelected([postalCode, contactTitle, contactName, region]); + }); + }); + + describe('Integration tests: ', () => { + let colProductID; + let colProductName; + beforeEach(fakeAsync(/** height/width setter rAF */() => { + fix = TestBed.createComponent(ProductsComponent); + fix.detectChanges(); + grid = fix.componentInstance.grid; + colProductID = grid.getColumnByName('ProductID'); + colProductName = grid.getColumnByName('ProductName'); + })); + + it('Filtering: Verify column selection when filter row is opened ', fakeAsync(() => { + grid.allowFiltering = true; + const colInStock = grid.getColumnByName('InStock'); + colProductID.selected = true; + fix.detectChanges(); + + GridFunctions.clickFilterCellChipUI(fix, 'ProductName'); // Name column contains nested object as a value + tick(150); + fix.detectChanges(); + + const filterRow = GridFunctions.getFilterRow(fix); + expect(filterRow).toBeDefined(); + + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + + GridFunctions.clickColumnHeaderUI('InStock', fix); + tick(); + + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + GridSelectionFunctions.verifyColumnAndCellsSelected(colInStock, false); + expect(grid.filteringRow.column.field).toEqual('InStock'); + + GridFunctions.clickColumnHeaderUI('ProductID', fix); + tick(); + + const productIDHeader = GridFunctions.getColumnHeader('ProductID', fix); + expect(productIDHeader.nativeElement.classList.contains(SELECTED_COLUMN_CLASS)).toBeFalsy(); + colProductID.cells.forEach(cell => { + expect(cell.nativeElement.classList.contains(SELECTED_COLUMN_CELL_CLASS)).toEqual(true); + }); + expect(grid.filteringRow.column.field).toEqual('ProductID'); + + // Hover column headers + const productNameHeader = GridFunctions.getColumnHeader('ProductName', fix); + productNameHeader.triggerEventHandler('pointerenter', null); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnHeaderHasSelectableClass(productNameHeader, false); + })); + + it('Filtering: Verify column selection when filter', () => { + colProductName.selected = true; + colProductID.selected = true; + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName); + + grid.filter('ProductName', 'Chocolate', IgxStringFilteringOperand.instance().condition('equals'), true); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName); + expect(grid.getSelectedColumnsData()).toEqual([{ ProductID: 10, ProductName: 'Chocolate' }]); + + grid.clearFilter(); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName); + expect(grid.getSelectedColumnsData()).toEqual(selectedData()); + }); + + it('Sorting: Verify column selection is not change when click on sort indicator', () => { + const productIDHeader = GridFunctions.getColumnHeader('ProductID', fix); + colProductName.selected = true; + colProductID.sortable = true; + colProductID.selected = true; + fix.detectChanges(); + + GridFunctions.clickHeaderSortIcon(productIDHeader); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID, true); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName, true); + expect(grid.getSelectedColumnsData()).toEqual(selectedData()); + + GridFunctions.clickHeaderSortIcon(productIDHeader); + fix.detectChanges(); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID, true); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName, true); + expect(grid.getSelectedColumnsData()).toEqual(selectedData().sort((a, b) => b.ProductID - a.ProductID)); + }); + + it('Pinning: Verify that when pin/unpin the column stays selected', () => { + colProductName.selected = true; + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName); + + // pin the column + colProductName.pinned = true; + fix.detectChanges(); + + GridFunctions.verifyColumnIsPinned(colProductName, true, 1); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName); + + colProductName.pinned = false; + fix.detectChanges(); + + GridFunctions.verifyColumnIsPinned(colProductName, false, 0); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName); + }); + + it('Hiding: Verify that when hide/unhide a column the column stays selected', () => { + colProductID.selected = true; + colProductName.selected = true; + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + + // hide the column + colProductName.hidden = true; + fix.detectChanges(); + + GridFunctions.verifyColumnIsHidden(colProductName, true, 4); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + expect(grid.getSelectedColumnsData()).toEqual(selectedData()); + expect(grid.selectedColumns().includes(colProductName)).toBeTruthy(); + + colProductName.hidden = false; + fix.detectChanges(); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + }); + + it('Moving: Verify that when move a column, it stays selected', () => { + colProductID.movable = true; + colProductName.movable = true; + colProductID.selected = true; + fix.detectChanges(); + + grid.moveColumn(colProductID, colProductName); + fix.detectChanges(); + + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName, false); + expect(colProductID.visibleIndex).toEqual(1); + }); + + it('Paging: Verify column stays selected when change page', fakeAsync(() => { + colProductName.selected = true; + colProductID.selected = true; + grid.paging = true; + grid.perPage = 3; + fix.detectChanges(); + tick(30); + + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName); + expect(grid.getSelectedColumnsData()).toEqual(selectedData()); + + grid.paginate(1); + fix.detectChanges(); + tick(16); + + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductID); + GridSelectionFunctions.verifyColumnAndCellsSelected(colProductName); + expect(grid.getSelectedColumnsData()).toEqual(selectedData()); + })); + }); +}); diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts index 7a6261b9070..a2ff0687b07 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts @@ -2669,9 +2669,8 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { expect(grid.filteredData.length).toEqual(1); const excelMenu = GridFunctions.getExcelStyleFilteringComponent(fix); - const checkbox: any[] = Array.from(GridFunctions.getExcelStyleFilteringCheckboxes(fix, excelMenu)); - - expect(checkbox.map(c => c.checked)).toEqual([false, false, false, false, false, false, false ]); + const checkboxes: any[] = Array.from(GridFunctions.getExcelStyleFilteringCheckboxes(fix, excelMenu)); + checkboxes.forEach(c => expect(c.checked).toBeFalsy()); })); it('Should not select values in list if two values with Or operator are entered and contains operand.', fakeAsync(() => { @@ -2690,9 +2689,8 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { expect(grid.filteredData.length).toEqual(2); const excelMenu = GridFunctions.getExcelStyleFilteringComponent(fix); - const checkbox: any[] = Array.from(GridFunctions.getExcelStyleFilteringCheckboxes(fix, excelMenu)); - - expect(checkbox.map(c => c.checked)).toEqual([false, false, false, false, false, false]); + const checkboxes: any[] = Array.from(GridFunctions.getExcelStyleFilteringCheckboxes(fix, excelMenu)); + checkboxes.forEach(c => expect(c.checked).toBeFalsy()); })); it('Should select values in list if two values with Or operator are entered and they are in the list below.', fakeAsync(() => { @@ -2711,10 +2709,11 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { expect(grid.filteredData.length).toEqual(2); const excelMenu = GridFunctions.getExcelStyleFilteringComponent(fix); - const checkbox: any[] = Array.from(GridFunctions.getExcelStyleFilteringCheckboxes(fix, excelMenu)); - - expect(checkbox.map(c => c.checked)).toEqual([true, false, false, true, false, false, true]); - expect(checkbox.map(c => c.indeterminate)).toEqual([true, false, false, false, false, false, false]); + const checkboxes: any[] = Array.from(GridFunctions.getExcelStyleFilteringCheckboxes(fix, excelMenu)); + expect(checkboxes[0].checked && checkboxes[0].indeterminate).toBeTruthy(); + expect(!checkboxes[1].checked && !checkboxes[1].indeterminate).toBeTruthy(); + expect(!checkboxes[2].checked && !checkboxes[2].indeterminate).toBeTruthy(); + expect(checkboxes[3].checked && !checkboxes[3].indeterminate).toBeTruthy(); })); it('Should change filter when changing And/Or operator.', fakeAsync(() => { @@ -2889,8 +2888,7 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { it('Should update filter icon when dialog is closed and the filter has been changed.', fakeAsync(() => { GridFunctions.clickExcelFilterIconFromCode(fix, grid, 'Downloads'); - const excelMenu = GridFunctions.getExcelStyleFilteringComponent(fix); - const checkbox = excelMenu.querySelectorAll('.igx-checkbox__composite'); + const checkbox = GridFunctions.getExcelStyleFilteringCheckboxes(fix); checkbox[0].click(); tick(); @@ -3133,7 +3131,6 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { it('Should enable/disable the apply button correctly.', fakeAsync(() => { GridFunctions.clickExcelFilterIconFromCode(fix, grid, 'ProductName'); - const excelMenu = GridFunctions.getExcelStyleFilteringComponent(fix); // Verify there are filtered-in results and that apply button is enabled. const listItems = GridFunctions.getExcelStyleSearchComponentListItems(fix); let applyButton = GridFunctions.getApplyButtonExcelStyleFiltering(fix) as HTMLElement; @@ -3187,6 +3184,29 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { fix.detectChanges(); })); + it('display dansity is properly applied on the column selection container', fakeAsync(() => { + GridFunctions.clickExcelFilterIconFromCode(fix, grid, 'ProductName'); + tick(100); + fix.detectChanges(); + + let columnSelectionContainer = GridFunctions.getExcelFilteringColumnSelectionContainer(fix); + let headerIcons = GridFunctions.getExcelFilteringHeaderIcons(fix); + + expect(columnSelectionContainer).not.toBeNull(); + expect(headerIcons.length).toEqual(0); + + grid.displayDensity = DisplayDensity.compact; + fix.detectChanges(); + + columnSelectionContainer = GridFunctions.getExcelFilteringColumnSelectionContainer(fix); + headerIcons = GridFunctions.getExcelFilteringHeaderIcons(fix); + const columnSelectionIcon = headerIcons.find((bi: any) => bi.innerText === 'done'); + + expect(columnSelectionContainer.tagName).toEqual('BUTTON'); + expect(columnSelectionIcon).not.toBeNull(); + + })); + it('display density is properly applied on the excel style custom filtering dialog', fakeAsync(() => { const column = grid.columns.find((c) => c.field === 'ProductName'); column.sortable = true; @@ -4169,6 +4189,10 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { expect(excelMenu.querySelector('.esf-custom-pinning')).not.toBeNull(); expect(GridFunctions.getExcelFilteringPinContainer(fix, excelMenu)).toBeNull(); expect(GridFunctions.getExcelFilteringUnpinContainer(fix, excelMenu)).toBeNull(); + + // Verify column selection custom template application. + expect(excelMenu.querySelector('.esf-custom-column-selection')).not.toBeNull(); + expect(GridFunctions.getExcelFilteringColumnSelectionContainer(fix, excelMenu)).toBeNull(); } })); }); @@ -4286,6 +4310,37 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => { fix.detectChanges(); expect(grid.columns[1].hidden).toBeTruthy(); })); + + it('Column selection button should be visible/hidden when column is selectable/not selectable', () => { + let columnSelectionContainer = GridFunctions.getExcelFilteringColumnSelectionContainer(fix); + expect(columnSelectionContainer).toBeNull(); + + const esf = fix.componentInstance.esf; + esf.column = grid.getColumnByName('Downloads'); + fix.detectChanges(); + + columnSelectionContainer = GridFunctions.getExcelFilteringColumnSelectionContainer(fix); + expect(columnSelectionContainer).not.toBeNull(); + }); + + it('should select/deselect column when interact with the column selection item through esf menu', () => { + const column = grid.getColumnByName('Downloads'); + fix.componentInstance.esf.column = column; + fix.detectChanges(); + + GridFunctions.clickColumnSelectionInExcelStyleFiltering(fix); + fix.detectChanges(); + + spyOn(grid.onColumnSelectionChange, 'emit'); + GridSelectionFunctions.verifyColumnAndCellsSelected(column, true); + + GridFunctions.clickColumnSelectionInExcelStyleFiltering(fix); + fix.detectChanges(); + + spyOn(grid.onColumnSelectionChange, 'emit'); + GridSelectionFunctions.verifyColumnAndCellsSelected(column, false); + }); + }); }); @@ -4682,8 +4737,14 @@ function verifyExcelStyleFilterAvailableOptions(fix, labels: string[], checked: const labelElements: any[] = Array.from(GridFunctions.getExcelStyleSearchComponentListItems(fix, excelMenu)); const checkboxElements: any[] = Array.from(GridFunctions.getExcelStyleFilteringCheckboxes(fix, excelMenu)); - expect(labelElements.map(c => c.innerText)).toEqual(labels); - expect(checkboxElements.map(c => c.indeterminate ? null : c.checked)).toEqual(checked); + expect(labelElements.length).toBeGreaterThan(2); + expect(checkboxElements.length).toBeGreaterThan(2); + labels.forEach((l, index) => { + expect(l).toEqual(labelElements[index].innerText); + }); + checked.forEach((c, index) => { + expect(checkboxElements[index].indeterminate ? null : checkboxElements[index].checked).toEqual(c); + }); } function toggleExcelStyleFilteringItems(fix, shouldApply: boolean, ...itemIndices: number[]) { diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.groupby.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.groupby.spec.ts index 1b0b7750c86..f1afba8ce69 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.groupby.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.groupby.spec.ts @@ -16,8 +16,7 @@ import { DefaultSortingStrategy } from '../../data-operations/sorting-strategy'; import { configureTestSuite } from '../../test-utils/configure-suite'; import { DataParent } from '../../test-utils/sample-test-data.spec'; import { MultiColumnHeadersWithGroupingComponent } from '../../test-utils/grid-samples.spec'; -import { resizeObserverIgnoreError, HelperUtils } from '../../test-utils/helper-utils.spec'; -import { GridSelectionFunctions } from '../../test-utils/grid-functions.spec'; +import { GridSelectionFunctions, GridFunctions } from '../../test-utils/grid-functions.spec'; import { GridSelectionMode } from '../common/enums'; import { ControlsFunction } from '../../test-utils/controls-functions.spec'; @@ -28,8 +27,6 @@ describe('IgxGrid - GroupBy #grid', () => { const CELL_CSS_CLASS = '.igx-grid__td'; const SORTING_ICON_ASC_CONTENT = 'arrow_upward'; const SORTING_ICON_DESC_CONTENT = 'arrow_downward'; - const SUMMARY_LABEL_CLASS = '.igx-grid-summary__label'; - const SUMMARY_VALUE_CLASS = '.igx-grid-summary__result'; const DISABLED_CHIP = 'igx-chip--disabled'; const CHIP = 'igx-chip'; @@ -675,23 +672,23 @@ describe('IgxGrid - GroupBy #grid', () => { fix.detectChanges(); const headers = fix.debugElement.queryAll(By.css(COLUMN_HEADER_CLASS)); - // click header - headers[0].triggerEventHandler('click', new Event('click')); + // click header sort icon + GridFunctions.clickHeaderSortIcon(headers[0]); tick(); fix.detectChanges(); const sortingIcon = fix.debugElement.query(By.css('.sort-icon')); expect(sortingIcon.nativeElement.textContent.trim()).toEqual(SORTING_ICON_ASC_CONTENT); - // click header again - headers[0].triggerEventHandler('click', new Event('click')); + // click header sort icon again + GridFunctions.clickHeaderSortIcon(headers[0]); tick(); fix.detectChanges(); expect(sortingIcon.nativeElement.textContent.trim()).toEqual(SORTING_ICON_DESC_CONTENT); - // click header again - headers[0].triggerEventHandler('click', new Event('click')); + // click header sort icon again + GridFunctions.clickHeaderSortIcon(headers[0]); tick(); fix.detectChanges(); expect(sortingIcon.nativeElement.textContent.trim()).toEqual(SORTING_ICON_ASC_CONTENT); diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.master-detail.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.master-detail.spec.ts index b7ce1032706..fe113c38176 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.master-detail.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.master-detail.spec.ts @@ -373,7 +373,7 @@ describe('IgxGrid Master Detail #grid', () => { const targetCellElement = grid.getCellByColumn(4, 'ContactName'); GridFunctions.simulateCellKeydown(targetCellElement, 'ArrowDown'); - await wait(); + await wait(DEBOUNCETIME); fix.detectChanges(); const detailRow = GridFunctions.getMasterRowDetail(row); @@ -468,7 +468,7 @@ describe('IgxGrid Master Detail #grid', () => { expect(document.activeElement).toBe(detailRow); }); - it(`Should focus detail row after hitting Shift+Tab on first cell in next data row and continue to the prev row.`, async() => { + it(`Should focus detail row after hitting Shift+Tab on first cell in next data row and continue to the prev row.`, async() => { const prevRow = grid.getRowByIndex(0) as IgxGridRowComponent; const targetCellElement = grid.getCellByColumn(2, 'ContactName'); @@ -479,7 +479,7 @@ describe('IgxGrid Master Detail #grid', () => { expect(document.activeElement).toBe(detailRow); GridFunctions.simulateDetailKeydown(grid, prevRow, 'Tab', false, true); - await wait(); + await wait(DEBOUNCETIME); fix.detectChanges(); expect(document.activeElement).toBe(grid.getCellByColumn(0, 'CompanyName').nativeElement); diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.sorting.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.sorting.spec.ts index 4a96fc8838c..08f5b68a0cf 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.sorting.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.sorting.spec.ts @@ -240,7 +240,7 @@ describe('IgxGrid - Grid Sorting #grid', () => { it('Should sort grid ascending by clicking once on first header cell UI', () => { const firstHeaderCell = fixture.debugElement.query(By.css('igx-grid-header')); - clickCurrentRow(firstHeaderCell); + GridFunctions.clickHeaderSortIcon(firstHeaderCell); fixture.detectChanges(); const firstRowFirstCell = getCurrentCellFromGrid(grid, 0, 0); @@ -258,12 +258,12 @@ describe('IgxGrid - Grid Sorting #grid', () => { expect(getValueFromCellElement(lastRowSecondCell)).toEqual(expectedResult); }); - it('Should sort grid descending by clicking twice on header cell UI', () => { + it('Should sort grid descending by clicking twice on sort icon UI', () => { const firstHeaderCell = fixture.debugElement.query(By.css('igx-grid-header')); - clickCurrentRow(firstHeaderCell); + GridFunctions.clickHeaderSortIcon(firstHeaderCell); fixture.detectChanges(); - clickCurrentRow(firstHeaderCell); + GridFunctions.clickHeaderSortIcon(firstHeaderCell); fixture.detectChanges(); const firstRowFirstCell = getCurrentCellFromGrid(grid, 0, 0); @@ -281,15 +281,15 @@ describe('IgxGrid - Grid Sorting #grid', () => { expect(getValueFromCellElement(lastRowSecondCell)).toEqual(expectedResult); }); - it('Should sort grid none when we click three time on header cell UI', () => { + it('Should sort grid none when we click three time on header sort icon UI', () => { const gridData = fixture.componentInstance.data; const firstHeaderCell = fixture.debugElement.query(By.css('igx-grid-header')); - clickCurrentRow(firstHeaderCell); + GridFunctions.clickHeaderSortIcon(firstHeaderCell); fixture.detectChanges(); - clickCurrentRow(firstHeaderCell); + GridFunctions.clickHeaderSortIcon(firstHeaderCell); fixture.detectChanges(); - clickCurrentRow(firstHeaderCell); + GridFunctions.clickHeaderSortIcon(firstHeaderCell); fixture.detectChanges(); const firstRowSecondCell = getCurrentCellFromGrid(grid, 0, 1); @@ -361,7 +361,7 @@ describe('IgxGrid - Grid Sorting #grid', () => { const gridData = fixture.componentInstance.data; const firstHeaderCell = GridFunctions.getColumnHeader('ID', fixture); - clickCurrentRow(firstHeaderCell); + GridFunctions.clickHeaderSortIcon(firstHeaderCell); fixture.detectChanges(); // Verify that the grid is NOT sorted. @@ -370,7 +370,7 @@ describe('IgxGrid - Grid Sorting #grid', () => { grid.rowList.map((item, index) => expect(grid.getCellByColumn(index, 'ID').value).toEqual(gridData[index].ID)); - clickCurrentRow(firstHeaderCell); + GridFunctions.clickHeaderSortIcon(firstHeaderCell); fixture.detectChanges(); // Verify that the grid is NOT sorted. @@ -452,10 +452,6 @@ function getCurrentCellFromGrid(grid, row, cell) { return gridCell; } -function clickCurrentRow(row) { - return row.triggerEventHandler('click', new Event('click')); -} - function getValueFromCellElement(cell) { return cell.nativeElement.textContent.trim(); } diff --git a/projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.html b/projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.html index 54d6a2b2bda..d1642852d9f 100644 --- a/projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.html +++ b/projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.html @@ -39,17 +39,24 @@ role="columnheader" [attr.aria-label]="column.header || column.field" [attr.aria-expanded]="column.expanded" + [attr.aria-selected]="column.selected" tabindex="0" [ngClass]="{ 'igx-grid__th--pinned-last': hasLastPinnedChildColumn, 'igx-grid__th--pinned-first': hasFirstPinnedChildColumn, - 'igx-grid__th--collapsible': column.collapsible}" + 'igx-grid__th--collapsible': column.collapsible, + 'igx-grid__th--selectable': selectable, + 'igx-grid__th--selected': selected}" [igxColumnMovingDrag]="column" [ghostHost]="grid.outletDirective.nativeElement" [attr.droppable]="true" - [igxColumnMovingDrop]="column"> + [igxColumnMovingDrop]="column" + (click)="groupClicked($event)" + (pointerenter)="onPinterEnter()" + (pointerleave)="onPointerLeave()" + > -
+
diff --git a/projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.ts b/projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.ts index dd96bc072c4..ce5dc1c3a89 100644 --- a/projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.ts +++ b/projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.ts @@ -195,7 +195,7 @@ export class IgxGridHeaderGroupComponent implements DoCheck { * @memberof IgxGridHeaderGroupComponent */ get isHeaderDragged(): boolean { - return this.grid.draggedColumn === this.column; + return this.grid.draggedColumn === this.column; } /** @@ -212,6 +212,23 @@ export class IgxGridHeaderGroupComponent implements DoCheck { return this.column.allChildren.some(child => child.isFirstPinned); } + /** + * @hidden + */ + get selectable() { + const selectableChildren = this.getSelectableChildren(this.column.children.toArray()); + return this.column.applySelectableClass + && !this.selected && selectableChildren.length > 0 + && !this.grid.filteringService.isFilterRowVisible; + } + + /** + * @hidden + */ + get selected() { + return this.column.selected; + } + /** * @hidden */ @@ -219,6 +236,48 @@ export class IgxGridHeaderGroupComponent implements DoCheck { return this.element.nativeElement.getBoundingClientRect().height; } + /** + * @hidden + */ + public groupClicked(event): void { + const columnsToSelect = this.getSelectableChildren(this.column.children.toArray()).map(c => c.field); + if (columnsToSelect.length > 0 && !this.grid.filteringService.isFilterRowVisible) { + if (!this.selected) { + this.grid.selectionService.selectColumns(columnsToSelect, !event.ctrlKey, event); + } else { + const selectedFields = this.grid.selectionService.getSelectedColumns(); + if ((selectedFields.length === columnsToSelect.length) && selectedFields.every(el => columnsToSelect.includes(el)) + || event.ctrlKey) { + this.grid.selectionService.deselectColumns(columnsToSelect, event); + } else { + this.grid.selectionService.selectColumns(columnsToSelect, !event.ctrlKey, event); + } + } + } + } + + private getSelectableChildren(children: IgxColumnComponent[]): IgxColumnComponent[] { + let result: IgxColumnComponent[] = []; + children.forEach(el => { + if (el.selectable && !el.hidden) { + if (el.children && el.columnGroup) { + result = result.concat(this.getSelectableChildren(el.children.toArray())); + } else { + result.push(el); + } + } + }); + return result; + } + + /** + * @hidden + */ + public toggleExpandState(event): void { + event.stopPropagation(); + this.column.expanded = !this.column.expanded; + } + /** * @hidden */ @@ -231,10 +290,23 @@ export class IgxGridHeaderGroupComponent implements DoCheck { public ngDoCheck() { this.cdr.markForCheck(); } + /** + * @hidden + */ + public onPinterEnter() { + this.column.applySelectableClass = true; + } + + /** + * @hidden + */ + public onPointerLeave() { + this.column.applySelectableClass = false; + } constructor(private cdr: ChangeDetectorRef, - public gridAPI: GridBaseAPIService, - public element: ElementRef, - public colResizingService: IgxColumnResizingService, - public filteringService: IgxFilteringService) { } + public gridAPI: GridBaseAPIService, + public element: ElementRef, + public colResizingService: IgxColumnResizingService, + public filteringService: IgxFilteringService) { } } diff --git a/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts b/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts index 174a93e7c04..fd0a730f8e0 100644 --- a/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts +++ b/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts @@ -18,7 +18,6 @@ import { DataType } from '../../data-operations/data-util'; import { SortingDirection } from '../../data-operations/sorting-expression.interface'; import { GridBaseAPIService } from '../api.service'; import { IgxColumnComponent } from '../columns/column.component'; -import { IgxFilteringService } from '../filtering/grid-filtering.service'; import { IgxGridBaseDirective } from '../grid-base.directive'; import { IgxColumnResizingService } from '../resizing/resizing.service'; import { IgxOverlayService } from '../../services/overlay/overlay'; @@ -54,6 +53,14 @@ export class IgxGridHeaderComponent implements DoCheck, OnInit, OnDestroy { @Input() public gridID: string; + /** + * Returns the `aria-selected` of the header. + */ + @HostBinding('attr.aria-selected') + public get ariaSelected(): boolean { + return this.column.selected; + } + @HostBinding('class') get styleClasses(): string { const defaultClasses = [ @@ -67,8 +74,10 @@ export class IgxGridHeaderComponent implements DoCheck, OnInit, OnDestroy { 'desc': this.descending, 'igx-grid__th--number': this.column.dataType === DataType.Number, 'igx-grid__th--sortable': this.column.sortable, + 'igx-grid__th--selectable': this.selectable, 'igx-grid__th--filtrable': this.column.filterable && this.grid.filteringService.isFilterRowVisible, - 'igx-grid__th--sorted': this.sorted + 'igx-grid__th--sorted': this.sorted, + 'igx-grid__th--selected': this.selected }; for (const klass of Object.keys(classList)) { @@ -112,6 +121,15 @@ export class IgxGridHeaderComponent implements DoCheck, OnInit, OnDestroy { return this.column.filteringExpressionsTree ? 'igx-excel-filter__icon--filtered' : 'igx-excel-filter__icon'; } + get selectable() { + return this.column.applySelectableClass && !this.column.selected && !this.grid.filteringService.isFilterRowVisible; + } + + get selected() { + return this.column.selected + && (!this.grid.filteringService.isFilterRowVisible || this.grid.filteringService.filteredColumn !== this.column); + } + @HostBinding('attr.role') public hostRole = 'columnheader'; @@ -131,7 +149,6 @@ export class IgxGridHeaderComponent implements DoCheck, OnInit, OnDestroy { public cdr: ChangeDetectorRef, public elementRef: ElementRef, public zone: NgZone, - private _filteringService: IgxFilteringService, private _moduleRef: NgModuleRef, @Inject(IgxOverlayService) private _overlayService: IgxOverlayService ) { } @@ -163,8 +180,12 @@ export class IgxGridHeaderComponent implements DoCheck, OnInit, OnDestroy { !this.grid.filteringService.isFilterComplex(this.column.field)) { this.grid.filteringService.filteredColumn = this.column; } - } else if (this.column.sortable) { - this.triggerSort(); + } else if (this.column.selectable) { + if (!this.column.selected || ( this.grid.selectionService.getSelectedColumns().length > 1 && !event.ctrlKey)) { + this.grid.selectionService.selectColumn(this.column.field, !event.ctrlKey, event); + } else { + this.grid.selectionService.deselectColumn(this.column.field, event); + } } } } @@ -185,10 +206,8 @@ export class IgxGridHeaderComponent implements DoCheck, OnInit, OnDestroy { } public onSortingIconClick(event) { - if (this.grid.filteringService.isFilterRowVisible) { - event.stopPropagation(); - this.triggerSort(); - } + event.stopPropagation(); + this.triggerSort(); } private triggerSort() { @@ -198,8 +217,10 @@ export class IgxGridHeaderComponent implements DoCheck, OnInit, OnDestroy { this.sortDirection + 1 > SortingDirection.Desc ? SortingDirection.Asc : SortingDirection.Desc : this.sortDirection + 1 > SortingDirection.Desc ? SortingDirection.None : this.sortDirection + 1; this.sortDirection = sortDir; - this.grid.sort({ fieldName: this.column.field, dir: this.sortDirection, ignoreCase: this.column.sortingIgnoreCase, - strategy: this.column.sortStrategy }); + this.grid.sort({ + fieldName: this.column.field, dir: this.sortDirection, ignoreCase: this.column.sortingIgnoreCase, + strategy: this.column.sortStrategy + }); } private toggleFilterDropdown() { @@ -261,4 +282,20 @@ export class IgxGridHeaderComponent implements DoCheck, OnInit, OnDestroy { private onOverlayClosed() { this._componentOverlayId = null; } + + /** + * @hidden + */ + @HostListener('pointerenter') + public onPinterEnter() { + this.column.applySelectableClass = true; + } + + /** + * @hidden + */ + @HostListener('pointerleave') + public onPointerLeave() { + this.column.applySelectableClass = false; + } } 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 76b0eb573cc..b2d329eab7b 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 @@ -259,17 +259,18 @@ describe('IgxHierarchicalGrid Integration #hGrid', () => { // enable sorting const childGrid = hierarchicalGrid.hgridAPI.getChildGrids(false)[0]; childGrid.columnList.toArray()[0].sortable = true; + tick(); fixture.detectChanges(); const childHeaders = fixture.debugElement.query(By.css('igx-child-grid-row')).queryAll(By.css('igx-grid-header')); - childHeaders[0].nativeElement.click(); + GridFunctions.clickHeaderSortIcon(childHeaders[0]); fixture.detectChanges(); - childHeaders[0].nativeElement.click(); + GridFunctions.clickHeaderSortIcon(childHeaders[0]); fixture.detectChanges(); const fChildCell = childGrid.dataRowList.toArray()[0].cells.toArray()[0]; expect(fChildCell.value).toBe('09'); - const icon = childHeaders[0].query(By.css('.sort-icon')); + const icon = GridFunctions.getHeaderSortIcon(childHeaders[0]); expect(icon).not.toBeNull(); expect(icon.nativeElement.textContent.toLowerCase().trim()).toBe('arrow_downward'); })); diff --git a/projects/igniteui-angular/src/lib/grids/moving/moving.drag.directive.ts b/projects/igniteui-angular/src/lib/grids/moving/moving.drag.directive.ts index 59dc4b049cd..9a80fc60edc 100644 --- a/projects/igniteui-angular/src/lib/grids/moving/moving.drag.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/moving/moving.drag.directive.ts @@ -5,7 +5,6 @@ import { IgxColumnComponent } from '../columns/column.component'; import { KEYS } from '../../core/utils'; import { IgxColumnMovingService } from './moving.service'; - /** * @hidden * @internal @@ -38,6 +37,7 @@ export class IgxColumnMovingDragDirective extends IgxDragDirective implements On private _ghostClass = 'igx-grid__drag-ghost-image'; private ghostImgIconClass = 'igx-grid__drag-ghost-image-icon'; private ghostImgIconGroupClass = 'igx-grid__drag-ghost-image-icon-group'; + private columnSelectedClass = 'igx-grid__th--selected'; constructor( public element: ElementRef, @@ -131,6 +131,8 @@ export class IgxColumnMovingDragDirective extends IgxDragDirective implements On this.ghostElement.style.flexBasis = null; this.ghostElement.style.position = null; + this.renderer.removeClass( this.ghostElement, this.columnSelectedClass); + const icon = document.createElement('i'); const text = document.createTextNode('block'); icon.appendChild(text); diff --git a/projects/igniteui-angular/src/lib/grids/selection/selection.service.ts b/projects/igniteui-angular/src/lib/grids/selection/selection.service.ts index 3f3db578d90..02be695e6fa 100644 --- a/projects/igniteui-angular/src/lib/grids/selection/selection.service.ts +++ b/projects/igniteui-angular/src/lib/grids/selection/selection.service.ts @@ -2,6 +2,7 @@ import { Injectable, EventEmitter, NgZone } from '@angular/core'; import { IGridEditEventArgs } from '../common/events'; import { IgxGridBaseDirective } from '../grid'; import { FilteringExpressionsTree } from '../../data-operations/filtering-expressions-tree'; +import { IfStmt } from '@angular/compiler'; export interface GridSelectionRange { @@ -38,6 +39,11 @@ interface ISelectionPointerState extends ISelectionKeyboardState { primaryButton: boolean; } +interface IColumnSelectionState { + field: null | string; + range: string[]; +} + type SelectionState = ISelectionKeyboardState | ISelectionPointerState; @@ -48,7 +54,7 @@ export class IgxRow { state: any; newData: any; - constructor(public id: any, public index: number, public data: any) {} + constructor(public id: any, public index: number, public data: any) { } createEditEventArgs(): IGridEditEventArgs { return { @@ -71,7 +77,7 @@ export class IgxCell { public column, public value: any, public editValue: any, - public rowData: any) {} + public rowData: any) { } castToNumber(value: any): any { if (this.column.dataType === 'number' && !this.column.inlineEditorTemplate) { @@ -211,13 +217,14 @@ export class IgxGridSelectionService { activeElement: ISelectionNode | null; keyboardState = {} as ISelectionKeyboardState; pointerState = {} as ISelectionPointerState; - + columnsState = {} as IColumnSelectionState; selection = new Map>(); temp = new Map>(); _ranges: Set = new Set(); _selectionRange: Range; rowSelection: Set = new Set(); + columnSelection: Set = new Set(); private allRowsSelected: boolean; /** @@ -250,6 +257,7 @@ export class IgxGridSelectionService { constructor(private zone: NgZone) { this.initPointerState(); this.initKeyboardState(); + this.initColumnsState(); } /** @@ -273,6 +281,14 @@ export class IgxGridSelectionService { this.pointerState.primaryButton = true; } + /** + * Resets the columns state + */ + initColumnsState(): void { + this.columnsState.field = null; + this.columnsState.range = []; + } + /** * Adds a single node. * Single clicks | Ctrl + single clicks on cells is the usual case. @@ -563,7 +579,7 @@ export class IgxGridSelectionService { /** Select all rows, if filtering is applied select only from filtered data. */ selectAllRows(event?) { const allRowIDs = this.getRowIDs(this.allData); - const addedRows = allRowIDs.filter((rID) => !this.isRowSelected(rID)); + const addedRows = allRowIDs.filter((rID) => !this.isRowSelected(rID)); const newSelection = this.rowSelection.size ? this.getSelectedRows().concat(addedRows) : addedRows; this.emitRowSelectionEvent(newSelection, addedRows, [], event); @@ -597,7 +613,7 @@ export class IgxGridSelectionService { } /** Deselect specified rows. No event is emitted. */ - deselectRowsWithNoEvent(rowIDs: any[]): void { + deselectRowsWithNoEvent(rowIDs: any[]): void { rowIDs.forEach(rowID => this.rowSelection.delete(rowID)); this.allRowsSelected = undefined; } @@ -607,7 +623,7 @@ export class IgxGridSelectionService { } /** Select range from last selected row to the current specified row.*/ - selectMultipleRows(rowID, rowData, event?): void { + selectMultipleRows(rowID, rowData, event?): void { this.allRowsSelected = undefined; if (!this.rowSelection.size || this.isRowDeleted(rowID)) { this.selectRowById(rowID); @@ -669,7 +685,7 @@ export class IgxGridSelectionService { return this.grid.primaryKey && data.length ? data.map(rec => rec[this.grid.primaryKey]) : data; } - public clearHeaderCBState(): void { + public clearHeaderCBState(): void { this.allRowsSelected = undefined; } @@ -699,6 +715,120 @@ export class IgxGridSelectionService { private isRowDeleted(rowID): boolean { return this.grid.gridAPI.row_deleted_transaction(rowID); } + + /** Returns array of the selected columns fields. */ + getSelectedColumns(): Array { + return this.columnSelection.size ? Array.from(this.columnSelection.keys()) : []; + } + + isColumnSelected(field: string): boolean { + return this.columnSelection.size > 0 && this.columnSelection.has(field); + } + + /** Select the specified column and emit event. */ + selectColumn(field: string, clearPrevSelection?, event?): void { + const stateColumn = this.columnsState.field ? this.grid.getColumnByName(this.columnsState.field) : null; + if (!event || !stateColumn || stateColumn.visibleIndex < 0 || !event.shiftKey ) { + this.columnsState.field = field; + this.columnsState.range = []; + + const newSelection = clearPrevSelection ? [field] : this.getSelectedColumns().indexOf(field) !== -1 ? + this.getSelectedColumns() : [...this.getSelectedColumns(), field]; + const removed = clearPrevSelection ? this.getSelectedColumns().filter(colField => colField !== field) : []; + const added = this.isColumnSelected(field) ? [] : [field]; + this.emitColumnSelectionEvent(newSelection, added, removed, event); + } else if (event && event.shiftKey) { + this.selectColumnsRange(field, event); + } + } + + /** Select specified columns. And emit event. */ + selectColumns(fields: string[], clearPrevSelection?, event?): void { + const columns = fields.map(f => this.grid.getColumnByName(f)).sort((a, b) => a.visibleIndex - b.visibleIndex); + const stateColumn = this.columnsState.field ? this.grid.getColumnByName(this.columnsState.field) : null; + if (!event || !stateColumn || stateColumn.visibleIndex < 0 || !event.shiftKey) { + this.columnsState.field = columns[0] ? columns[0].field : null; + this.columnsState.range = []; + + const added = fields.filter(colField => !this.isColumnSelected(colField)); + const removed = clearPrevSelection ? this.getSelectedColumns().filter(colField => fields.indexOf(colField) === -1) : []; + const newSelection = clearPrevSelection ? fields : this.getSelectedColumns().concat(added); + + this.emitColumnSelectionEvent(newSelection, added, removed, event); + } else { + const filedStart = stateColumn.visibleIndex > + columns[columns.length - 1].visibleIndex ? columns[0].field : columns[columns.length - 1].field; + this.selectColumnsRange(filedStart, event); + } + } + + /** Select range from last clicked column to the current specified column.*/ + selectColumnsRange(field: string, event): void { + const currIndex = this.grid.getColumnByName(this.columnsState.field).visibleIndex; + const newIndex = this.grid.columnToVisibleIndex(field); + const columnsFields = this.grid.visibleColumns + .filter(c => !c.columnGroup) + .sort((a, b) => a.visibleIndex - b.visibleIndex) + .slice(Math.min(currIndex, newIndex), Math.max(currIndex, newIndex) + 1) + .filter(col => col.selectable).map(col => col.field); + const removed = []; + const oldAdded = []; + const added = columnsFields.filter(colField => !this.isColumnSelected(colField)); + this.columnsState.range.forEach(f => { + if (columnsFields.indexOf(f) === -1) { + removed.push(f); + } else { + oldAdded.push(f); + } + }); + this.columnsState.range = columnsFields.filter(colField => !this.isColumnSelected(colField) || oldAdded.indexOf(colField) > -1); + const newSelection = this.getSelectedColumns().concat(added).filter(c => removed.indexOf(c) === -1); + this.emitColumnSelectionEvent(newSelection, added, removed, event); + } + + /** Select specified columns. No event is emitted. */ + selectColumnsWithNoEvent(fields: string[], clearPrevSelection?): void { + if (clearPrevSelection) { this.columnSelection.clear(); } + fields.forEach(field => { this.columnSelection.add(field); }); + } + + /** Deselect the specified column and emit event. */ + deselectColumn(field: string, event?): void { + this.initColumnsState(); + const newSelection = this.getSelectedColumns().filter(c => c !== field); + this.emitColumnSelectionEvent(newSelection, [], [field], event); + } + + /** Deselect specified columns. No event is emitted. */ + deselectColumnsWithNoEvent(fields: string[]): void { + fields.forEach(field => this.columnSelection.delete(field)); + } + + /** Deselect specified columns. And emit event. */ + deselectColumns(fields: string[], event?): void { + const removed = this.getSelectedColumns().filter(colField => fields.indexOf(colField) > -1); + const newSelection = this.getSelectedColumns().filter(colField => fields.indexOf(colField) === -1); + + this.emitColumnSelectionEvent(newSelection, [], removed, event); + } + + public emitColumnSelectionEvent(newSelection, added, removed, event?): boolean { + const currSelection = this.getSelectedColumns(); + if (this.areEqualCollections(currSelection, newSelection)) { return; } + + const args = { + oldSelection: currSelection, newSelection: newSelection, + added: added, removed: removed, event: event, cancel: false + }; + this.grid.onColumnSelectionChange.emit(args); + if (args.cancel) { return; } + this.selectColumnsWithNoEvent(args.newSelection, true); + } + + /**Clear columnSelection*/ + public clearAllSelectedColumns(): void { + this.columnSelection.clear(); + } } export function isChromium(): boolean { diff --git a/projects/igniteui-angular/src/lib/grids/state.directive.spec.ts b/projects/igniteui-angular/src/lib/grids/state.directive.spec.ts index 7981fa4e3f6..07ef392f9b0 100644 --- a/projects/igniteui-angular/src/lib/grids/state.directive.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/state.directive.spec.ts @@ -72,7 +72,7 @@ describe('IgxGridState - input properties #grid', () => { it('getState should return corect JSON string', () => { // tslint:disable-next-line:max-line-length - const initialGridState = '{"columns":[{"pinned":true,"sortable":true,"filterable":true,"editable":false,"sortingIgnoreCase":true,"filteringIgnoreCase":true,"headerClasses":"testCss","headerGroupClasses":"","maxWidth":"300px","groupable":false,"movable":true,"hidden":false,"dataType":"number","hasSummary":false,"field":"ProductID","width":"150px","header":"Product ID","resizable":true,"searchable":false},{"pinned":false,"sortable":true,"filterable":true,"editable":false,"sortingIgnoreCase":true,"filteringIgnoreCase":true,"headerClasses":"","headerGroupClasses":"","maxWidth":"300px","groupable":true,"movable":true,"hidden":false,"dataType":"string","hasSummary":false,"field":"ProductName","width":"150px","header":"Prodyct Name","resizable":true,"searchable":true},{"pinned":false,"sortable":false,"filterable":true,"editable":true,"sortingIgnoreCase":true,"filteringIgnoreCase":true,"headerClasses":"","headerGroupClasses":"","maxWidth":"300px","groupable":false,"movable":false,"hidden":false,"dataType":"boolean","hasSummary":true,"field":"InStock","width":"140px","header":"In Stock","resizable":true,"searchable":true},{"pinned":false,"sortable":true,"filterable":false,"editable":true,"sortingIgnoreCase":true,"filteringIgnoreCase":true,"headerClasses":"","headerGroupClasses":"","maxWidth":"300px","groupable":true,"movable":false,"hidden":false,"dataType":"date","hasSummary":false,"field":"OrderDate","width":"110px","header":"Date ordered","resizable":false,"searchable":true}],"filtering":{"filteringOperands":[],"operator":0},"sorting":[],"groupBy":{"expressions":[],"expansion":[],"defaultExpanded":true},"paging":{"index":0,"recordsPerPage":15,"metadata":{"countPages":1,"countRecords":10,"error":0}},"cellSelection":[],"rowSelection":[]}'; + const initialGridState = '{"columns":[{"pinned":true,"sortable":true,"filterable":true,"editable":false,"sortingIgnoreCase":true,"filteringIgnoreCase":true,"headerClasses":"testCss","headerGroupClasses":"","maxWidth":"300px","groupable":false,"movable":true,"hidden":false,"dataType":"number","hasSummary":false,"field":"ProductID","width":"150px","header":"Product ID","resizable":true,"searchable":false},{"pinned":false,"sortable":true,"filterable":true,"editable":false,"sortingIgnoreCase":true,"filteringIgnoreCase":true,"headerClasses":"","headerGroupClasses":"","maxWidth":"300px","groupable":true,"movable":true,"hidden":false,"dataType":"string","hasSummary":false,"field":"ProductName","width":"150px","header":"Prodyct Name","resizable":true,"searchable":true},{"pinned":false,"sortable":false,"filterable":true,"editable":true,"sortingIgnoreCase":true,"filteringIgnoreCase":true,"headerClasses":"","headerGroupClasses":"","maxWidth":"300px","groupable":false,"movable":false,"hidden":false,"dataType":"boolean","hasSummary":true,"field":"InStock","width":"140px","header":"In Stock","resizable":true,"searchable":true},{"pinned":false,"sortable":true,"filterable":false,"editable":true,"sortingIgnoreCase":true,"filteringIgnoreCase":true,"headerClasses":"","headerGroupClasses":"","maxWidth":"300px","groupable":true,"movable":false,"hidden":false,"dataType":"date","hasSummary":false,"field":"OrderDate","width":"110px","header":"Date ordered","resizable":false,"searchable":true}],"filtering":{"filteringOperands":[],"operator":0},"sorting":[],"groupBy":{"expressions":[],"expansion":[],"defaultExpanded":true},"paging":{"index":0,"recordsPerPage":15,"metadata":{"countPages":1,"countRecords":10,"error":0}},"cellSelection":[],"rowSelection":[],"columnSelection":[]}'; const fix = TestBed.createComponent(IgxGridStateComponent); fix.detectChanges(); diff --git a/projects/igniteui-angular/src/lib/grids/state.directive.ts b/projects/igniteui-angular/src/lib/grids/state.directive.ts index b3d3078a4c0..8be1c13acce 100644 --- a/projects/igniteui-angular/src/lib/grids/state.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/state.directive.ts @@ -22,6 +22,7 @@ export interface IGridState { groupBy?: IGroupingState; cellSelection?: GridSelectionRange[]; rowSelection?: any[]; + columnSelection?: string[]; } export interface IGridStateOptions { @@ -33,6 +34,7 @@ export interface IGridStateOptions { paging?: boolean; cellSelection?: boolean; rowSelection?: boolean; + columnSelection?: boolean; } export interface IColumnState { @@ -65,6 +67,7 @@ const GROUPBY = 'groupBy'; const PAGING = 'paging'; const ROW_SELECTION = 'rowSelection'; const CELL_SELECTION = 'cellSelection'; +const COLUMN_SELECTION = 'columnSelection'; @Directive({ selector: '[igxGridState]' @@ -79,7 +82,8 @@ export class IgxGridStateDirective { groupBy: true, paging: true, cellSelection: true, - rowSelection: true + rowSelection: true, + columnSelection: true }; private state: IGridState; @@ -216,6 +220,10 @@ export class IgxGridStateDirective { this.restoreCellSelection(state as GridSelectionRange[]); break; } + case COLUMN_SELECTION: { + this.restoreColumnSelection(state as string[]); + break; + } } } @@ -275,6 +283,10 @@ export class IgxGridStateDirective { Object.assign(state, this.getCellSelection()); break; } + case COLUMN_SELECTION: { + Object.assign(state, this.getColumnSelection()); + break; + } } return state; } @@ -348,6 +360,11 @@ export class IgxGridStateDirective { return { rowSelection: selection }; } + private getColumnSelection(): IGridState { + const selection = this.grid.selectedColumns().map(c => c.field); + return { columnSelection: selection }; + } + private getCellSelection(): IGridState { const selection = this.grid.getSelectedRanges().map(range => { return { rowStart: range.rowStart, rowEnd: range.rowEnd, columnStart: range.columnStart, columnEnd: range.columnEnd }; @@ -427,6 +444,10 @@ export class IgxGridStateDirective { this.grid.selectRows(state); } + private restoreColumnSelection(state: string[]) { + this.grid.selectColumns(state); + } + private restoreCellSelection(state: GridSelectionRange[]) { state.forEach(r => { const range = { rowStart: r.rowStart, rowEnd: r.rowEnd, columnStart: r.columnStart, columnEnd: r.columnEnd}; diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-sorting.spec.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-sorting.spec.ts index b0c52e7bf8e..c6547ba336e 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-sorting.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-sorting.spec.ts @@ -7,6 +7,7 @@ import { TreeGridFunctions } from '../../test-utils/tree-grid-functions.spec'; import { configureTestSuite } from '../../test-utils/configure-suite'; import { DefaultSortingStrategy } from '../../data-operations/sorting-strategy'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { GridFunctions } from '../../test-utils/grid-functions.spec'; describe('IgxTreeGrid - Sorting #tGrid', () => { configureTestSuite(); @@ -203,8 +204,9 @@ describe('IgxTreeGrid - Sorting #tGrid', () => { describe('UI sorting', () => { it('should sort descending all treeGrid levels by column name through UI', () => { - TreeGridFunctions.clickHeaderCell(fix, 'Name'); - TreeGridFunctions.clickHeaderCell(fix, 'Name'); + const header = TreeGridFunctions.getHeaderCell(fix, 'Name'); + GridFunctions.clickHeaderSortIcon(header); + GridFunctions.clickHeaderSortIcon(header); fix.detectChanges(); // Verify first level records are desc sorted @@ -224,7 +226,8 @@ describe('IgxTreeGrid - Sorting #tGrid', () => { }); it('should sort ascending all treeGrid levels by column name through UI', () => { - TreeGridFunctions.clickHeaderCell(fix, 'Age'); + const header = TreeGridFunctions.getHeaderCell(fix, 'Age'); + GridFunctions.clickHeaderSortIcon(header); fix.detectChanges(); // Verify first level records are asc sorted @@ -250,7 +253,8 @@ describe('IgxTreeGrid - Sorting #tGrid', () => { expect(treeGrid.getCellByColumn(4, 'Age').value).toEqual(35); // Click header once - TreeGridFunctions.clickHeaderCell(fix, 'Age'); + const header = TreeGridFunctions.getHeaderCell(fix, 'Age'); + GridFunctions.clickHeaderSortIcon(header); fix.detectChanges(); // Verify first record of all 3 levels (sorted layout) @@ -259,9 +263,9 @@ describe('IgxTreeGrid - Sorting #tGrid', () => { expect(treeGrid.getCellByColumn(6, 'Age').value).toEqual(25); // Click header two more times - TreeGridFunctions.clickHeaderCell(fix, 'Age'); + GridFunctions.clickHeaderSortIcon(header); fix.detectChanges(); - TreeGridFunctions.clickHeaderCell(fix, 'Age'); + GridFunctions.clickHeaderSortIcon(header); fix.detectChanges(); // Verify first record of all 3 levels (default layout) @@ -278,11 +282,13 @@ describe('IgxTreeGrid - Sorting #tGrid', () => { fix.detectChanges(); // Sort by 'Name' in asc order and by 'Age' in desc order - TreeGridFunctions.clickHeaderCell(fix, 'Name'); + const headerName = TreeGridFunctions.getHeaderCell(fix, 'Name'); + const headerAge = TreeGridFunctions.getHeaderCell(fix, 'Age'); + GridFunctions.clickHeaderSortIcon(headerName); fix.detectChanges(); - TreeGridFunctions.clickHeaderCell(fix, 'Age'); + GridFunctions.clickHeaderSortIcon(headerAge); fix.detectChanges(); - TreeGridFunctions.clickHeaderCell(fix, 'Age'); + GridFunctions.clickHeaderSortIcon(headerAge); fix.detectChanges(); expect(treeGrid.sortingExpressions.length).toBe(2); @@ -326,17 +332,19 @@ describe('IgxTreeGrid - Sorting #tGrid', () => { fix.detectChanges(); // Sort by 'Name' in asc order and by 'Age' in desc order - TreeGridFunctions.clickHeaderCell(fix, 'Name'); + const headerName = TreeGridFunctions.getHeaderCell(fix, 'Name'); + const headerAge = TreeGridFunctions.getHeaderCell(fix, 'Age'); + GridFunctions.clickHeaderSortIcon(headerName); fix.detectChanges(); - TreeGridFunctions.clickHeaderCell(fix, 'Age'); + GridFunctions.clickHeaderSortIcon(headerAge); fix.detectChanges(); - TreeGridFunctions.clickHeaderCell(fix, 'Age'); + GridFunctions.clickHeaderSortIcon(headerAge); fix.detectChanges(); // Clear sorting for 'Name' column - TreeGridFunctions.clickHeaderCell(fix, 'Name'); + GridFunctions.clickHeaderSortIcon(headerName); fix.detectChanges(); - TreeGridFunctions.clickHeaderCell(fix, 'Name'); + GridFunctions.clickHeaderSortIcon(headerName); fix.detectChanges(); expect(treeGrid.sortingExpressions.length).toBe(1); diff --git a/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts b/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts index e168d3c4842..f0c6b310553 100644 --- a/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts @@ -12,7 +12,7 @@ import { IgxGridHeaderGroupComponent } from '../grids/headers/grid-header-group. import { SortingDirection } from '../data-operations/sorting-expression.interface'; import { IgxCheckboxComponent } from '../checkbox/checkbox.component'; import { UIInteractions, wait } from './ui-interactions.spec'; -import { IgxGridGroupByRowComponent, IgxGridCellComponent, IgxGridRowComponent } from '../grids/grid'; +import { IgxGridGroupByRowComponent, IgxGridCellComponent, IgxGridRowComponent, IgxColumnComponent } from '../grids/grid'; import { ControlsFunction } from './controls-functions.spec'; import { IgxGridExpandableCellComponent } from '../grids/grid/expandable-cell.component'; @@ -58,6 +58,10 @@ const ROW_CSS_CLASS = '.igx-grid__tr'; const FOCUSED_CHECKBOX_CLASS = 'igx-checkbox--focused'; const GRID_BODY_CLASS = '.igx-grid__tbody'; const DISPLAY_CONTAINER = 'igx-display-container'; +const SORT_ICON_CLASS = '.sort-icon'; +const SELECTED_COLUMN_CLASS = 'igx-grid__th--selected'; +const HOVERED_COLUMN_CLASS = 'igx-grid__th--selectable'; +const SELECTED_COLUMN_CELL_CLASS = 'igx-grid__td--column-selected'; export class GridFunctions { @@ -838,7 +842,7 @@ export class GridFunctions { hideIcon.click(); } - public static getIconFromButton(iconName: string, component: any, fix: ComponentFixture) { + public static getIconFromButton(iconName: string, component: any) { const icons = component.querySelectorAll('igx-icon'); return Array.from(icons).find((sortIcon: any) => sortIcon.innerText === iconName); } @@ -847,15 +851,23 @@ export class GridFunctions { * Click the sort ascending button in the ESF. */ public static clickSortAscInExcelStyleFiltering(fix: ComponentFixture) { - const sortAscIcon: any = this.getIconFromButton('arrow_upwards', GridFunctions.getExcelFilteringSortComponent(fix), fix); + const sortAscIcon: any = this.getIconFromButton('arrow_upwards', GridFunctions.getExcelFilteringSortComponent(fix)); sortAscIcon.click(); } + /** + * Click the column selection button in the ESF. + */ + public static clickColumnSelectionInExcelStyleFiltering(fix: ComponentFixture) { + const columnSelectIcon: any = this.getIconFromButton('done', GridFunctions.getExcelFilteringColumnSelectionContainer(fix)); + columnSelectIcon.click(); + } + /** * Click the sort descending button in the ESF. */ public static clickSortDescInExcelStyleFiltering(fix: ComponentFixture) { - const sortDescIcon: any = this.getIconFromButton('arrow_downwards', GridFunctions.getExcelFilteringSortComponent(fix), fix); + const sortDescIcon: any = this.getIconFromButton('arrow_downwards', GridFunctions.getExcelFilteringSortComponent(fix)); sortDescIcon.click(); } @@ -863,19 +875,18 @@ export class GridFunctions { * Click the move left button in the ESF. */ public static clickMoveLeftInExcelStyleFiltering(fix: ComponentFixture) { - const moveLeftIcon: any = this.getIconFromButton('arrow_back', GridFunctions.getExcelFilteringMoveComponent(fix), fix); + const moveLeftIcon: any = this.getIconFromButton('arrow_back', GridFunctions.getExcelFilteringMoveComponent(fix)); moveLeftIcon.click(); } /** * Click the move right button in the ESF. - */ + */ public static clickMoveRightInExcelStyleFiltering(fix: ComponentFixture) { - const moveRightIcon: any = this.getIconFromButton('arrow_forwards', GridFunctions.getExcelFilteringMoveComponent(fix), fix); + const moveRightIcon: any = this.getIconFromButton('arrow_forwards', GridFunctions.getExcelFilteringMoveComponent(fix)); moveRightIcon.click(); } - public static getExcelFilteringInput(fix: ComponentFixture, expressionIndex: number = 0): HTMLInputElement { const expr = GridFunctions.getExcelCustomFilteringDefaultExpressions(fix)[expressionIndex]; return expr.querySelectorAll('.igx-input-group__input').item(1) as HTMLInputElement; @@ -912,6 +923,13 @@ export class GridFunctions { clearIcon.click(); } + /** + * returns the filter row debug element. + */ + public static getFilterRow(fix: ComponentFixture): DebugElement { + return fix.debugElement.query(By.css(FILTER_UI_ROW)); + } + /** * Open filtering row for a column. */ @@ -976,8 +994,8 @@ export class GridFunctions { return excelMenu; } public static getExcelStyleFilteringCheckboxes(fix, menu = null): HTMLElement[] { - const excelMenu = menu ? menu : GridFunctions.getExcelStyleFilteringComponent(fix); - return GridFunctions.sortNativeElementsVertically(Array.from(excelMenu.querySelectorAll(CHECKBOX_INPUT_CSS_CLASS))); + const searchComp = GridFunctions.getExcelStyleSearchComponent(fix, menu); + return GridFunctions.sortNativeElementsVertically(Array.from(searchComp.querySelectorAll(CHECKBOX_INPUT_CSS_CLASS))); } public static getExcelStyleFilteringSortContainer(fix, menu = null) { @@ -1028,6 +1046,18 @@ export class GridFunctions { }); } + public static clickColumnHeaderUI(columnField: string, fix: ComponentFixture, ctrlKey = false, shiftKey = false) { + const header = this.getColumnHeader(columnField, fix); + header.triggerEventHandler('click', new MouseEvent('click', { shiftKey: shiftKey, ctrlKey: ctrlKey})); + fix.detectChanges(); + } + + public static clickColumnGroupHeaderUI(columnField: string, fix: ComponentFixture, ctrlKey = false, shiftKey = false) { + const header = this.getColumnGroupHeaderCell(columnField, fix); + header.triggerEventHandler('click', new MouseEvent('click', { shiftKey: shiftKey, ctrlKey: ctrlKey})); + fix.detectChanges(); + } + public static getColumnHeaderByIndex(fix: ComponentFixture, index: number) { const nativeHeaders = fix.debugElement.queryAll(By.directive(IgxGridHeaderComponent)) .map((header) => header.nativeElement); @@ -1090,6 +1120,12 @@ export class GridFunctions { return excelMenu.querySelector('igx-excel-style-column-moving'); } + public static getExcelFilteringColumnSelectionContainer(fix: ComponentFixture, menu = null): HTMLElement { + const excelMenu = menu ? menu : GridFunctions.getExcelStyleFilteringComponent(fix); + return excelMenu.querySelector('.igx-excel-filter__actions-select') || + excelMenu.querySelector('.igx-excel-filter__actions-selected'); + } + public static getExcelFilteringLoadingIndicator(fix: ComponentFixture) { const searchComponent = GridFunctions.getExcelStyleSearchComponent(fix); const loadingIndicator = searchComponent.querySelector('.igx-excel-filter__loading'); @@ -1779,6 +1815,15 @@ export class GridFunctions { rowComp.onBlur(); } } + + public static getHeaderSortIcon(header: DebugElement): DebugElement { + return header.query(By.css(SORT_ICON_CLASS)); + } + + public static clickHeaderSortIcon(header: DebugElement) { + const sortIcon = header.query(By.css(SORT_ICON_CLASS)); + sortIcon.triggerEventHandler('click', new Event('click')); + } } export class GridSummaryFunctions { public static verifyColumnSummariesBySummaryRowIndex(fix, rowIndex: number, summaryIndex: number, summaryLabels, summaryResults) { @@ -1967,7 +2012,6 @@ export class GridSelectionFunctions { expect(selectedCellFromGrid.rowIndex).toEqual(cell.rowIndex); } - public static verifyRowSelected(row, selected = true, hasCheckbox = true) { expect(row.selected).toBe(selected); expect(row.nativeElement.classList.contains(ROW_SELECTION_CSS_CLASS)).toBe(selected); @@ -2071,4 +2115,46 @@ export class GridSelectionFunctions { public static expandRowIsland(rowNumber = 1) { (document.getElementsByClassName(ICON_CSS_CLASS)[rowNumber]).click(); } + + public static verifyColumnSelected(column: IgxColumnComponent, selected = true) { + expect(column.selected).toEqual(selected); + if (!column.hidden) { + expect(column.headerCell.elementRef.nativeElement.classList.contains(SELECTED_COLUMN_CLASS)).toEqual(selected); + } + } + + public static verifyColumnsSelected(columns: IgxColumnComponent[], selected = true) { + columns.forEach(c => this.verifyColumnSelected(c, selected )); + } + + public static verifyColumnGroupSelected(fixture: ComponentFixture, column: IgxColumnGroupComponent, selected = true) { + expect(column.selected).toEqual(selected); + const header = GridFunctions.getColumnGroupHeaderCell(column.header, fixture); + expect(header.nativeElement.classList.contains(SELECTED_COLUMN_CLASS)).toEqual(selected); + } + + public static verifyColumnHeaderHasSelectableClass(header: DebugElement, hovered = true) { + expect(header.nativeElement.classList.contains(HOVERED_COLUMN_CLASS)).toEqual(hovered); + } + + public static verifyColumnsHeadersHasSelectableClass(headers: DebugElement[], hovered = true) { + headers.forEach(header => this.verifyColumnHeaderHasSelectableClass(header, hovered)); + } + + public static verifyColumnAndCellsSelected(column: IgxColumnComponent, selected = true) { + this.verifyColumnSelected(column, selected); + column.cells.forEach(cell => { + expect(cell.nativeElement.classList.contains(SELECTED_COLUMN_CELL_CLASS)).toEqual(selected); + }); + } + + public static clickOnColumnToSelect(column: IgxColumnComponent, ctrlKey = false, shiftKey= false) { + const event = { + shiftKey: shiftKey, + ctrlKey: ctrlKey, + stopPropagation: () => { } + }; + + column.headerCell.onClick(event); + } } 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 5568c1bc83c..1a421c896a2 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 @@ -979,9 +979,10 @@ export class IgxGridFilteringComponent extends BasicGridComponent { + [filterable]="filterable" [resizable]="resizable" dataType="string" [selectable]="false"> - + @@ -1095,6 +1096,7 @@ export class IgxGridFilteringESFLoadOnDemandComponent extends BasicGridComponent
Hiding Template
Moving Template
Pinning Template
+
Column Selection Template
` }) export class IgxGridFilteringESFTemplatesComponent extends BasicGridComponent { @@ -1734,6 +1736,38 @@ export class CollapsibleColumnGroupTestComponent { data = SampleTestData.contactInfoDataFull(); } + +@Component({ + template: ` + + + + + + + + + + + + + + + + + + + + + + + ` +}) +export class ColumnSelectionGroupTestComponent { + @ViewChild(IgxGridComponent, { read: IgxGridComponent, static: true }) + grid: IgxGridComponent; + data = SampleTestData.contactInfoDataFull(); +} @Component({ template: ` diff --git a/src/app/app.component.ts b/src/app/app.component.ts index f531afd8e94..e76c796b9f3 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -187,6 +187,11 @@ export class AppComponent implements OnInit { icon: 'view_column', name: 'Grid Column Moving' }, + { + link: '/gridColumnSelecting', + icon: 'view_column', + name: 'Grid Column Selection' + }, { link: '/gridColumnPinning', icon: 'view_column', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 352880aa685..436538a5c65 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -115,6 +115,7 @@ import { GridExternalFilteringComponent } from './grid-external-filtering/grid-e import { AboutComponent } from './grid-state/about.component'; import { GridSaveStateComponent } from './grid-state/grid-state.component'; import { GridMasterDetailSampleComponent } from './grid-master-detail/grid-master-detail.sample'; +import { GridColumnSelectionSampleComponent, GridColumnSelectionFilterPipe } from './grid-column-selection/grid-column-selection.sample'; import { ReactiveFormSampleComponent } from './reactive-from/reactive-form-sample.component'; import { GridRowPinningSampleComponent } from './grid-row-pinning/grid-row-pinning.sample'; @@ -177,6 +178,8 @@ const components = [ GridCellEditingComponent, GridSampleComponent, GridColumnMovingSampleComponent, + GridColumnSelectionSampleComponent, + GridColumnSelectionFilterPipe, GridColumnPinningSampleComponent, GridRowPinningSampleComponent, GridColumnResizingSampleComponent, diff --git a/src/app/grid-column-moving/grid-column-moving.sample.html b/src/app/grid-column-moving/grid-column-moving.sample.html index 394b8eadd54..cd7321fc5e8 100644 --- a/src/app/grid-column-moving/grid-column-moving.sample.html +++ b/src/app/grid-column-moving/grid-column-moving.sample.html @@ -18,7 +18,7 @@ (onColumnMovingStart)="onColumnMovingStart($event)" (onColumnMoving)="onColumnMoving($event)" (onColumnMovingEnd)="onColumnMovingEnd($event)" - [rowSelectable]="true" + [rowSelection] ="'multiple'" [filterMode]="'excelStyleFilter'" [paging]="false" [width]="'900px'" diff --git a/src/app/grid-column-selection/grid-column-selection.sample.css b/src/app/grid-column-selection/grid-column-selection.sample.css new file mode 100644 index 00000000000..ca9985f2024 --- /dev/null +++ b/src/app/grid-column-selection/grid-column-selection.sample.css @@ -0,0 +1,12 @@ +.sample-buttons { + margin-top: 24px; +} + +[igxButton]+[igxButton] { + margin-left: 8px; +} + +.density-chooser { + margin-bottom: 16px; + max-width: 900px; +} diff --git a/src/app/grid-column-selection/grid-column-selection.sample.html b/src/app/grid-column-selection/grid-column-selection.sample.html new file mode 100644 index 00000000000..75a26760e1f --- /dev/null +++ b/src/app/grid-column-selection/grid-column-selection.sample.html @@ -0,0 +1,89 @@ +
+ + Allows selecting a columns + + +
+
+
+ +
+ + + + + + + + + + +
+ + {{ columnItem.field }} + +
+
+ + +
+
+
+
+ +

TEST EXAMPLE

+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ Enable Paging + + + + + + +
+
+
+
\ No newline at end of file diff --git a/src/app/grid-column-selection/grid-column-selection.sample.ts b/src/app/grid-column-selection/grid-column-selection.sample.ts new file mode 100644 index 00000000000..126a28b9985 --- /dev/null +++ b/src/app/grid-column-selection/grid-column-selection.sample.ts @@ -0,0 +1,161 @@ +import { Component, ViewChild, OnInit, Pipe, PipeTransform, ɵbypassSanitizationTrustResourceUrl, AfterViewInit } from '@angular/core'; +import { + IgxGridComponent, + OverlaySettings, + ConnectedPositioningStrategy, + AbsoluteScrollStrategy, + PositionSettings, + HorizontalAlignment, + VerticalAlignment, + IgxDropDownComponent, + IgxButtonDirective, + IgxColumnComponent, + FilterMode +} from 'igniteui-angular'; +import { SAMPLE_DATA } from '../shared/sample-data'; + +@Component({ + providers: [], + selector: 'app-grid-column-selection-sample', + styleUrls: ['grid-column-selection.sample.css'], + templateUrl: 'grid-column-selection.sample.html' +}) + +export class GridColumnSelectionSampleComponent implements OnInit { + public searchSelectedColumn = ''; + public data: Array; + public columns: Array; + // public data = []; + public filterModes = [ + { + label: 'Filter Row', + value: FilterMode.quickFilter, + selected: false, + togglable: true }, + { + label: 'Excel Style', + value: FilterMode.excelStyleFilter, + selected: true, + togglable: true + } + ]; + private _positionSettings: PositionSettings = { + horizontalDirection: HorizontalAlignment.Left, + horizontalStartPoint: HorizontalAlignment.Right, + verticalDirection: VerticalAlignment.Bottom, + verticalStartPoint: VerticalAlignment.Bottom + }; + private _overlaySettings: OverlaySettings = { + positionStrategy: new ConnectedPositioningStrategy(this._positionSettings), + scrollStrategy: new AbsoluteScrollStrategy(), + modal: false, + closeOnOutsideClick: true, + excludePositionTarget: true + }; + + @ViewChild('grid1', { static: true }) public grid1: IgxGridComponent; + @ViewChild('grid', { static: true }) public grid: IgxGridComponent; + + @ViewChild('columnSelectionDropdown', { read: IgxDropDownComponent }) + public columnSelectionDropdown: IgxDropDownComponent; + + @ViewChild('columnSelectionButton', { read: IgxButtonDirective }) + public columnSelectionButton: IgxButtonDirective; + + public density = 'comfortable'; + public displayDensities; + + log(event) { + console.log(event); + } + + public ngOnInit(): void { + this.displayDensities = [ + { label: 'comfortable', selected: this.density === 'comfortable', togglable: true }, + { label: 'cosy', selected: this.density === 'cosy', togglable: true }, + { label: 'compact', selected: this.density === 'compact', togglable: true } + ]; + this.data = SAMPLE_DATA.slice(0); + + this.columns = [ + { field: 'ID', width: 150, groupable: true, summary: true, selectable: true, type: 'string' }, + { field: 'CompanyName', width: 150, groupable: true, summary: true, selectable: true, type: 'string', }, + { field: 'ContactName', width: 150, resizable: true, movable: true, selectable: false, summary: true, type: 'string' }, + { field: 'ContactTitle', width: 150, movable: true, sortable: true, selectable: true, summary: true, type: 'string' }, + { field: 'Address', width: 150, resizable: true, movable: true, sortable: true, selectable: true, type: 'string' }, + { field: 'City', width: 150, movable: true, sortable: false, selectable: true, type: 'string' }, + { field: 'Region', width: 150, movable: true, sortable: true, selectable: true, type: 'string' }, + { field: 'PostalCode', width: 150, movable: true, selectable: true, type: 'string' }, + { field: 'Phone', width: 150, resizable: true, movable: true, sortable: true, type: 'string' }, + { field: 'Fax', width: 150, resizable: true, movable: true, selectable: true, type: 'string' }, + { field: 'Employees', width: 150, resizable: true, summary: false, selectable: true, type: 'number' }, + { field: 'DateCreated', width: 150, resizable: true, selectable: true, type: 'date' }, + { field: 'Contract', width: 150, resizable: true, selectable: true, type: 'boolean' } + ]; + } + + public getGenInfoState() { + console.log('general info', this.grid.getColumnByName('CompanyName').parent.selected); + console.log('company name', this.grid.getColumnByName('CompanyName').selected); + } + + public selectDensity(event) { + this.density = this.displayDensities[event.index].label; + } + + getGridSelectedColunsData() { + const data = this.grid1.getSelectedColumnsData(); + console.log(data); + } + deselectCol() { + this.grid1.getColumnByName('ID').selected = true; + } + + public toggleColumnSelection() { + this._overlaySettings.positionStrategy.settings.target = this.columnSelectionButton.nativeElement; + this._overlaySettings.outlet = this.grid1.outletDirective; + this.columnSelectionDropdown.toggle(this._overlaySettings); + } + + onColumnSelection(event) { + console.log(event); + } + + getGridSelectedColumns() { + console.log(this.grid1.selectedColumns()); + } + + selectedColumns() { + this.grid1.selectColumns(['ID', 'Name', 'aaaa']); + } + + public selectFilterMode(event) { + const filterMode = this.filterModes[event.index].value as FilterMode; + if (filterMode !== this.grid1.filterMode) { + this.grid1.filterMode = filterMode; + } + } +} + + +@Pipe({ + name: 'filterColumns' +}) +export class GridColumnSelectionFilterPipe implements PipeTransform { + public transform(items: any[], searchText: string): any[] { + if (!items || !items.length) { + return []; + } + + if (!searchText) { + return items; + } + + searchText = searchText.toLowerCase(); + const result = items.filter((it) => + it.field.toLowerCase().indexOf(searchText) > -1 ); + + return result; + } +} + diff --git a/src/app/grid-state/grid-state.component.html b/src/app/grid-state/grid-state.component.html index ae5db0d9707..7e77d6400dd 100644 --- a/src/app/grid-state/grid-state.component.html +++ b/src/app/grid-state/grid-state.component.html @@ -17,6 +17,9 @@
Cell Selection
+
+ Column Selection +
Filtering
@@ -82,7 +85,7 @@