diff --git a/apps/code-examples/src/app/app.component.html b/apps/code-examples/src/app/app.component.html index a5b4552fdc..719285f526 100644 --- a/apps/code-examples/src/app/app.component.html +++ b/apps/code-examples/src/app/app.component.html @@ -34,6 +34,11 @@ Data Entry Grid with Data Manager +
  • + + Data Entry Grid with inline help + +
  • Data Grid (basic)
  • @@ -57,6 +62,11 @@ Data Grid with top scrollbar +
  • + + Data Grid with inline help + +
  • diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-context-menu.component.html b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-context-menu.component.html new file mode 100644 index 0000000000..4449c58976 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-context-menu.component.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-context-menu.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-context-menu.component.ts new file mode 100644 index 0000000000..83aa1aa499 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-context-menu.component.ts @@ -0,0 +1,27 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +@Component({ + selector: 'app-data-entry-grid-docs-demo-context-menu', + templateUrl: './data-entry-grid-docs-demo-context-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SkyDataEntryGridContextMenuComponent + implements ICellRendererAngularComp +{ + #name = ''; + + public agInit(params: ICellRendererParams): void { + this.#name = params.data && params.data.#name; + } + + public refresh(): boolean { + return false; + } + + public actionClicked(action: string): void { + alert(`${action} clicked for ${this.#name}`); + } +} diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-data.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-data.ts new file mode 100644 index 0000000000..dd9a923dd9 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-data.ts @@ -0,0 +1,156 @@ +export interface SkyAutocompleteOption { + id: string; + name: string; +} + +export const SKY_DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const SKY_JOB_TITLES: { [name: string]: SkyAutocompleteOption[] } = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface SkyAgGridDemoRow { + selected: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: SkyAutocompleteOption; + jobTitle?: SkyAutocompleteOption; +} + +export const SKY_AG_GRID_DEMO_DATA = [ + { + selected: true, + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: SKY_DEPARTMENTS[3], + jobTitle: SKY_JOB_TITLES['Customer Support'][1], + }, + { + selected: false, + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: SKY_DEPARTMENTS[2], + jobTitle: SKY_JOB_TITLES['Engineering'][2], + }, + { + selected: false, + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: SKY_DEPARTMENTS[1], + }, + { + selected: false, + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: SKY_DEPARTMENTS[2], + jobTitle: SKY_JOB_TITLES['Engineering'][4], + }, + { + selected: true, + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: SKY_DEPARTMENTS[0], + jobTitle: SKY_JOB_TITLES['Marketing'][2], + }, + { + selected: false, + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: SKY_DEPARTMENTS[2], + jobTitle: SKY_JOB_TITLES['Engineering'][0], + }, + { + selected: false, + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: SKY_DEPARTMENTS[2], + jobTitle: SKY_JOB_TITLES['Engineering'][3], + }, +]; diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-edit-modal-context.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-edit-modal-context.ts new file mode 100644 index 0000000000..7023f91acd --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-edit-modal-context.ts @@ -0,0 +1,5 @@ +import { SkyAgGridDemoRow } from './data-entry-grid-docs-demo-data'; + +export class SkyDataEntryGridEditModalContext { + public gridData: SkyAgGridDemoRow[] = []; +} diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-edit-modal.component.html b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-edit-modal.component.html new file mode 100644 index 0000000000..1336ac6d46 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-edit-modal.component.html @@ -0,0 +1,31 @@ + + Edit employee information + +
    + + + + +
    +
    + + + + +
    diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-edit-modal.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-edit-modal.component.ts new file mode 100644 index 0000000000..22291be956 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo-edit-modal.component.ts @@ -0,0 +1,213 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, +} from '@angular/core'; +import { + SkyAgGridAutocompleteProperties, + SkyAgGridDatepickerProperties, + SkyAgGridService, + SkyCellType, +} from '@skyux/ag-grid'; +import { SkyAutocompleteSelectionChange } from '@skyux/lookup'; +import { SkyModalInstance } from '@skyux/modals'; + +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + ICellEditorParams, + NewValueParams, + RowNode, +} from 'ag-grid-community'; + +import { + SKY_DEPARTMENTS, + SKY_JOB_TITLES, + SkyAgGridDemoRow, +} from './data-entry-grid-docs-demo-data'; +import { SkyDataEntryGridEditModalContext } from './data-entry-grid-docs-demo-edit-modal-context'; + +@Component({ + selector: 'app-data-entry-grid-docs-demo-edit-modal', + templateUrl: './data-entry-grid-docs-demo-edit-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SkyDataEntryGridEditModalComponent { + public columnDefs: ColDef[]; + public gridData: SkyAgGridDemoRow[]; + public gridOptions: GridOptions; + + #gridApi: GridApi | undefined; + #agGridService: SkyAgGridService; + #changeDetector: ChangeDetectorRef; + + constructor( + public context: SkyDataEntryGridEditModalContext, + public instance: SkyModalInstance, + agGridService: SkyAgGridService, + changeDetector: ChangeDetectorRef + ) { + this.#agGridService = agGridService; + this.#changeDetector = changeDetector; + + this.columnDefs = [ + { + field: 'name', + headerName: 'Name', + type: SkyCellType.Text, + cellRendererParams: { + skyComponentProperties: { + validator: (value): boolean => String(value).length <= 10, + validatorMessage: `Value exceeds maximum length`, + }, + }, + cellEditorParams: { + skyComponentProperties: { + maxlength: 10, + }, + }, + editable: true, + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + cellRendererParams: { + skyComponentProperties: { + validator: (value) => value >= 18, + validatorMessage: `Age must be 18+`, + }, + }, + maxWidth: 60, + cellEditorParams: { + skyComponentProperties: { + min: 18, + }, + }, + editable: true, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + editable: true, + cellEditorParams: ( + params: ICellEditorParams + ): { skyComponentProperties: SkyAgGridDatepickerProperties } => { + return { skyComponentProperties: { minDate: params.data.startDate } }; + }, + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + editable: true, + cellEditorParams: ( + params: ICellEditorParams + ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { + return { + skyComponentProperties: { + data: SKY_DEPARTMENTS, + selectionChange: ( + change: SkyAutocompleteSelectionChange + ): void => { + this.#departmentSelectionChange(change, params.node); + }, + }, + }; + }, + onCellValueChanged: (event: NewValueParams): void => { + if (event.newValue !== event.oldValue) { + this.#clearJobTitle(event.node); + } + }, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + editable: true, + cellEditorParams: ( + params: ICellEditorParams + ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { + const selectedDepartment: string = + params.data && + params.data.department && + params.data.department.name; + const editParams: { + skyComponentProperties: SkyAgGridAutocompleteProperties; + } = { skyComponentProperties: { data: [] } }; + + if (selectedDepartment) { + editParams.skyComponentProperties.data = + SKY_JOB_TITLES[selectedDepartment]; + } + return editParams; + }, + }, + { + colId: 'validationCurrency', + field: 'validationCurrency', + headerName: 'Validation currency', + type: [SkyCellType.CurrencyValidator], + editable: true, + }, + { + colId: 'validationDate', + field: 'validationDate', + headerName: 'Validation date', + type: [SkyCellType.Date, SkyCellType.Validator], + cellRendererParams: { + skyComponentProperties: { + validator: (value: Date) => + !!value && value > new Date(1985, 9, 26), + validatorMessage: 'Please enter a future date', + }, + }, + editable: true, + }, + ]; + this.gridData = this.context.gridData; + this.gridOptions = { + columnDefs: this.columnDefs, + onGridReady: (gridReadyEvent) => this.onGridReady(gridReadyEvent), + }; + this.gridOptions = this.#agGridService.getEditableGridOptions({ + gridOptions: this.gridOptions, + }); + this.#changeDetector.markForCheck(); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.#gridApi = gridReadyEvent.api; + this.#gridApi.sizeColumnsToFit(); + this.#changeDetector.markForCheck(); + } + + #departmentSelectionChange( + change: SkyAutocompleteSelectionChange, + node: RowNode + ): void { + if (change.selectedItem && change.selectedItem !== node.data.department) { + this.#clearJobTitle(node); + } + } + + #clearJobTitle(node: RowNode | null): void { + if (node) { + node.data.jobTitle = undefined; + this.#changeDetector.markForCheck(); + if (this.#gridApi) { + this.#gridApi.refreshCells({ rowNodes: [node] }); + } + } + } +} diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo.component.html b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo.component.html new file mode 100644 index 0000000000..40ded45897 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo.component.html @@ -0,0 +1,31 @@ +
    + + + + + + + + + + + + + +
    diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo.component.ts new file mode 100644 index 0000000000..a78c08e32c --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo.component.ts @@ -0,0 +1,219 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, +} from '@angular/core'; +import { SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { SkyModalCloseArgs, SkyModalService } from '@skyux/modals'; + +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + ValueFormatterParams, +} from 'ag-grid-community'; + +import { SkyDataEntryGridContextMenuComponent } from './data-entry-grid-docs-demo-context-menu.component'; +import { SKY_AG_GRID_DEMO_DATA } from './data-entry-grid-docs-demo-data'; +import { SkyDataEntryGridEditModalContext } from './data-entry-grid-docs-demo-edit-modal-context'; +import { SkyDataEntryGridEditModalComponent } from './data-entry-grid-docs-demo-edit-modal.component'; +import { InlineHelpComponent } from './inline-help.component'; + +@Component({ + selector: 'app-data-entry-grid-docs-demo', + templateUrl: './data-entry-grid-docs-demo.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SkyDataEntryGridDemoComponent { + public gridData = SKY_AG_GRID_DEMO_DATA; + public columnDefs: ColDef[] = [ + { + field: 'selected', + type: SkyCellType.RowSelector, + }, + { + colId: 'context', + headerName: '', + maxWidth: 50, + sortable: false, + cellRenderer: SkyDataEntryGridContextMenuComponent, + }, + { + field: 'name', + headerName: 'Name', + type: SkyCellType.Text, + cellRendererParams: { + skyComponentProperties: { + validator: (value): boolean => String(value).length <= 10, + validatorMessage: `Value exceeds maximum length`, + }, + }, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + cellRendererParams: { + skyComponentProperties: { + validator: (value) => value >= 18, + validatorMessage: `Age must be 18+`, + }, + }, + maxWidth: 60, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + valueFormatter: this.#endDateFormatter, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + colId: 'validationCurrency', + field: 'validationCurrency', + headerName: 'Validation currency', + type: [SkyCellType.CurrencyValidator], + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + colId: 'validationDate', + field: 'validationDate', + headerName: 'Validation date', + type: [SkyCellType.Date, SkyCellType.Validator], + cellRendererParams: { + skyComponentProperties: { + validator: (value: Date) => !!value && value > new Date(1985, 9, 26), + validatorMessage: 'Please enter a future date', + }, + }, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + ]; + + public gridApi: GridApi | undefined; + public gridOptions: GridOptions; + public searchText = ''; + public noRowsTemplate = `
    No results found.
    `; + + #agGridService: SkyAgGridService; + #modalService: SkyModalService; + #changeDetection: ChangeDetectorRef; + + constructor( + agGridService: SkyAgGridService, + modalService: SkyModalService, + changeDetection: ChangeDetectorRef + ) { + this.#agGridService = agGridService; + this.#modalService = modalService; + this.#changeDetection = changeDetection; + + this.gridOptions = { + columnDefs: this.columnDefs, + onGridReady: (gridReadyEvent) => this.onGridReady(gridReadyEvent), + }; + + this.gridOptions = this.#agGridService.getGridOptions({ + gridOptions: this.gridOptions, + }); + this.#changeDetection.markForCheck(); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.gridApi = gridReadyEvent.api; + this.gridApi.sizeColumnsToFit(); + this.#changeDetection.markForCheck(); + } + + public openModal(): void { + const context = new SkyDataEntryGridEditModalContext(); + context.gridData = this.gridData; + + const options = { + providers: [ + { provide: SkyDataEntryGridEditModalContext, useValue: context }, + ], + ariaDescribedBy: 'docs-edit-grid-modal-content', + size: 'large', + }; + + const modalInstance = this.#modalService.open( + SkyDataEntryGridEditModalComponent, + options + ); + + modalInstance.closed.subscribe((result: SkyModalCloseArgs) => { + if (result.reason === 'cancel' || result.reason === 'close') { + alert('Edits canceled!'); + } else { + this.gridData = result.data; + if (this.gridApi) { + this.gridApi.refreshCells(); + } + alert('Saving data!'); + } + }); + } + + public searchApplied(searchText: string | void): void { + if (searchText) { + this.searchText = searchText; + } else { + this.searchText = ''; + } + if (this.gridApi) { + this.gridApi.setQuickFilter(this.searchText); + const displayedRowCount = this.gridApi.getDisplayedRowCount(); + if (displayedRowCount > 0) { + this.gridApi.hideOverlay(); + } else { + this.gridApi.showNoRowsOverlay(); + } + } + } + + #endDateFormatter(params: ValueFormatterParams): string { + const dateConfig = { year: 'numeric', month: '2-digit', day: '2-digit' }; + return params.value + ? params.value.toLocaleDateString('en-us', dateConfig) + : 'N/A'; + } +} diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo.module.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo.module.ts new file mode 100644 index 0000000000..dcb6c41ce1 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core'; +import { SkyAgGridModule } from '@skyux/ag-grid'; +import { SkyHelpInlineModule } from '@skyux/indicators'; +import { SkyToolbarModule } from '@skyux/layout'; +import { SkySearchModule } from '@skyux/lookup'; +import { SkyModalModule } from '@skyux/modals'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { AgGridModule } from 'ag-grid-angular'; + +import { SkyDataEntryGridContextMenuComponent } from './data-entry-grid-docs-demo-context-menu.component'; +import { SkyDataEntryGridEditModalComponent } from './data-entry-grid-docs-demo-edit-modal.component'; +import { SkyDataEntryGridDemoComponent } from './data-entry-grid-docs-demo.component'; +import { InlineHelpComponent } from './inline-help.component'; + +@NgModule({ + declarations: [ + SkyDataEntryGridContextMenuComponent, + SkyDataEntryGridDemoComponent, + SkyDataEntryGridEditModalComponent, + InlineHelpComponent, + ], + imports: [ + SkyToolbarModule, + SkySearchModule, + AgGridModule, + SkyAgGridModule, + SkyModalModule, + SkyDropdownModule, + SkyHelpInlineModule, + ], + exports: [SkyDataEntryGridDemoComponent], +}) +export class SkyDataEntryGridDocsDemoModule {} diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/inline-help.component.html b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/inline-help.component.html new file mode 100644 index 0000000000..bd7eeb3e98 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/inline-help.component.html @@ -0,0 +1,4 @@ + diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/inline-help.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/inline-help.component.ts new file mode 100644 index 0000000000..59eb31ed05 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/inline-help/inline-help.component.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyAgGridHeaderInfo } from '@skyux/ag-grid'; + +@Component({ + selector: 'app-inline-help', + templateUrl: './inline-help.component.html', + styles: [ + ` + :host { + display: block; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InlineHelpComponent { + readonly #displayName: string; + + constructor(headerInfo: SkyAgGridHeaderInfo) { + this.#displayName = headerInfo.displayName; + } + + public onHelpClick(): void { + alert(`Help was clicked for ${this.#displayName}.`); + } +} diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/data-grid-demo-data.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/data-grid-demo-data.ts new file mode 100644 index 0000000000..2ddd015cbf --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/data-grid-demo-data.ts @@ -0,0 +1,149 @@ +export interface SkyAutocompleteOption { + id: string; + name: string; +} + +export const SKY_DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const SKY_JOB_TITLES: { [name: string]: SkyAutocompleteOption[] } = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface SkyAgGridDemoRow { + selected: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: SkyAutocompleteOption; + jobTitle?: SkyAutocompleteOption; +} + +export const SKY_AG_GRID_DEMO_DATA = [ + { + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: SKY_DEPARTMENTS[3], + jobTitle: SKY_JOB_TITLES['Customer Support'][1], + }, + { + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: SKY_DEPARTMENTS[2], + jobTitle: SKY_JOB_TITLES['Engineering'][2], + }, + { + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: SKY_DEPARTMENTS[1], + }, + { + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: SKY_DEPARTMENTS[2], + jobTitle: SKY_JOB_TITLES['Engineering'][4], + }, + { + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: SKY_DEPARTMENTS[0], + jobTitle: SKY_JOB_TITLES['Marketing'][2], + }, + { + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: SKY_DEPARTMENTS[2], + jobTitle: SKY_JOB_TITLES['Engineering'][0], + }, + { + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: SKY_DEPARTMENTS[2], + jobTitle: SKY_JOB_TITLES['Engineering'][3], + }, +]; diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/data-grid-demo.component.html b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/data-grid-demo.component.html new file mode 100644 index 0000000000..423eeac270 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/data-grid-demo.component.html @@ -0,0 +1,22 @@ +
    + + + + + + + + + + +
    diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/data-grid-demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/data-grid-demo.component.ts new file mode 100644 index 0000000000..a1eb7b7a94 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/data-grid-demo.component.ts @@ -0,0 +1,136 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, +} from '@angular/core'; +import { SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; + +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + ValueFormatterParams, +} from 'ag-grid-community'; + +import { SKY_AG_GRID_DEMO_DATA } from './data-grid-demo-data'; +import { InlineHelpComponent } from './inline-help.component'; + +@Component({ + selector: 'app-data-grid-demo', + templateUrl: './data-grid-demo.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DataGridDemoComponent { + public columnDefs: ColDef[] = [ + { + field: 'selected', + type: SkyCellType.RowSelector, + }, + { + field: 'name', + headerName: 'Name', + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + maxWidth: 60, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + valueFormatter: this.#endDateFormatter, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + ]; + + public gridApi: GridApi | undefined; + public gridData = SKY_AG_GRID_DEMO_DATA; + public gridOptions: GridOptions; + public searchText = ''; + public noRowsTemplate: string; + + #agGridService: SkyAgGridService; + #changeDetector: ChangeDetectorRef; + + constructor( + agGridService: SkyAgGridService, + changeDetector: ChangeDetectorRef + ) { + this.#agGridService = agGridService; + this.#changeDetector = changeDetector; + this.noRowsTemplate = `
    No results found.
    `; + this.gridOptions = this.#agGridService.getGridOptions({ + gridOptions: { + columnDefs: this.columnDefs, + onGridReady: this.onGridReady.bind(this), + }, + }); + this.#changeDetector.markForCheck(); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.gridApi = gridReadyEvent.api; + this.gridApi.sizeColumnsToFit(); + this.#changeDetector.markForCheck(); + } + + public searchApplied(searchText: string | void): void { + if (searchText) { + this.searchText = searchText; + } else { + this.searchText = ''; + } + if (this.gridApi) { + this.gridApi.setQuickFilter(this.searchText); + const displayedRowCount = this.gridApi.getDisplayedRowCount(); + if (displayedRowCount > 0) { + this.gridApi.hideOverlay(); + } else { + this.gridApi.showNoRowsOverlay(); + } + } + } + + #endDateFormatter(params: ValueFormatterParams): string { + const dateConfig = { year: 'numeric', month: '2-digit', day: '2-digit' }; + return params.value + ? params.value.toLocaleDateString('en-us', dateConfig) + : 'N/A'; + } +} diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/data-grid-demo.module.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/data-grid-demo.module.ts new file mode 100644 index 0000000000..cb35a2616c --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/data-grid-demo.module.ts @@ -0,0 +1,31 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { SkyAgGridModule } from '@skyux/ag-grid'; +import { + SkyDataManagerModule, + SkyDataManagerService, +} from '@skyux/data-manager'; +import { SkyHelpInlineModule } from '@skyux/indicators'; +import { SkyToolbarModule } from '@skyux/layout'; +import { SkySearchModule } from '@skyux/lookup'; + +import { AgGridModule } from 'ag-grid-angular'; + +import { DataGridDemoComponent } from './data-grid-demo.component'; +import { InlineHelpComponent } from './inline-help.component'; + +@NgModule({ + declarations: [DataGridDemoComponent, InlineHelpComponent], + exports: [DataGridDemoComponent], + imports: [ + CommonModule, + SkyToolbarModule, + SkySearchModule, + SkyDataManagerModule, + SkyAgGridModule, + AgGridModule, + SkyHelpInlineModule, + ], + providers: [SkyDataManagerService], +}) +export class DataGridDemoModule {} diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/inline-help.component.html b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/inline-help.component.html new file mode 100644 index 0000000000..bd7eeb3e98 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/inline-help.component.html @@ -0,0 +1,4 @@ + diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/inline-help.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/inline-help.component.ts new file mode 100644 index 0000000000..59eb31ed05 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-grid/inline-help/inline-help.component.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyAgGridHeaderInfo } from '@skyux/ag-grid'; + +@Component({ + selector: 'app-inline-help', + templateUrl: './inline-help.component.html', + styles: [ + ` + :host { + display: block; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InlineHelpComponent { + readonly #displayName: string; + + constructor(headerInfo: SkyAgGridHeaderInfo) { + this.#displayName = headerInfo.displayName; + } + + public onHelpClick(): void { + alert(`Help was clicked for ${this.#displayName}.`); + } +} diff --git a/apps/code-examples/src/app/features/ag-grid.module.ts b/apps/code-examples/src/app/features/ag-grid.module.ts index b51839aa63..44eda9435f 100644 --- a/apps/code-examples/src/app/features/ag-grid.module.ts +++ b/apps/code-examples/src/app/features/ag-grid.module.ts @@ -5,6 +5,8 @@ import { SkyDataEntryGridDemoComponent as DataEntryGridBasicDataEntryGridDocsDem import { SkyDataEntryGridDocsDemoModule as DataEntryGridBasicDataEntryGridDocsDemoModule } from '../code-examples/ag-grid/data-entry-grid/basic/data-entry-grid-docs-demo.module'; import { SkyDataManagerDataEntryGridDemoComponent as DataEntryGridDataManagerAddedDataManagerDataEntryGridDocsDemoComponent } from '../code-examples/ag-grid/data-entry-grid/data-manager-added/data-manager-data-entry-grid-docs-demo.component'; import { SkyDataManagerDataEntryGridDocsDemoModule as DataEntryGridDataManagerAddedDataManagerDataEntryGridDocsDemoModule } from '../code-examples/ag-grid/data-entry-grid/data-manager-added/data-manager-data-entry-grid-docs-demo.module'; +import { SkyDataEntryGridDemoComponent as DataEntryGridInlineHelpDataEntryGridDocsDemoComponent } from '../code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo.component'; +import { SkyDataEntryGridDocsDemoModule as DataEntryGridInlineHelpDataEntryGridDocsDemoModule } from '../code-examples/ag-grid/data-entry-grid/inline-help/data-entry-grid-docs-demo.module'; import { SkyBasicMultiselectDataGridDemoComponent as DataGridBasicMultiselectDataGridDocsDemoComponent } from '../code-examples/ag-grid/data-grid/basic-multiselect/basic-multiselect-data-grid-docs-demo.component'; import { SkyBasicMultiselectDataGridDocsDemoModule } from '../code-examples/ag-grid/data-grid/basic-multiselect/basic-multiselect-data-grid-docs-demo.module'; import { SkyBasicDataGridDemoComponent as DataGridBasicDataGridDocsDemoComponent } from '../code-examples/ag-grid/data-grid/basic/basic-data-grid-docs-demo.component'; @@ -13,6 +15,8 @@ import { SkyDataManagerMultiselectDataGridDemoComponent as DataGridDataManagerAd import { SkyDataManagerMultiselectDataGridDocsDemoModule } from '../code-examples/ag-grid/data-grid/data-manager-multiselect/data-manager-multiselect-data-grid-docs-demo.module'; import { SkyDataManagerDataGridDemoComponent as DataGridDataManagerAddedDataManagerDataGridDocsDemoComponent } from '../code-examples/ag-grid/data-grid/data-manager/data-manager-data-grid-docs-demo.component'; import { SkyDataManagerDataGridDocsDemoModule } from '../code-examples/ag-grid/data-grid/data-manager/data-manager-data-grid-docs-demo.module'; +import { DataGridDemoComponent as InlineHelpDataGridDemoComponent } from '../code-examples/ag-grid/data-grid/inline-help/data-grid-demo.component'; +import { DataGridDemoModule as InlineHelpDataGridDemoModule } from '../code-examples/ag-grid/data-grid/inline-help/data-grid-demo.module'; import { SkyTopScrollDataGridDemoComponent } from '../code-examples/ag-grid/data-grid/top-scroll/top-scroll-data-grid-demo.component'; import { SkyTopScrollDataGridDemoModule } from '../code-examples/ag-grid/data-grid/top-scroll/top-scroll-data-grid-demo.module'; @@ -26,6 +30,10 @@ const routes: Routes = [ component: DataEntryGridDataManagerAddedDataManagerDataEntryGridDocsDemoComponent, }, + { + path: 'data-entry-grid/inline-help', + component: DataEntryGridInlineHelpDataEntryGridDocsDemoComponent, + }, { path: 'data-grid/basic', component: DataGridBasicDataGridDocsDemoComponent, @@ -47,6 +55,10 @@ const routes: Routes = [ path: 'data-grid/top-scroll', component: SkyTopScrollDataGridDemoComponent, }, + { + path: 'data-grid/inline-help', + component: InlineHelpDataGridDemoComponent, + }, ]; @NgModule({ @@ -59,7 +71,9 @@ export class AgGridFeatureRoutingModule {} imports: [ DataEntryGridBasicDataEntryGridDocsDemoModule, DataEntryGridDataManagerAddedDataManagerDataEntryGridDocsDemoModule, + DataEntryGridInlineHelpDataEntryGridDocsDemoModule, DataGridBasicBasicDataGridDocsDemoModule, + InlineHelpDataGridDemoModule, SkyBasicMultiselectDataGridDocsDemoModule, SkyDataManagerDataGridDocsDemoModule, SkyDataManagerMultiselectDataGridDocsDemoModule, diff --git a/apps/e2e/ag-grid-storybook-e2e/src/e2e/ag-grid-stories.component.cy.ts b/apps/e2e/ag-grid-storybook-e2e/src/e2e/ag-grid-stories.component.cy.ts index 172d4ff8e4..a6bbc38600 100644 --- a/apps/e2e/ag-grid-storybook-e2e/src/e2e/ag-grid-stories.component.cy.ts +++ b/apps/e2e/ag-grid-storybook-e2e/src/e2e/ag-grid-stories.component.cy.ts @@ -34,6 +34,22 @@ describe(`ag-grid-storybook`, () => { .should('contain.text', 'Expected a number between 1 and 18.') .end() + // Expect inline help buttons to be visible in three grids. + .get('#row-delete [col-id="name"] button.sky-help-inline') + .should('exist') + .should('be.visible') + .end() + + .get('#back-to-top [col-id="name"] button.sky-help-inline') + .should('exist') + .should('be.visible') + .end() + + .get('#validation [col-id="name"] button.sky-help-inline') + .should('exist') + .should('be.visible') + .end() + .get('#root') .should('exist') .should('be.visible') diff --git a/apps/e2e/ag-grid-storybook-e2e/src/e2e/data-entry-grid.component.cy.ts b/apps/e2e/ag-grid-storybook-e2e/src/e2e/data-entry-grid.component.cy.ts index c49bb0a469..c4af7f38f5 100644 --- a/apps/e2e/ag-grid-storybook-e2e/src/e2e/data-entry-grid.component.cy.ts +++ b/apps/e2e/ag-grid-storybook-e2e/src/e2e/data-entry-grid.component.cy.ts @@ -23,20 +23,20 @@ describe('ag-grid-storybook', () => { .end() // Activate a date field without the calendar. - .get('#editDate div[row-id="aparilu01"] > div[col-id="birthday"]') + .get('#editDate div[row-id="bankser01"] > div[col-id="birthday"]') .should('be.visible') .click() .end() // Activate a date field. .get( - '#editDateWithCalendar div[row-id="berrayo01"] > div[col-id="birthday"]' + '#editDateWithCalendar div[row-id="blylebe01"] > div[col-id="birthday"]' ) .should('be.visible') .click() .end() - // Open the calendar. + // Open the calendar and verify 04/06/1951 is selected. .get( '#editDateWithCalendar .ag-popup-editor button[aria-label="Select date"]' ) @@ -45,7 +45,23 @@ describe('ag-grid-storybook', () => { .end() .get('.ag-custom-component-popup .sky-datepicker-btn-selected') .should('be.visible') - .should('contain.text', '12') + .should('contain.text', '06') + .end() + + // Expect inline help buttons to be visible in three grids. + .get('#editDateWithCalendar [col-id="name"] button.sky-help-inline') + .should('exist') + .should('be.visible') + .end() + + .get('#editDate [col-id="name"] button.sky-help-inline') + .should('exist') + .should('be.visible') + .end() + + .get('#editLookup [col-id="name"] button.sky-help-inline') + .should('exist') + .should('be.visible') .end() // Screenshot the three grids with active editors. @@ -80,7 +96,7 @@ describe('ag-grid-storybook', () => { .waitForFaAndBbFonts() // Activate a text field. - .get('#editText div[row-id="bankser01"] > div[col-id="name"]') + .get('#editText div[row-id="benchjo01"] > div[col-id="name"]') .should('be.visible') .click() .end() diff --git a/apps/e2e/ag-grid-storybook/src/app/ag-grid/ag-grid-stories.component.ts b/apps/e2e/ag-grid-storybook/src/app/ag-grid/ag-grid-stories.component.ts index 150718e1c2..f2c3a54418 100644 --- a/apps/e2e/ag-grid-storybook/src/app/ag-grid/ag-grid-stories.component.ts +++ b/apps/e2e/ag-grid-storybook/src/app/ag-grid/ag-grid-stories.component.ts @@ -210,7 +210,7 @@ export class AgGridStoriesComponent // Scroll down to show the back-to-top button. this.#doc .querySelector( - '#back-to-top .sky-ag-grid-row-hunteca01 [col-id="name"]' + '#back-to-top .sky-ag-grid-row-johnsra05 [col-id="name"]' ) .scrollIntoView(); @@ -226,7 +226,7 @@ export class AgGridStoriesComponent // Trigger validation popover to show up. this.#doc .querySelector( - '#validation [row-index="0"] [col-id="seasons_played"]' + '#validation .sky-ag-grid-row-martipe02 [col-id="seasons_played"]' ) .dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' })); diff --git a/apps/e2e/ag-grid-storybook/src/app/ag-grid/ag-grid-stories.module.ts b/apps/e2e/ag-grid-storybook/src/app/ag-grid/ag-grid-stories.module.ts index 01b554875d..728d8f4513 100644 --- a/apps/e2e/ag-grid-storybook/src/app/ag-grid/ag-grid-stories.module.ts +++ b/apps/e2e/ag-grid-storybook/src/app/ag-grid/ag-grid-stories.module.ts @@ -10,6 +10,8 @@ import { SkyThemeModule, SkyThemeService } from '@skyux/theme'; import { AgGridModule } from 'ag-grid-angular'; +import { InlineHelpModule } from '../shared/inline-help/inline-help.module'; + import { AgGridStoriesComponent } from './ag-grid-stories.component'; import { ContextMenuComponent } from './context-menu.component'; @@ -21,6 +23,7 @@ const routes: Routes = [{ path: '', component: AgGridStoriesComponent }]; RouterModule.forChild(routes), SkyAgGridModule, AgGridModule, + InlineHelpModule, SkyDropdownModule, SkyBackToTopModule, SkyThemeModule, diff --git a/apps/e2e/ag-grid-storybook/src/app/data-entry-grid/data-entry-grid.component.ts b/apps/e2e/ag-grid-storybook/src/app/data-entry-grid/data-entry-grid.component.ts index a37d7f857c..31d2b3e5c3 100644 --- a/apps/e2e/ag-grid-storybook/src/app/data-entry-grid/data-entry-grid.component.ts +++ b/apps/e2e/ag-grid-storybook/src/app/data-entry-grid/data-entry-grid.component.ts @@ -20,6 +20,7 @@ import { BehaviorSubject, Observable, Subscription, combineLatest } from 'rxjs'; import { delay, filter, map } from 'rxjs/operators'; import { columnDefinitions, data } from '../shared/baseball-players-data'; +import { InlineHelpComponent } from '../shared/inline-help/inline-help.component'; type DataSet = { id: string; data: any[] }; @@ -130,6 +131,12 @@ export class DataEntryGridComponent columnDefs = columnDefinitions .slice(tripleCrownIndex, tripleCrownIndex + 3) .map((col) => { + if (col.field === 'mvp') { + col.headerComponentParams = { + inlineHelpComponent: InlineHelpComponent, + }; + col.initialSort = 'desc'; + } return { ...col, editable: true, diff --git a/apps/e2e/ag-grid-storybook/src/app/data-manager/data-manager.module.ts b/apps/e2e/ag-grid-storybook/src/app/data-manager/data-manager.module.ts index a46f038abd..aad4a801b4 100644 --- a/apps/e2e/ag-grid-storybook/src/app/data-manager/data-manager.module.ts +++ b/apps/e2e/ag-grid-storybook/src/app/data-manager/data-manager.module.ts @@ -11,6 +11,8 @@ import { SkyCheckboxModule, SkyRadioModule } from '@skyux/forms'; import { AgGridModule } from 'ag-grid-angular'; +import { InlineHelpModule } from '../shared/inline-help/inline-help.module'; + import { DataManagerComponent } from './data-manager.component'; const routes: Routes = [{ path: '', component: DataManagerComponent }]; @@ -25,6 +27,7 @@ const routes: Routes = [{ path: '', component: DataManagerComponent }]; SkyRadioModule, ReactiveFormsModule, AgGridModule, + InlineHelpModule, ], exports: [DataManagerComponent], providers: [SkyDataManagerService], diff --git a/apps/e2e/ag-grid-storybook/src/app/shared/baseball-players-data.ts b/apps/e2e/ag-grid-storybook/src/app/shared/baseball-players-data.ts index 8b6e8b9a49..1535e234d9 100644 --- a/apps/e2e/ag-grid-storybook/src/app/shared/baseball-players-data.ts +++ b/apps/e2e/ag-grid-storybook/src/app/shared/baseball-players-data.ts @@ -2,6 +2,8 @@ import { SkyCellType } from '@skyux/ag-grid'; import { ColDef } from 'ag-grid-community'; +import { InlineHelpComponent } from './inline-help/inline-help.component'; + export const columnDefinitions: ColDef[] = [ { field: 'id', @@ -16,6 +18,10 @@ export const columnDefinitions: ColDef[] = [ sortable: true, type: SkyCellType.Text, minWidth: 200, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + initialSort: 'asc', }, { field: 'birthday', @@ -24,6 +30,9 @@ export const columnDefinitions: ColDef[] = [ sortable: true, type: SkyCellType.Date, minWidth: 300, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, ...[ ['seasons_played', 'Seasons Played'], diff --git a/apps/e2e/ag-grid-storybook/src/app/shared/inline-help/inline-help.component.html b/apps/e2e/ag-grid-storybook/src/app/shared/inline-help/inline-help.component.html new file mode 100644 index 0000000000..bd7eeb3e98 --- /dev/null +++ b/apps/e2e/ag-grid-storybook/src/app/shared/inline-help/inline-help.component.html @@ -0,0 +1,4 @@ + diff --git a/apps/e2e/ag-grid-storybook/src/app/shared/inline-help/inline-help.component.ts b/apps/e2e/ag-grid-storybook/src/app/shared/inline-help/inline-help.component.ts new file mode 100644 index 0000000000..07931e84af --- /dev/null +++ b/apps/e2e/ag-grid-storybook/src/app/shared/inline-help/inline-help.component.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyAgGridHeaderInfo } from '@skyux/ag-grid'; + +@Component({ + selector: 'app-inline-help', + templateUrl: './inline-help.component.html', + styles: [ + ` + :host { + display: block; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InlineHelpComponent { + readonly #displayName: string; + + constructor({ displayName }: SkyAgGridHeaderInfo) { + this.#displayName = displayName; + } + + public onHelpClick(): void { + alert(`Help was clicked for ${this.#displayName}.`); + } +} diff --git a/apps/e2e/ag-grid-storybook/src/app/shared/inline-help/inline-help.module.ts b/apps/e2e/ag-grid-storybook/src/app/shared/inline-help/inline-help.module.ts new file mode 100644 index 0000000000..9678f4007f --- /dev/null +++ b/apps/e2e/ag-grid-storybook/src/app/shared/inline-help/inline-help.module.ts @@ -0,0 +1,12 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { SkyHelpInlineModule } from '@skyux/indicators'; + +import { InlineHelpComponent } from './inline-help.component'; + +@NgModule({ + declarations: [InlineHelpComponent], + exports: [InlineHelpComponent], + imports: [CommonModule, SkyHelpInlineModule], +}) +export class InlineHelpModule {} diff --git a/apps/playground/src/app/components/ag-grid/data-manager-large/data-manager-large.component.html b/apps/playground/src/app/components/ag-grid/data-manager-large/data-manager-large.component.html index 52dbcf88c9..220d4c3a81 100644 --- a/apps/playground/src/app/components/ag-grid/data-manager-large/data-manager-large.component.html +++ b/apps/playground/src/app/components/ag-grid/data-manager-large/data-manager-large.component.html @@ -7,6 +7,11 @@ icon="arrows-h" label="Top scroll" > +
    { this.isActive$.next(false); this.enableTopScroll = value.enableTopScroll; + this.useColumnGroups = value.useColumnGroups; this.domLayout = value.domLayout; this.applyGridOptions(); setTimeout(() => this.isActive$.next(true)); @@ -126,7 +133,12 @@ export class DataManagerLargeComponent implements OnInit { private applyGridOptions() { this.gridOptions = this.agGridService.getGridOptions({ gridOptions: { - columnDefs: columnDefinitions, + defaultColGroupDef: { + columnGroupShow: 'open', + }, + columnDefs: this.useColumnGroups + ? columnDefinitionsGrouped + : columnDefinitions, columnTypes: { custom_link: { cellRendererFramework: CustomLinkComponent, diff --git a/apps/playground/src/app/components/ag-grid/data-manager-large/data-manager-large.module.ts b/apps/playground/src/app/components/ag-grid/data-manager-large/data-manager-large.module.ts index 3da59b346c..8c71e4f0cb 100644 --- a/apps/playground/src/app/components/ag-grid/data-manager-large/data-manager-large.module.ts +++ b/apps/playground/src/app/components/ag-grid/data-manager-large/data-manager-large.module.ts @@ -8,17 +8,24 @@ import { SkyDataManagerService, } from '@skyux/data-manager'; import { SkyCheckboxModule, SkyRadioModule } from '@skyux/forms'; -import { SkyIconModule } from '@skyux/indicators'; +import { SkyHelpInlineModule, SkyIconModule } from '@skyux/indicators'; import { AgGridModule } from 'ag-grid-angular'; import { CustomLinkComponent } from './custom-link/custom-link.component'; import { DataManagerLargeRoutingModule } from './data-manager-large-routing.module'; import { DataManagerLargeComponent } from './data-manager-large.component'; +import { GroupInlineHelpComponent } from './inline-help/group-inline-help.component'; +import { InlineHelpComponent } from './inline-help/inline-help.component'; import { LocalStorageConfigService } from './local-storage-config.service'; @NgModule({ - declarations: [DataManagerLargeComponent, CustomLinkComponent], + declarations: [ + DataManagerLargeComponent, + CustomLinkComponent, + GroupInlineHelpComponent, + InlineHelpComponent, + ], imports: [ AgGridModule, CommonModule, @@ -29,6 +36,7 @@ import { LocalStorageConfigService } from './local-storage-config.service'; SkyIconModule, SkyRadioModule, ReactiveFormsModule, + SkyHelpInlineModule, ], providers: [ SkyDataManagerService, diff --git a/apps/playground/src/app/components/ag-grid/data-manager-large/data-set-large.ts b/apps/playground/src/app/components/ag-grid/data-manager-large/data-set-large.ts index 8fb382944b..c97bf5fffb 100644 --- a/apps/playground/src/app/components/ag-grid/data-manager-large/data-set-large.ts +++ b/apps/playground/src/app/components/ag-grid/data-manager-large/data-set-large.ts @@ -1,7 +1,10 @@ // Source: https://github.com/metmuseum/openaccess import { SkyCellType } from '@skyux/ag-grid'; -import { ColDef } from 'ag-grid-community'; +import { ColDef, ColGroupDef } from 'ag-grid-community'; + +import { GroupInlineHelpComponent } from './inline-help/group-inline-help.component'; +import { InlineHelpComponent } from './inline-help/inline-help.component'; export const columnDefinitions: ColDef[] = [ { @@ -14,6 +17,9 @@ export const columnDefinitions: ColDef[] = [ headerName: 'Object Number', type: [], sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { field: 'is_highlight', @@ -32,24 +38,36 @@ export const columnDefinitions: ColDef[] = [ headerName: 'Is Public Domain', type: [], sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { field: 'object_id', headerName: 'Object ID', type: [], sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { field: 'gallery_number', headerName: 'Gallery Number', type: [], sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { field: 'department', headerName: 'Department', type: [], sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { field: 'accessionyear', @@ -62,12 +80,18 @@ export const columnDefinitions: ColDef[] = [ headerName: 'Object Name', type: [], sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { field: 'title', headerName: 'Title', type: [], sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { field: 'culture', @@ -122,12 +146,18 @@ export const columnDefinitions: ColDef[] = [ headerName: 'Artist Display Name', type: [], sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { field: 'artist_display_bio', headerName: 'Artist Display Bio', type: [], sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { field: 'artist_suffix', @@ -182,6 +212,9 @@ export const columnDefinitions: ColDef[] = [ headerName: 'Object Date', type: [], sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { field: 'object_begin_date', @@ -302,6 +335,9 @@ export const columnDefinitions: ColDef[] = [ headerName: 'Object Wikidata URL', type: ['custom_link'], sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { field: 'metadata_date', @@ -335,6 +371,237 @@ export const columnDefinitions: ColDef[] = [ }, ]; +export const columnDefinitionsGrouped: ColGroupDef[] = [ + { + headerName: 'Object Information', + headerGroupComponentParams: { + inlineHelpComponent: GroupInlineHelpComponent, + }, + children: [ + { + field: 'object_name', + headerName: 'Object Name', + type: [], + sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'department', + headerName: 'Department', + type: [], + sortable: true, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'is_public_domain', + headerName: 'Is Public Domain', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + ], + }, + { + headerName: 'Titular Information', + headerGroupComponentParams: { + inlineHelpComponent: GroupInlineHelpComponent, + }, + children: [ + { + field: 'title', + headerName: 'Title', + type: [], + sortable: true, + }, + { + field: 'culture', + headerName: 'Culture', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + { + field: 'period', + headerName: 'Period', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + ], + }, + { + headerName: 'Artist', + children: [ + { + field: 'artist_role', + headerName: 'Artist Role', + type: [], + sortable: true, + }, + { + field: 'artist_display_name', + headerName: 'Artist Display Name', + type: [], + sortable: true, + }, + { + field: 'artist_display_bio', + headerName: 'Artist Display Bio', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + { + field: 'artist_suffix', + headerName: 'Artist Suffix', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + { + field: 'artist_nationality', + headerName: 'Artist Nationality', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + { + field: 'artist_wikidata_url', + headerName: 'Artist Wikidata URL', + type: ['custom_link'], + sortable: true, + columnGroupShow: 'open', + }, + ], + }, + { + headerName: 'Collection', + children: [ + { + field: 'object_date', + headerName: 'Object Date', + type: [], + sortable: true, + }, + { + field: 'object_begin_date', + headerName: 'Object Begin Date', + type: [], + sortable: true, + }, + { + field: 'object_end_date', + headerName: 'Object End Date', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + { + field: 'medium', + headerName: 'Medium', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + { + field: 'dimensions', + headerName: 'Dimensions', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + ], + }, + { + headerName: 'Geography & Classification', + children: [ + { + field: 'geography_type', + headerName: 'Geography Type', + type: [], + sortable: true, + }, + { + field: 'city', + headerName: 'City', + type: [], + sortable: true, + }, + { + field: 'state', + headerName: 'State', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + { + field: 'county', + headerName: 'County', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + { + field: 'country', + headerName: 'Country', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + ], + }, + { + headerName: 'Metadata', + children: [ + { + field: 'classification', + headerName: 'Classification', + type: [], + sortable: true, + }, + { + field: 'rights_and_reproduction', + headerName: 'Rights and Reproduction', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + { + field: 'link_resource', + headerName: 'Link Resource', + type: ['custom_link'], + sortable: true, + columnGroupShow: 'open', + }, + { + field: 'object_wikidata_url', + headerName: 'Object Wikidata URL', + type: ['custom_link'], + sortable: true, + columnGroupShow: 'open', + }, + { + field: 'metadata_date', + headerName: 'Metadata Date', + type: [], + sortable: true, + columnGroupShow: 'open', + }, + { + field: 'tags_wikidata_url', + headerName: 'Tags Wikidata URL', + type: ['custom_link'], + sortable: true, + columnGroupShow: 'open', + }, + ], + }, +]; + export const data = [ { object_number: '24.109.38a\u2013c', diff --git a/apps/playground/src/app/components/ag-grid/data-manager-large/inline-help/group-inline-help.component.html b/apps/playground/src/app/components/ag-grid/data-manager-large/inline-help/group-inline-help.component.html new file mode 100644 index 0000000000..bd7eeb3e98 --- /dev/null +++ b/apps/playground/src/app/components/ag-grid/data-manager-large/inline-help/group-inline-help.component.html @@ -0,0 +1,4 @@ + diff --git a/apps/playground/src/app/components/ag-grid/data-manager-large/inline-help/group-inline-help.component.ts b/apps/playground/src/app/components/ag-grid/data-manager-large/inline-help/group-inline-help.component.ts new file mode 100644 index 0000000000..e025a97fde --- /dev/null +++ b/apps/playground/src/app/components/ag-grid/data-manager-large/inline-help/group-inline-help.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyAgGridHeaderGroupInfo } from '@skyux/ag-grid'; + +@Component({ + selector: 'app-group-inline-help', + templateUrl: './group-inline-help.component.html', + styles: [ + ` + :host { + display: block; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class GroupInlineHelpComponent { + constructor(public info: SkyAgGridHeaderGroupInfo) {} + + public onHelpClick(): void { + console.log(`Help was clicked for ${this.info.displayName}.`); + } +} diff --git a/apps/playground/src/app/components/ag-grid/data-manager-large/inline-help/inline-help.component.html b/apps/playground/src/app/components/ag-grid/data-manager-large/inline-help/inline-help.component.html new file mode 100644 index 0000000000..bd7eeb3e98 --- /dev/null +++ b/apps/playground/src/app/components/ag-grid/data-manager-large/inline-help/inline-help.component.html @@ -0,0 +1,4 @@ + diff --git a/apps/playground/src/app/components/ag-grid/data-manager-large/inline-help/inline-help.component.ts b/apps/playground/src/app/components/ag-grid/data-manager-large/inline-help/inline-help.component.ts new file mode 100644 index 0000000000..b8c2ccc58b --- /dev/null +++ b/apps/playground/src/app/components/ag-grid/data-manager-large/inline-help/inline-help.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyAgGridHeaderInfo } from '@skyux/ag-grid'; + +@Component({ + selector: 'app-inline-help', + templateUrl: './inline-help.component.html', + styles: [ + ` + :host { + display: block; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InlineHelpComponent { + constructor(public info: SkyAgGridHeaderInfo) {} + + public onHelpClick(): void { + console.log(`Help was clicked for ${this.info.displayName}.`); + } +} diff --git a/apps/playground/src/app/components/ag-grid/edit-complex-cells/edit-complex-cells.component.ts b/apps/playground/src/app/components/ag-grid/edit-complex-cells/edit-complex-cells.component.ts index 7b3084d0fd..a249140670 100644 --- a/apps/playground/src/app/components/ag-grid/edit-complex-cells/edit-complex-cells.component.ts +++ b/apps/playground/src/app/components/ag-grid/edit-complex-cells/edit-complex-cells.component.ts @@ -35,6 +35,7 @@ import { EditableGridOption, EditableGridRow, } from './edit-complex-cells-data'; +import { InlineHelpComponent } from './inline-help/inline-help.component'; @Component({ selector: 'app-edit-complex-cells-visual', @@ -92,6 +93,9 @@ export class EditComplexCellsComponent implements OnInit { minWidth: 220, editable: this.editMode, type: SkyCellType.Text, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { colId: 'language', @@ -103,6 +107,9 @@ export class EditComplexCellsComponent implements OnInit { values: ['English', 'Spanish', 'French', 'Portuguese', '(other)'], }, editable: this.editMode, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, }, { colId: 'validationAutocomplete', @@ -122,6 +129,8 @@ export class EditComplexCellsComponent implements OnInit { validatorMessage: 'Please choose an odd number option', }, }, + sortable: true, + filter: true, }, { colId: 'validationCurrency', @@ -130,6 +139,7 @@ export class EditComplexCellsComponent implements OnInit { maxWidth: 235, editable: this.editMode, type: [SkyCellType.CurrencyValidator], + sortable: true, }, { colId: 'validationDate', @@ -145,6 +155,7 @@ export class EditComplexCellsComponent implements OnInit { validatorMessage: 'Please enter a future date', }, }, + sortable: true, }, { colId: 'lookupSingle', diff --git a/apps/playground/src/app/components/ag-grid/edit-complex-cells/edit-complex-cells.module.ts b/apps/playground/src/app/components/ag-grid/edit-complex-cells/edit-complex-cells.module.ts index 3023ccb6b9..b000ab9233 100644 --- a/apps/playground/src/app/components/ag-grid/edit-complex-cells/edit-complex-cells.module.ts +++ b/apps/playground/src/app/components/ag-grid/edit-complex-cells/edit-complex-cells.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { SkyAgGridModule } from '@skyux/ag-grid'; +import { SkyHelpInlineModule } from '@skyux/indicators'; import { SkyToolbarModule } from '@skyux/layout'; import { AgGridModule } from 'ag-grid-angular'; @@ -8,9 +9,10 @@ import { AgGridModule } from 'ag-grid-angular'; import { CustomMultilineModule } from './custom-multiline/custom-multiline.module'; import { EditComplexCellsRoutingModule } from './edit-complex-cells-routing.module'; import { EditComplexCellsComponent } from './edit-complex-cells.component'; +import { InlineHelpComponent } from './inline-help/inline-help.component'; @NgModule({ - declarations: [EditComplexCellsComponent], + declarations: [EditComplexCellsComponent, InlineHelpComponent], imports: [ AgGridModule, CommonModule, @@ -18,6 +20,7 @@ import { EditComplexCellsComponent } from './edit-complex-cells.component'; EditComplexCellsRoutingModule, SkyAgGridModule, SkyToolbarModule, + SkyHelpInlineModule, ], }) export class EditComplexCellsModule { diff --git a/apps/playground/src/app/components/ag-grid/edit-complex-cells/inline-help/inline-help.component.html b/apps/playground/src/app/components/ag-grid/edit-complex-cells/inline-help/inline-help.component.html new file mode 100644 index 0000000000..bd7eeb3e98 --- /dev/null +++ b/apps/playground/src/app/components/ag-grid/edit-complex-cells/inline-help/inline-help.component.html @@ -0,0 +1,4 @@ + diff --git a/apps/playground/src/app/components/ag-grid/edit-complex-cells/inline-help/inline-help.component.ts b/apps/playground/src/app/components/ag-grid/edit-complex-cells/inline-help/inline-help.component.ts new file mode 100644 index 0000000000..7190e40433 --- /dev/null +++ b/apps/playground/src/app/components/ag-grid/edit-complex-cells/inline-help/inline-help.component.ts @@ -0,0 +1,19 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'app-inline-help', + templateUrl: './inline-help.component.html', + styles: [ + ` + :host { + display: block; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InlineHelpComponent { + public onHelpClick(): void { + console.log(`Help was clicked.`); + } +} diff --git a/libs/components/ag-grid/src/assets/locales/resources_en_US.json b/libs/components/ag-grid/src/assets/locales/resources_en_US.json index 149d0edde6..23d190c3e6 100644 --- a/libs/components/ag-grid/src/assets/locales/resources_en_US.json +++ b/libs/components/ag-grid/src/assets/locales/resources_en_US.json @@ -30,5 +30,13 @@ "sky_ag_grid_cell_editor_currency_aria_label": { "_description": "aria label for the currency cell editor", "message": "Editable currency {0} for row {1}" + }, + "sky_ag_grid_column_group_header_expand_aria_label": { + "_description": "aria label for the expand column group header button", + "message": "Expand column group {0}" + }, + "sky_ag_grid_column_group_header_collapse_aria_label": { + "_description": "aria label for the collapse column group header button", + "message": "Collapse column group {0}" } } diff --git a/libs/components/ag-grid/src/index.ts b/libs/components/ag-grid/src/index.ts index f8bedf2377..ecf08ad788 100644 --- a/libs/components/ag-grid/src/index.ts +++ b/libs/components/ag-grid/src/index.ts @@ -11,6 +11,10 @@ export * from './lib/modules/ag-grid/types/cell-type'; export * from './lib/modules/ag-grid/types/currency-properties'; export * from './lib/modules/ag-grid/types/datepicker-properties'; export * from './lib/modules/ag-grid/types/header-class'; +export * from './lib/modules/ag-grid/types/header-group-info'; +export * from './lib/modules/ag-grid/types/header-group-params'; +export * from './lib/modules/ag-grid/types/header-info'; +export * from './lib/modules/ag-grid/types/header-params'; export * from './lib/modules/ag-grid/types/lookup-properties'; export * from './lib/modules/ag-grid/types/number-properties'; export * from './lib/modules/ag-grid/types/sky-grid-options'; @@ -35,3 +39,5 @@ export { SkyAgGridRowDeleteDirective as λ13 } from './lib/modules/ag-grid/ag-gr export { SkyAgGridDataManagerAdapterDirective as λ14 } from './lib/modules/ag-grid/ag-grid-data-manager-adapter.directive'; export { SkyAgGridCellEditorLookupComponent as λ15 } from './lib/modules/ag-grid/cell-editors/cell-editor-lookup/cell-editor-lookup.component'; export { SkyAgGridCellRendererLookupComponent as λ16 } from './lib/modules/ag-grid/cell-renderers/cell-renderer-lookup/cell-renderer-lookup.component'; +export { SkyAgGridHeaderComponent as λ17 } from './lib/modules/ag-grid/header/header.component'; +export { SkyAgGridHeaderGroupComponent as λ18 } from './lib/modules/ag-grid/header/header-group.component'; diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.spec.ts index 5ecfee3d9d..90b76fce19 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.spec.ts @@ -15,11 +15,12 @@ import { Subject } from 'rxjs'; import { SkyAgGridAdapterService } from './ag-grid-adapter.service'; import { SkyAgGridWrapperComponent } from './ag-grid-wrapper.component'; -import { SkyAgGridModule } from './ag-grid.module'; import { EnableTopScroll, SkyAgGridFixtureComponent, } from './fixtures/ag-grid.component.fixture'; +import { SkyAgGridFixtureModule } from './fixtures/ag-grid.module.fixture'; +import { SecondInlineHelpComponent } from './fixtures/inline-help.component'; describe('SkyAgGridWrapperComponent', () => { let gridAdapterService: SkyAgGridAdapterService; @@ -37,7 +38,7 @@ describe('SkyAgGridWrapperComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [SkyAgGridModule], + imports: [SkyAgGridFixtureModule], }); gridWrapperFixture = TestBed.createComponent(SkyAgGridWrapperComponent); @@ -282,7 +283,7 @@ describe('SkyAgGridWrapperComponent via fixture', () => { it('should move the horizontal scroll based on enableTopScroll check, static data', async () => { TestBed.configureTestingModule({ - imports: [SkyAgGridModule], + imports: [SkyAgGridFixtureModule], providers: [ { provide: EnableTopScroll, @@ -309,7 +310,7 @@ describe('SkyAgGridWrapperComponent via fixture', () => { it('should move the horizontal scroll based on enableTopScroll check, async loading', async () => { TestBed.configureTestingModule({ - imports: [SkyAgGridModule], + imports: [SkyAgGridFixtureModule], }); gridWrapperFixture = TestBed.createComponent(SkyAgGridFixtureComponent); gridWrapperNativeElement = gridWrapperFixture.nativeElement; @@ -357,4 +358,80 @@ describe('SkyAgGridWrapperComponent via fixture', () => { 'ag-overlay', ]); }); + + it('should show inline help', async () => { + TestBed.configureTestingModule({ + imports: [SkyAgGridFixtureModule], + }); + gridWrapperFixture = TestBed.createComponent(SkyAgGridFixtureComponent); + gridWrapperNativeElement = gridWrapperFixture.nativeElement; + + gridWrapperFixture.detectChanges(); + await gridWrapperFixture.whenStable(); + + expect( + gridWrapperNativeElement.querySelector( + `[col-id="name"] .sky-control-help` + ) + ).toBeTruthy(); + expect( + gridWrapperNativeElement.querySelector( + `[col-id="value"] .sky-control-help` + ) + ).toBeTruthy(); + expect( + gridWrapperNativeElement + .querySelector(`[col-id="value"] .sky-control-help`) + .getAttribute('title') + ).toEqual('Current Value help'); + + gridWrapperFixture.componentInstance.agGrid.api.setColumnDefs([ + ...gridWrapperFixture.componentInstance.columnDefs.map((col) => { + switch (col.field) { + case 'name': + return { + ...col, + headerComponentParams: { + ...col.headerComponentParams, + inlineHelpComponent: undefined, + }, + }; + case 'value': + return { + ...col, + headerComponentParams: { + ...col.headerComponentParams, + inlineHelpComponent: SecondInlineHelpComponent, + }, + }; + case 'target': + return { + ...col, + hide: true, + }; + default: + return col; + } + }), + ]); + gridWrapperFixture.detectChanges(); + await gridWrapperFixture.whenStable(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + expect( + gridWrapperNativeElement.querySelector( + `[col-id="name"] .sky-control-help` + ) + ).toBeFalsy(); + expect( + gridWrapperNativeElement.querySelector( + `[col-id="value"] .sky-control-help` + ) + ).toBeTruthy(); + expect( + gridWrapperNativeElement + .querySelector(`[col-id="value"] .sky-control-help`) + .getAttribute('title') + ).toEqual('Current Value help replaced'); + }); }); diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid.module.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid.module.ts index 66ec06b369..e15ded1e81 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid.module.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid.module.ts @@ -2,7 +2,10 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { SkyViewkeeperModule } from '@skyux/core'; import { SkyDataManagerModule } from '@skyux/data-manager'; +import { SkyI18nModule } from '@skyux/i18n'; +import { SkyIconModule } from '@skyux/indicators'; import { SkyInlineDeleteModule } from '@skyux/layout'; +import { SkyThemeModule } from '@skyux/theme'; import { AgGridModule } from 'ag-grid-angular'; @@ -21,10 +24,14 @@ import { SkyAgGridCellRendererLookupModule } from './cell-renderers/cell-rendere import { SkyAgGridCellRendererRowSelectorModule } from './cell-renderers/cell-renderer-row-selector/cell-renderer-row-selector.module'; import { SkyAgGridCellRendererValidatorTooltipModule } from './cell-renderers/cell-renderer-validator-tooltip/cell-renderer-validator-tooltip.module'; import { SkyAgGridCellValidatorModule } from './cell-validator/ag-grid-cell-validator.module'; +import { SkyAgGridHeaderGroupComponent } from './header/header-group.component'; +import { SkyAgGridHeaderComponent } from './header/header.component'; @NgModule({ declarations: [ SkyAgGridDataManagerAdapterDirective, + SkyAgGridHeaderComponent, + SkyAgGridHeaderGroupComponent, SkyAgGridRowDeleteComponent, SkyAgGridRowDeleteDirective, SkyAgGridWrapperComponent, @@ -44,14 +51,19 @@ import { SkyAgGridCellValidatorModule } from './cell-validator/ag-grid-cell-vali SkyAgGridCellValidatorModule, SkyAgGridCellEditorTextModule, SkyDataManagerModule, + SkyI18nModule, + SkyIconModule, SkyInlineDeleteModule, SkyViewkeeperModule, + SkyThemeModule, ], exports: [ SkyAgGridDataManagerAdapterDirective, SkyAgGridRowDeleteComponent, SkyAgGridRowDeleteDirective, SkyAgGridWrapperComponent, + SkyAgGridHeaderComponent, + SkyAgGridHeaderGroupComponent, ], }) export class SkyAgGridModule {} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid.service.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid.service.ts index d982d45299..f2ad91abd3 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid.service.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid.service.ts @@ -27,6 +27,8 @@ import { SkyAgGridCellRendererCurrencyComponent } from './cell-renderers/cell-re import { SkyAgGridCellRendererLookupComponent } from './cell-renderers/cell-renderer-lookup/cell-renderer-lookup.component'; import { SkyAgGridCellRendererRowSelectorComponent } from './cell-renderers/cell-renderer-row-selector/cell-renderer-row-selector.component'; import { SkyAgGridCellRendererValidatorTooltipComponent } from './cell-renderers/cell-renderer-validator-tooltip/cell-renderer-validator-tooltip.component'; +import { SkyAgGridHeaderGroupComponent } from './header/header-group.component'; +import { SkyAgGridHeaderComponent } from './header/header.component'; import { SkyCellClass } from './types/cell-class'; import { SkyCellType } from './types/cell-type'; import { SkyHeaderClass } from './types/header-class'; @@ -183,7 +185,7 @@ export class SkyAgGridService implements OnDestroy { defaultGridOptions: GridOptions, providedGridOptions: GridOptions ): GridOptions { - const mergedGridOptions = { + const mergedGridOptions: GridOptions = { ...defaultGridOptions, ...providedGridOptions, components: { @@ -202,6 +204,10 @@ export class SkyAgGridService implements OnDestroy { // allow consumers to override all defaultColDef properties except cellClassRules, which we reserve for styling cellClassRules: defaultGridOptions.defaultColDef.cellClassRules, }, + defaultColGroupDef: { + ...defaultGridOptions.defaultColGroupDef, + ...providedGridOptions.defaultColGroupDef, + }, icons: { ...defaultGridOptions.icons, ...providedGridOptions.icons, @@ -366,12 +372,16 @@ export class SkyAgGridService implements OnDestroy { }, defaultColDef: { cellClassRules: editableCellClassRules, + headerComponent: SkyAgGridHeaderComponent, minWidth: 100, resizable: true, sortable: true, suppressKeyboardEvent: (keypress: SuppressKeyboardEventParams) => this.suppressTab(keypress), }, + defaultColGroupDef: { + headerGroupComponent: SkyAgGridHeaderGroupComponent, + }, domLayout: 'autoHeight', enterMovesDownAfterEdit: true, components: { diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/fixtures/ag-grid.component.fixture.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/fixtures/ag-grid.component.fixture.ts index ff3887f02d..e5f7b27719 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/fixtures/ag-grid.component.fixture.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/fixtures/ag-grid.component.fixture.ts @@ -9,12 +9,13 @@ import { } from '@angular/core'; import { AgGridAngular } from 'ag-grid-angular'; -import { GridOptions } from 'ag-grid-community'; +import { ColDef, GridOptions } from 'ag-grid-community'; import { SkyAgGridService } from '../ag-grid.service'; import { SkyCellType } from '../types/cell-type'; import { SKY_AG_GRID_DATA, SKY_AG_GRID_LOOKUP } from './ag-grid-data.fixture'; +import { FirstInlineHelpComponent } from './inline-help.component'; export const EnableTopScroll = new InjectionToken('EnableTopScroll'); @@ -28,7 +29,7 @@ export class SkyAgGridFixtureComponent implements OnInit { public agGrid: AgGridAngular; public gridData = SKY_AG_GRID_DATA; - public columnDefs = [ + public columnDefs: ColDef[] = [ { field: 'selected', headerName: '', @@ -39,6 +40,9 @@ export class SkyAgGridFixtureComponent implements OnInit { { field: 'name', headerName: 'First Name', + headerComponentParams: { + inlineHelpComponent: FirstInlineHelpComponent, + }, }, { field: 'nickname', @@ -51,35 +55,53 @@ export class SkyAgGridFixtureComponent implements OnInit { headerName: 'Current Value', editable: true, type: SkyCellType.Number, + headerComponentParams: { + inlineHelpComponent: FirstInlineHelpComponent, + }, }, { field: 'target', headerName: 'Goal', type: SkyCellType.Number, + headerComponentParams: { + inlineHelpComponent: FirstInlineHelpComponent, + }, }, { field: 'date', headerName: 'Completed Date', editable: true, type: SkyCellType.Date, + headerComponentParams: { + inlineHelpComponent: FirstInlineHelpComponent, + }, }, { field: 'currency', headerName: 'Currency amount', editable: true, type: SkyCellType.Currency, + headerComponentParams: { + inlineHelpComponent: FirstInlineHelpComponent, + }, }, { field: 'validNumber', headerName: 'Valid number', editable: true, type: SkyCellType.NumberValidator, + headerComponentParams: { + inlineHelpComponent: FirstInlineHelpComponent, + }, }, { field: 'validCurrency', headerName: 'Valid currency', editable: true, type: SkyCellType.Currency, + headerComponentParams: { + inlineHelpComponent: FirstInlineHelpComponent, + }, }, { field: 'validDate', @@ -95,6 +117,9 @@ export class SkyAgGridFixtureComponent implements OnInit { validatorMessage: 'Please enter a future date', }, }, + headerComponentParams: { + inlineHelpComponent: FirstInlineHelpComponent, + }, }, { colId: 'lookupSingle', @@ -116,6 +141,9 @@ export class SkyAgGridFixtureComponent implements OnInit { descriptorProperty: 'name', }, }, + headerComponentParams: { + inlineHelpComponent: FirstInlineHelpComponent, + }, }, { colId: 'lookupMultiple', @@ -138,6 +166,9 @@ export class SkyAgGridFixtureComponent implements OnInit { descriptorProperty: 'name', }, }, + headerComponentParams: { + inlineHelpComponent: FirstInlineHelpComponent, + }, }, ]; diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/fixtures/ag-grid.module.fixture.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/fixtures/ag-grid.module.fixture.ts index fb67584316..147b2929d7 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/fixtures/ag-grid.module.fixture.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/fixtures/ag-grid.module.fixture.ts @@ -13,6 +13,10 @@ import { SkyAgGridCellValidatorTooltipFixtureComponent } from './ag-grid-cell-va import { SkyAgGridDataManagerFixtureComponent } from './ag-grid-data-manager.component.fixture'; import { SkyAgGridRowDeleteFixtureComponent } from './ag-grid-row-delete.component.fixture'; import { SkyAgGridFixtureComponent } from './ag-grid.component.fixture'; +import { + FirstInlineHelpComponent, + SecondInlineHelpComponent, +} from './inline-help.component'; @NgModule({ imports: [ @@ -29,12 +33,16 @@ import { SkyAgGridFixtureComponent } from './ag-grid.component.fixture'; SkyAgGridFixtureComponent, SkyAgGridRowDeleteFixtureComponent, SkyAgGridCellValidatorTooltipFixtureComponent, + FirstInlineHelpComponent, + SecondInlineHelpComponent, ], exports: [ SkyAgGridDataManagerFixtureComponent, SkyAgGridFixtureComponent, SkyAgGridRowDeleteFixtureComponent, SkyAgGridCellValidatorTooltipFixtureComponent, + FirstInlineHelpComponent, + SecondInlineHelpComponent, ], }) export class SkyAgGridFixtureModule {} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/fixtures/inline-help.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/fixtures/inline-help.component.ts new file mode 100644 index 0000000000..d0b70354eb --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/fixtures/inline-help.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyAgGridHeaderInfo } from '@skyux/ag-grid'; + +@Component({ + selector: 'app-first-inline-help', + template: ` + ℹ︎ + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FirstInlineHelpComponent { + public readonly displayName: string; + + constructor({ displayName }: SkyAgGridHeaderInfo) { + this.displayName = displayName; + } +} + +@Component({ + selector: 'app-second-inline-help', + template: ` + ℹ︎ + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SecondInlineHelpComponent { + public readonly displayName: string; + + constructor({ displayName }: SkyAgGridHeaderInfo) { + this.displayName = displayName; + } +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header-group.component.html b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header-group.component.html new file mode 100644 index 0000000000..1f7a784b3d --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header-group.component.html @@ -0,0 +1,37 @@ +{{ + params?.displayName +}} + + + + + diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header-group.component.scss b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header-group.component.scss new file mode 100644 index 0000000000..0e75946156 --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header-group.component.scss @@ -0,0 +1,22 @@ +:host { + display: flex; + max-width: 100%; + padding-right: 12px; + padding-left: 12px; + align-items: center; + align-content: center; +} + +.header-group-text { + flex-grow: 0; + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; +} + +button { + cursor: pointer; + margin-left: var(--sky-margin-inline-sm); + flex-grow: 0; + flex-shrink: 0; +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header-group.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header-group.component.spec.ts new file mode 100644 index 0000000000..00be84c139 --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header-group.component.spec.ts @@ -0,0 +1,132 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { SkyAgGridHeaderGroupParams } from '@skyux/ag-grid'; +import { SkyI18nModule } from '@skyux/i18n'; +import { SkyIconModule } from '@skyux/indicators'; +import { SkyThemeModule } from '@skyux/theme'; + +import { + ColGroupDef, + ColumnGroup, + Events, + ProvidedColumnGroup, +} from 'ag-grid-community'; + +import { SkyAgGridHeaderGroupComponent } from './header-group.component'; + +@Component({ + template: `Help text`, +}) +class TestHelpComponent {} + +type mockEventParam = { columnGroup: ProvidedColumnGroup }; + +describe('SkyAgGridHeaderGroupComponent', () => { + let component: SkyAgGridHeaderGroupComponent; + let fixture: ComponentFixture; + let events: { [key: string]: ((value: mockEventParam) => void)[] }; + let expanded: boolean; + let providedColumnGroup: ProvidedColumnGroup; + const baseProvidedColumnGroup = { + isExpanded: () => expanded, + isExpandable: () => true, + }; + const baseParams = { + displayName: 'Test Column', + api: { + addEventListener: ( + eventType: string, + listener: (value: mockEventParam) => void + ) => { + events[eventType] = events[eventType] || []; + events[eventType].push(listener); + }, + removeEventListener: ( + eventType: string, + listener: (value: mockEventParam) => void + ) => { + events[eventType] = events[eventType] || []; + events[eventType] = events[eventType].filter((l) => l !== listener); + }, + }, + columnGroup: { + getProvidedColumnGroup: () => providedColumnGroup, + }, + setExpanded: (open: boolean) => { + expanded = open; + (events[Events.EVENT_COLUMN_GROUP_OPENED] || []).forEach((l) => + l({ columnGroup: providedColumnGroup }) + ); + }, + } as unknown as SkyAgGridHeaderGroupParams; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [SkyAgGridHeaderGroupComponent, TestHelpComponent], + imports: [SkyI18nModule, SkyIconModule, SkyThemeModule], + providers: [], + }); + events = {}; + expanded = false; + providedColumnGroup = { + ...baseProvidedColumnGroup, + } as unknown as ProvidedColumnGroup; + + fixture = TestBed.createComponent(SkyAgGridHeaderGroupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', async () => { + expect(component).toBeTruthy(); + component.agInit(undefined); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.query(By.css('.header-group-text')).properties[ + 'innerText' + ] + ).toBeFalsy(); + providedColumnGroup = { + ...baseProvidedColumnGroup, + isExpandable: () => false, + } as unknown as ProvidedColumnGroup; + component.agInit({ + ...baseParams, + columnGroup: { + ...baseParams.columnGroup, + getColGroupDef: () => + ({ + headerGroupComponent: undefined, + } as unknown as ColGroupDef), + } as unknown as ColumnGroup, + }); + }); + + it('should expand and collapse', async () => { + component.agInit({ + ...baseParams, + columnGroup: { + ...baseParams.columnGroup, + getColGroupDef: () => + ({ + headerGroupComponent: SkyAgGridHeaderGroupComponent, + headerGroupComponentParams: { + inlineHelpComponent: TestHelpComponent, + }, + } as ColGroupDef), + } as unknown as ColumnGroup, + }); + component.ngAfterViewInit(); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.query(By.css('.test-help-component')) + ).toBeTruthy(); + component.setExpanded(true); + fixture.detectChanges(); + await fixture.whenStable(); + expect(expanded).toBeTruthy(); + }); +}); diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header-group.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header-group.component.ts new file mode 100644 index 0000000000..1278966946 --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header-group.component.ts @@ -0,0 +1,124 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + OnDestroy, + ViewChild, +} from '@angular/core'; +import { + SkyDynamicComponentLocation, + SkyDynamicComponentOptions, + SkyDynamicComponentService, +} from '@skyux/core'; + +import { IHeaderGroupAngularComp } from 'ag-grid-angular'; +import { + ColumnGroupOpenedEvent, + Events, + ProvidedColumnGroup, +} from 'ag-grid-community'; +import { BehaviorSubject, Observable, Subscription, fromEvent } from 'rxjs'; + +import { SkyAgGridHeaderGroupInfo } from '../types/header-group-info'; +import { SkyAgGridHeaderGroupParams } from '../types/header-group-params'; + +/** + * @internal + */ +@Component({ + selector: 'sky-header-group', + templateUrl: './header-group.component.html', + styleUrls: ['./header-group.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SkyAgGridHeaderGroupComponent + implements IHeaderGroupAngularComp, OnDestroy, AfterViewInit +{ + @ViewChild('inlineHelpContainer', { read: ElementRef, static: true }) + public inlineHelpContainer: ElementRef; + + public params: SkyAgGridHeaderGroupParams | undefined = undefined; + public isExpanded$: Observable; + + #columnGroup: ProvidedColumnGroup | undefined = undefined; + #isExpandedSubject = new BehaviorSubject(false); + #subscriptions = new Subscription(); + readonly #changeDetector: ChangeDetectorRef; + readonly #dynamicComponentService: SkyDynamicComponentService; + #viewInitialized = false; + #agIntialized = false; + + constructor( + changeDetector: ChangeDetectorRef, + dynamicComponentService: SkyDynamicComponentService + ) { + this.#changeDetector = changeDetector; + this.#dynamicComponentService = dynamicComponentService; + this.isExpanded$ = this.#isExpandedSubject.asObservable(); + } + + public ngAfterViewInit(): void { + this.#viewInitialized = true; + this.#updateInlineHelp(); + this.#changeDetector.markForCheck(); + } + + public ngOnDestroy(): void { + this.#subscriptions.unsubscribe(); + } + + public agInit(params: SkyAgGridHeaderGroupParams): void { + this.#agIntialized = true; + this.params = params; + this.#subscriptions.unsubscribe(); + if (!this.params) { + return; + } + this.#subscriptions = new Subscription(); + this.#columnGroup = this.params.columnGroup.getProvidedColumnGroup(); + if (this.#columnGroup.isExpandable()) { + this.#subscriptions.add( + fromEvent(this.params.api, Events.EVENT_COLUMN_GROUP_OPENED).subscribe( + (event: ColumnGroupOpenedEvent) => { + if (event.columnGroup === this.#columnGroup) { + this.#isExpandedSubject.next(this.#columnGroup.isExpanded()); + } + } + ) + ); + } + this.#updateInlineHelp(); + this.#changeDetector.markForCheck(); + } + + public setExpanded($event: boolean): void { + this.params.setExpanded($event); + } + + #updateInlineHelp(): void { + if (!this.#viewInitialized || !this.#agIntialized) { + return; + } + const colGroupDef = this.params?.columnGroup?.getColGroupDef(); + const inlineHelpComponent = + colGroupDef?.headerGroupComponentParams?.inlineHelpComponent; + if (inlineHelpComponent) { + this.#dynamicComponentService.createComponent(inlineHelpComponent, { + providers: [ + { + provide: SkyAgGridHeaderGroupInfo, + useValue: { + columnGroup: this.params.columnGroup, + context: this.params.context, + displayName: this.params.displayName, + } as SkyAgGridHeaderGroupInfo, + }, + ], + referenceEl: this.inlineHelpContainer.nativeElement, + location: SkyDynamicComponentLocation.ElementBottom, + } as SkyDynamicComponentOptions); + } + } +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html new file mode 100644 index 0000000000..7167d6eae3 --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html @@ -0,0 +1,49 @@ + + diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.scss b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.scss new file mode 100644 index 0000000000..1ad08ea0f5 --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.scss @@ -0,0 +1,19 @@ +:host { + display: flex; + max-width: 100%; + padding-right: 12px; + padding-left: 12px; + align-items: center; + align-content: center; +} + +.ag-header-cell-label { + padding-right: 0; + padding-left: 0; +} + +button { + cursor: pointer; + flex-grow: 0; + flex-shrink: 0; +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.spec.ts new file mode 100644 index 0000000000..47f247735c --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.spec.ts @@ -0,0 +1,189 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { SkyIconModule } from '@skyux/indicators'; +import { SkyThemeModule } from '@skyux/theme'; + +import { Column, ColumnApi } from 'ag-grid-community'; + +import { SkyAgGridHeaderParams } from '../types/header-params'; + +import { SkyAgGridHeaderComponent } from './header.component'; + +@Component({ + template: `Help text`, +}) +class TestHelpComponent {} + +@Component({ + template: `Other help text`, +}) +class OtherTestHelpComponent {} + +describe('HeaderComponent', () => { + let component: SkyAgGridHeaderComponent; + let fixture: ComponentFixture; + let apiEvents: { [key: string]: (() => void)[] }; + let columnEvents: { [key: string]: (() => void)[] }; + let params: SkyAgGridHeaderParams; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ + SkyAgGridHeaderComponent, + TestHelpComponent, + OtherTestHelpComponent, + ], + imports: [SkyIconModule, SkyThemeModule], + }); + apiEvents = {}; + columnEvents = {}; + params = { + displayName: 'Test Column', + showColumnMenu: () => undefined, + progressSort: () => undefined, + api: { + addEventListener: (eventType: string, listener: () => void) => { + apiEvents[eventType] = apiEvents[eventType] || []; + apiEvents[eventType].push(listener); + }, + removeEventListener: (eventType: string, listener: () => void) => { + apiEvents[eventType] = apiEvents[eventType] || []; + apiEvents[eventType] = apiEvents[eventType].filter( + (l) => l !== listener + ); + }, + }, + column: { + addEventListener: (eventType: string, listener: () => void) => { + columnEvents[eventType] = columnEvents[eventType] || []; + columnEvents[eventType].push(listener); + }, + removeEventListener: (eventType: string, listener: () => void) => { + columnEvents[eventType] = columnEvents[eventType] || []; + columnEvents[eventType] = columnEvents[eventType].filter( + (l) => l !== listener + ); + }, + isFilterActive: () => false, + isFilterAllowed: () => false, + isSortAscending: () => false, + isSortDescending: () => false, + isSortNone: () => true, + getSort: (): 'asc' | 'desc' | null | undefined => undefined, + getSortIndex: () => undefined, + getColId: () => 'test', + } as Column, + enableSorting: false, + columnApi: { + getAllColumns: () => [], + } as ColumnApi, + } as unknown as SkyAgGridHeaderParams; + + fixture = TestBed.createComponent(SkyAgGridHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + component.agInit(undefined); + expect(component).toBeTruthy(); + }); + + it('should implement IHeaderAngularComp', () => { + component.agInit(params); + fixture.detectChanges(); + expect( + fixture.debugElement.query(By.css('.ag-header-cell-text')).nativeElement + .textContent + ).toBe('Test Column'); + const sortSpy = spyOn(params, 'progressSort'); + component.onSortRequested({ shiftKey: false }); + expect(sortSpy).toHaveBeenCalled(); + const menuSpy = spyOn(params, 'showColumnMenu'); + component.onMenuClick({ target: {} } as Event); + expect(menuSpy).toHaveBeenCalled(); + expect(component.refresh(params)).toBe(false); + }); + + it('should handle events', async () => { + let useSort = 'asc'; + params = { + ...params, + enableSorting: true, + column: { + ...params.column, + getSort: () => useSort, + getSortIndex: () => useSort && 0, + isFilterActive: () => true, + isFilterAllowed: () => true, + isSortAscending: () => useSort === 'asc', + isSortDescending: () => useSort === 'desc', + isSortNone: () => !useSort, + } as unknown as Column, + columnApi: { + ...params.columnApi, + getAllColumns() { + return [ + { + getColId: () => 'test', + getSort: () => useSort, + }, + { + getColId: () => 'other', + getSort: () => useSort, + }, + ]; + }, + } as unknown as ColumnApi, + }; + component.agInit(params); + fixture.detectChanges(); + expect(apiEvents['sortChanged'].length).toBeGreaterThanOrEqual(1); + expect(columnEvents['sortChanged'].length).toBeGreaterThanOrEqual(1); + expect(columnEvents['filterChanged'].length).toBeGreaterThanOrEqual(1); + apiEvents['sortChanged'].forEach((listener) => listener()); + columnEvents['sortChanged'].forEach((listener) => listener()); + expect( + fixture.debugElement.query(By.css('.ag-header-label-icon')).attributes[ + 'icon' + ] + ).toBe('caret-up'); + useSort = undefined; + apiEvents['sortChanged'].forEach((listener) => listener()); + columnEvents['sortChanged'].forEach((listener) => listener()); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.query(By.css('.ag-header-label-icon')) + ).toBeFalsy(); + + columnEvents['filterChanged'].forEach((listener) => listener()); + expect(component.filterEnabled$.getValue()).toBe(true); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.debugElement.query(By.css('.ag-filter-icon'))).toBeTruthy(); + }); + + it('should inject help component', () => { + component.agInit({ + ...params, + inlineHelpComponent: TestHelpComponent, + }); + fixture.detectChanges(); + expect( + fixture.debugElement.query(By.css('.test-help-component')) + ).toBeTruthy(); + component.refresh({ + ...params, + inlineHelpComponent: OtherTestHelpComponent, + }); + fixture.detectChanges(); + expect( + fixture.debugElement.query(By.css('.test-help-component')) + ).toBeFalsy(); + expect( + fixture.debugElement.query(By.css('.other-help-component')) + ).toBeTruthy(); + }); +}); diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.ts new file mode 100644 index 0000000000..44de2d1f4f --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.ts @@ -0,0 +1,176 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ComponentRef, + ElementRef, + OnDestroy, + ViewChild, +} from '@angular/core'; +import { + SkyDynamicComponentLocation, + SkyDynamicComponentOptions, + SkyDynamicComponentService, +} from '@skyux/core'; + +import { IHeaderAngularComp } from 'ag-grid-angular'; +import { Events } from 'ag-grid-community'; +import { BehaviorSubject, Subscription, fromEvent } from 'rxjs'; + +import { SkyAgGridHeaderInfo } from '../types/header-info'; +import { SkyAgGridHeaderParams } from '../types/header-params'; + +/** + * @internal + */ +@Component({ + selector: 'sky-ag-grid-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SkyAgGridHeaderComponent + implements IHeaderAngularComp, OnDestroy, AfterViewInit +{ + @ViewChild('inlineHelpContainer', { read: ElementRef, static: true }) + public inlineHelpContainer: ElementRef; + + public params: SkyAgGridHeaderParams | undefined = undefined; + public sorted = ''; + public readonly filterEnabled$ = new BehaviorSubject(false); + public readonly sortOrder$ = new BehaviorSubject<'asc' | 'desc' | undefined>( + undefined + ); + public readonly sortIndexDisplay$ = new BehaviorSubject(''); + + #subscriptions = new Subscription(); + readonly #changeDetector: ChangeDetectorRef; + readonly #dynamicComponentService: SkyDynamicComponentService; + #inlineHelpComponentRef: ComponentRef | undefined; + #viewInitialized = false; + #agIntialized = false; + + constructor( + changeDetector: ChangeDetectorRef, + dynamicComponentService: SkyDynamicComponentService + ) { + this.#changeDetector = changeDetector; + this.#dynamicComponentService = dynamicComponentService; + } + + public ngAfterViewInit(): void { + this.#viewInitialized = true; + this.#updateInlineHelp(); + } + + public ngOnDestroy(): void { + this.#subscriptions.unsubscribe(); + } + + public agInit(params: SkyAgGridHeaderParams): void { + this.#agIntialized = true; + this.params = params; + this.#subscriptions.unsubscribe(); + if (!this.params) { + return; + } + this.#subscriptions = new Subscription(); + if (params.column.isFilterAllowed()) { + this.#subscriptions.add( + fromEvent(params.column, 'filterChanged').subscribe(() => { + const isFilterActive = params.column.isFilterActive(); + if (isFilterActive !== this.filterEnabled$.getValue()) { + this.filterEnabled$.next(isFilterActive); + } + }) + ); + } + if (params.enableSorting) { + // Column sort state changes + this.#subscriptions.add( + fromEvent(params.column, Events.EVENT_SORT_CHANGED).subscribe(() => { + this.#updateSort(); + }) + ); + // Other column sort state changes, for multi-column sorting + this.#subscriptions.add( + fromEvent(params.api, Events.EVENT_SORT_CHANGED).subscribe(() => { + this.#updateSortIndex(); + }) + ); + this.#updateSort(); + this.#updateSortIndex(); + } + this.#updateInlineHelp(); + this.#changeDetector.markForCheck(); + } + + public onMenuClick($event: Event): void { + this.params.showColumnMenu($event.target as HTMLElement); + } + + public onSortRequested(event): void { + this.params.progressSort(!!event.shiftKey); + } + + public refresh(params: SkyAgGridHeaderParams): boolean { + this.agInit(params); + return false; + } + + #updateInlineHelp(): void { + if (!this.#viewInitialized || !this.#agIntialized) { + return; + } + const inlineHelpComponent = this.params.inlineHelpComponent; + if ( + inlineHelpComponent && + (!this.#inlineHelpComponentRef || + this.#inlineHelpComponentRef.componentType !== inlineHelpComponent) + ) { + this.#dynamicComponentService.removeComponent( + this.#inlineHelpComponentRef + ); + this.#inlineHelpComponentRef = + this.#dynamicComponentService.createComponent(inlineHelpComponent, { + providers: [ + { + provide: SkyAgGridHeaderInfo, + useValue: { + column: this.params.column, + context: this.params.context, + displayName: this.params.displayName, + } as SkyAgGridHeaderInfo, + }, + ], + referenceEl: this.inlineHelpContainer.nativeElement, + location: SkyDynamicComponentLocation.ElementBottom, + } as SkyDynamicComponentOptions); + } else if (!inlineHelpComponent) { + this.#dynamicComponentService.removeComponent( + this.#inlineHelpComponentRef + ); + } + } + + #updateSort(): void { + this.sortOrder$.next(this.params.column.getSort() || undefined); + } + + #updateSortIndex(): void { + const sortIndex = this.params.column.getSortIndex(); + const otherSortColumns = this.params.columnApi + .getAllColumns() + .some( + (column) => + column.getColId() !== this.params.column.getColId() && + !!column.getSort() + ); + if (sortIndex !== undefined && sortIndex !== null && otherSortColumns) { + this.sortIndexDisplay$.next(`${sortIndex + 1}`); + } else { + this.sortIndexDisplay$.next(''); + } + } +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-group-info.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-group-info.ts new file mode 100644 index 0000000000..43d31099da --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-group-info.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; + +import { ColumnGroup } from 'ag-grid-community'; + +/** + * To display a help button beside the column group header, create a component that includes the help button element, + * such as `sky-help-inline`, include a `SkyAgGridHeaderGroupInfo` parameter in the constructor to access the column + * group information, such as display name, and add the component to the `headerComponentParams.inlineHelpComponent` + * property of the column definition. + */ +@Injectable() +export class SkyAgGridHeaderGroupInfo { + columnGroup: ColumnGroup; + displayName: string; + /** + * Application context as set on `gridOptions.context`. + */ + context: any; +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-group-params.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-group-params.ts new file mode 100644 index 0000000000..786ed12d54 --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-group-params.ts @@ -0,0 +1,12 @@ +import { Type } from '@angular/core'; + +import { IHeaderGroupParams } from 'ag-grid-community'; + +/** + * Interface to use for the + * [`headerGroupComponentParams`](https://www.ag-grid.com/angular-data-grid/column-properties/#reference-groupsHeader-headerGroupComponentParams) + * property on `ColDef`. + */ +export interface SkyAgGridHeaderGroupParams extends IHeaderGroupParams { + inlineHelpComponent?: Type; +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-info.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-info.ts new file mode 100644 index 0000000000..dd824e5e0a --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-info.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; + +import { Column } from 'ag-grid-community'; + +/** + * To display a help button beside the column header, create a component that includes the help button element, + * such as `sky-help-inline`, include a `SkyAgGridHeaderInfo` parameter in the constructor to access the column + * information, such as display name, and add the component to the `headerComponentParams.inlineHelpComponent` property + * of the column definition. + */ +@Injectable() +export class SkyAgGridHeaderInfo { + column: Column; + displayName: string; + /** + * Application context as set on `gridOptions.context`. + */ + context: any; +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-params.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-params.ts new file mode 100644 index 0000000000..cdef4a5d96 --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-params.ts @@ -0,0 +1,12 @@ +import { Type } from '@angular/core'; + +import { IHeaderParams } from 'ag-grid-community/dist/lib/headerRendering/cells/column/headerComp'; + +/** + * Interface to use for the + * [`headerComponentParams`](https://www.ag-grid.com/angular-data-grid/column-properties/#reference-header-headerComponentParams) + * property on `ColDef`. + */ +export interface SkyAgGridHeaderParams extends IHeaderParams { + inlineHelpComponent?: Type; +} diff --git a/libs/components/ag-grid/src/lib/modules/shared/sky-ag-grid-resources.module.ts b/libs/components/ag-grid/src/lib/modules/shared/sky-ag-grid-resources.module.ts index 5c3e4f01fb..850ce530d9 100644 --- a/libs/components/ag-grid/src/lib/modules/shared/sky-ag-grid-resources.module.ts +++ b/libs/components/ag-grid/src/lib/modules/shared/sky-ag-grid-resources.module.ts @@ -38,6 +38,12 @@ const RESOURCES: { [locale: string]: SkyLibResources } = { sky_ag_grid_cell_editor_currency_aria_label: { message: 'Editable currency {0} for row {1}', }, + sky_ag_grid_column_group_header_expand_aria_label: { + message: 'Expand column group {0}', + }, + sky_ag_grid_column_group_header_collapse_aria_label: { + message: 'Collapse column group {0}', + }, }, }; diff --git a/libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss b/libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss index 767bee911b..fadce4aa81 100644 --- a/libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss +++ b/libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss @@ -631,7 +631,7 @@ ag-grid-angular { padding-right: 12px; } - .sky-ag-grid-header-right-aligned .ag-header-cell-label { + .ag-header-cell.sky-ag-grid-header-right-aligned { justify-content: flex-end; } }