diff --git a/CHANGELOG.md b/CHANGELOG.md index 928db2f64f7..bfad53630bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes for each version of this project will be documented in this - **Theming** - The standard scrollbar properties expose only two colors and three width keywords, so most `scrollbar-theme` properties no longer have any effect. `$sb-thumb-bg-color` and `$sb-track-bg-color` continue to work. The following have become no-ops: `$sb-thumb-bg-color-hover`, `$sb-track-bg-color-hover`, `$sb-thumb-min-height`, `$sb-thumb-border-color`, `$sb-thumb-border-size`, `$sb-thumb-border-radius`, `$sb-track-border-color`, `$sb-track-border-size`, `$sb-corner-bg`, `$sb-corner-border-color` and `$sb-corner-border-size`. They remain valid arguments to `scrollbar-theme()`, so existing themes keep compiling, but the values are ignored. The `ng update` migration for 22.2.0 removes these arguments from existing `scrollbar-theme(...)` calls automatically. - **Theming** - `$sb-size` no longer sets the scrollbar thickness. `scrollbar-width` accepts only `auto`, `thin` or `none`, so a length cannot drive it. The migration removes `$sb-size` along with the properties above; set `--sb-width: thin` on the scope that declares the scrollbar tokens, or `scrollbar-width: thin` directly on the scrolling element, where a thinner scrollbar is required. +- **Theming** - The `grid-summary-theme` properties `$border-width` and `$pinned-border-width` were removed, along with their `--ig-grid-summary-border-width` and `--ig-grid-summary-pinned-border-width` CSS custom properties. A summary cell takes its border width from the grid itself - `grid-theme`'s `$header-border-width` for the cell separator and `$pinned-border-width` for the pinned border - so a summary border can no longer be thicker or thinner than the column border it continues. The border styles and colors stay themable: `$border-style`, `$pinned-border-style`, `$border-color` and `$pinned-border-color`, and their CSS custom properties, are unchanged. The `ng update` migration for 22.2.0 drops the removed arguments from existing `grid-summary-theme(...)` calls and renames the CSS custom properties to the grid ones that now drive them - note that those grid properties style the grid's own borders too, so review the result where a summary-only width was set. ### Behavioral Changes diff --git a/projects/igniteui-angular/grids/core/src/summaries/themes/_base.scss b/projects/igniteui-angular/grids/core/src/summaries/themes/_base.scss index 13c9e18d0e1..15293e99ec6 100644 --- a/projects/igniteui-angular/grids/core/src/summaries/themes/_base.scss +++ b/projects/igniteui-angular/grids/core/src/summaries/themes/_base.scss @@ -1,4 +1,3 @@ -@use 'sass:map'; @use 'igniteui-theming/sass/bem' as *; @use 'igniteui-theming/sass/themes' as *; @use 'igniteui-theming/sass/typography' as *; @@ -8,11 +7,6 @@ $_theme: digest-schema($material-grid-summary); @include layer(base) { - $cell-pin: ( - style: var-get($_theme, 'pinned-border-width') var-get($_theme, 'pinned-border-style'), - color: var-get($_theme, 'pinned-border-color') - ); - @include b(igx-grid-summary) { position: relative; display: flex; @@ -24,7 +18,10 @@ $_theme: digest-schema($material-grid-summary); overflow: hidden; outline-style: none; - border-inline-end: var-get($_theme, 'border-width') var-get($_theme, 'border-style') var-get($_theme, 'border-color'); + // The border width is the grid's - a summary cell must never be thicker or + // thinner than the grid's column borders. The style and the color stay + // themable through the grid-summary schema. + border-inline-end: var(--_summary-column-border-width) var-get($_theme, 'border-style') var-get($_theme, 'border-color'); @include e(item) { display: flex; @@ -67,40 +64,26 @@ $_theme: digest-schema($material-grid-summary); } } + // Inner edge of the end-pinned area @include m(pinned-first) { - border-inline-start: map.get($cell-pin, 'style') map.get($cell-pin, 'color'); - - &:dir(ltr) { - border-inline-start: map.get($cell-pin, 'style') map.get($cell-pin, 'color'); - } - - &:dir(rtl) { - border-inline-end: map.get($cell-pin, 'style') map.get($cell-pin, 'color'); - } + border-inline-start: var(--_summary-pinned-border-width) var-get($_theme, 'pinned-border-style') var-get($_theme, 'pinned-border-color'); @media print { - border-inline-start: map.get($cell-pin, 'style') #999; + border-inline-start: var(--_summary-pinned-border-width) var-get($_theme, 'pinned-border-style') #999; } } + // Inner edge of the start-pinned area @include m(pinned-last) { - border-inline-end: map.get($cell-pin, 'style') map.get($cell-pin, 'color'); + border-inline-end: var(--_summary-pinned-border-width) var-get($_theme, 'pinned-border-style') var-get($_theme, 'pinned-border-color'); &::after { inset-inline-start: rem(1px); } @media print { - border-inline-end: map.get($cell-pin, 'style') #999; + border-inline-end: var(--_summary-pinned-border-width) var-get($_theme, 'pinned-border-style') #999; } - - // &:dir(ltr) { - // box-shadow: inset rem(-1px) 0 0 0 var-get($_theme, 'pinned-border-color'); - // } - - // &:dir(rtl) { - // box-shadow: inset rem(1px) 0 0 0 var-get($_theme, 'pinned-border-color'); - // } } @include m(fw) { diff --git a/projects/igniteui-angular/grids/themes/_base.scss b/projects/igniteui-angular/grids/themes/_base.scss index 0b371d5e035..4a0e95766ff 100644 --- a/projects/igniteui-angular/grids/themes/_base.scss +++ b/projects/igniteui-angular/grids/themes/_base.scss @@ -17,6 +17,7 @@ $grid-header-border-color: var(--_grid-header-border-color, hsla(from var-get($t $grid-action-border-color: var(--_grid-action-border-color, hsla(from var-get($theme, 'action-border-color') h s l / 0.38)); $grid-header-border: var-get($theme, 'header-border-width') var-get($theme, 'header-border-style') $grid-header-border-color; $grid-row-border: var-get($theme, 'row-border-width') var-get($theme, 'row-border-style') var-get($theme, 'row-border-color'); +$grid-pinned-border: var-get($theme, 'pinned-border-width') var-get($theme, 'pinned-border-style') var-get($theme, 'pinned-border-color'); $grid-scrollbar-borders: rem(1px) solid var(--ig-grid-summary-border-color, var(--row-border-color)); $grid-body-column-border-odd: var-get($theme, 'header-border-width') solid var-get($theme, 'body-column-border-color-odd'); $grid-body-column-border-even: var-get($theme, 'header-border-width') solid var-get($theme, 'body-column-border-color-even'); @@ -253,6 +254,13 @@ $z-grid-loading: 10003; --_grid-head-font-size: #{rem(12px)}; + // Border widths handed down to the nested igx-grid-summary cells. Their own + // token scope shadows the grid's --pinned-border-width, and a summary border + // must never be thicker or thinner than the column border it continues, so + // the grid-summary width tokens are deprecated and never consulted. + --_summary-column-border-width: #{var-get($theme, 'header-border-width')}; + --_summary-pinned-border-width: #{var-get($theme, 'pinned-border-width')}; + // Core grid structure position: relative; display: grid; @@ -360,12 +368,12 @@ $z-grid-loading: 10003; // Scroll start section - a pinned column always takes precedence with a thicker border @include e(scroll-start, $m: pinned) { - border-inline-end: rem(2px) solid var(--ig-grid-summary-pinned-border-color, var(--pinned-border-color)); + border-inline-end: var-get($theme, 'pinned-border-width') var-get($theme, 'pinned-border-style') var-get($theme, 'pinned-border-color'); } @include e(scroll-end) { background: var(--_grid-background, var-get($theme, 'content-background')); - border-inline-start: var-get($theme, 'pinned-border-width') solid var(--ig-grid-summary-pinned-border-color, var(--pinned-border-color)); + border-inline-start: var-get($theme, 'pinned-border-width') var-get($theme, 'pinned-border-style') var-get($theme, 'pinned-border-color'); } // Scroll main section @@ -746,7 +754,7 @@ $z-grid-loading: 10003; // First pinned column (left border) @include e(td, $m: pinned-first) { - border-inline-start: var-get($theme, 'pinned-border-width') var-get($theme, 'pinned-border-style') var-get($theme, 'pinned-border-color'); + border-inline-start: $grid-pinned-border; &::after { width: calc(100% - $pinned-shadow-size); @@ -757,7 +765,7 @@ $z-grid-loading: 10003; // Last pinned column (right border) @include e(td, $m: pinned-last) { - border-inline-end: var-get($theme, 'pinned-border-width') var-get($theme, 'pinned-border-style') var-get($theme, 'pinned-border-color'); + border-inline-end: $grid-pinned-border; } // Pinned cell when selected @@ -1923,7 +1931,7 @@ $z-grid-loading: 10003; @include e(summaries-patch) { position: relative; background: inherit; - border-inline-end: var(--ig-grid-summary-border-width, rem(1px)) + border-inline-end: var-get($theme, 'header-border-width') var(--ig-grid-summary-border-style, solid) var(--ig-grid-summary-border-color, $grid-action-border-color); z-index: $z-grid-base; @@ -2371,36 +2379,13 @@ $z-grid-loading: 10003; } } - // Pinned header (first pinned column) @include m(pinned-first) { - border-inline-start: var-get($theme, 'pinned-border-width') var-get($theme, 'pinned-border-style'); - - &:dir(ltr) { - border-inline-start-color: var-get($theme, 'pinned-border-color'); - border-inline-end: none; - } - - &:dir(rtl) { - border-inline-end-color: var-get($theme, 'pinned-border-color'); - border-inline-start: none; - } + border-inline-start: $grid-pinned-border; + border-inline-end: none; } - // Pinned header (last pinned column) @include m(pinned-last) { - border-inline-end: var-get($theme, 'pinned-border-width') var-get($theme, 'pinned-border-style') var-get($theme, 'pinned-border-color'); - - &:dir(ltr) { - border-inline-end-color: var-get($theme, 'header-border-color'); - border-inline-end-width: var-get($theme, 'pinned-border-width'); - border-inline-end-style: var-get($theme, 'pinned-border-style'); - } - - &:dir(rtl) { - border-inline-start-color: var-get($theme, 'header-border-color'); - border-inline-start-width: var-get($theme, 'pinned-border-width'); - border-inline-start-style: var-get($theme, 'pinned-border-style'); - } + border-inline-end: $grid-pinned-border; } @include m(selected) { @@ -2574,6 +2559,20 @@ $z-grid-loading: 10003; border-block-end: $grid-header-border; border-inline-end: $grid-header-border; height: var(--header-size); + + // The title cell of a pinned column group sits on the edge of the pinned + // area and carries the same pinned modifiers as a leaf header. Those are + // declared for .igx-grid-th--pinned-* earlier in this layer at the same + // specificity, so the pinned border has to be re-asserted here to win over + // the plain header border above - otherwise the pinned border stops at the + // group title and only continues from the leaf header down. + &.igx-grid-th--pinned-last { + border-inline-end: $grid-pinned-border; + } + + &.igx-grid-th--pinned-first { + border-inline-start: $grid-pinned-border; + } } // Header group (contains multiple columns) diff --git a/projects/igniteui-angular/migrations/migration-collection.json b/projects/igniteui-angular/migrations/migration-collection.json index 9a341858507..d5090b6f363 100644 --- a/projects/igniteui-angular/migrations/migration-collection.json +++ b/projects/igniteui-angular/migrations/migration-collection.json @@ -314,6 +314,11 @@ "version": "22.2.0", "description": "Removes scrollbar-theme properties with no effect under the standard scrollbar properties", "factory": "./update-22_2_0" + }, + "migration-62": { + "version": "22.2.0", + "description": "Removes the grid-summary border width properties now driven by the grid's own borders", + "factory": "./update-22_2_0_grid-summary-borders" } } } diff --git a/projects/igniteui-angular/migrations/update-22_2_0_grid-summary-borders/changes/theme-changes.json b/projects/igniteui-angular/migrations/update-22_2_0_grid-summary-borders/changes/theme-changes.json new file mode 100644 index 00000000000..9632f7fcaab --- /dev/null +++ b/projects/igniteui-angular/migrations/update-22_2_0_grid-summary-borders/changes/theme-changes.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../common/schema/theme-changes.schema.json", + "changes": [ + { + "name": "$border-width", + "remove": true, + "owner": "grid-summary-theme", + "type": "property" + }, + { + "name": "$pinned-border-width", + "remove": true, + "owner": "grid-summary-theme", + "type": "property" + } + ] +} diff --git a/projects/igniteui-angular/migrations/update-22_2_0_grid-summary-borders/index.spec.ts b/projects/igniteui-angular/migrations/update-22_2_0_grid-summary-borders/index.spec.ts new file mode 100644 index 00000000000..4dfe6cd2593 --- /dev/null +++ b/projects/igniteui-angular/migrations/update-22_2_0_grid-summary-borders/index.spec.ts @@ -0,0 +1,88 @@ +import * as path from 'path'; + +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing/index.js'; +import { setupTestTree } from '../common/setup.spec'; + +const version = '22.2.0'; + +describe(`Update to ${version}`, () => { + let appTree: UnitTestTree; + const schematicRunner = new SchematicTestRunner('ig-migrate', path.join(__dirname, '../migration-collection.json')); + + beforeEach(() => { + appTree = setupTestTree(); + }); + + const migrationName = 'migration-62'; + + it('should remove the border width properties from grid-summary-theme', async () => { + const testFilePath = `/testSrc/appPrefix/component/test.component.scss`; + + appTree.create( + testFilePath, + `$my-summary-theme: grid-summary-theme( + $background-color: red, + $border-width: 4px, + $border-style: dashed, + $border-color: blue, + $pinned-border-width: 6px, + $pinned-border-style: dotted, + $pinned-border-color: green + );` + ); + + const tree = await schematicRunner.runSchematic(migrationName, {}, appTree); + + expect(tree.readContent(testFilePath)).toEqual( + `$my-summary-theme: grid-summary-theme( + $background-color: red, + $border-style: dashed, + $border-color: blue, + $pinned-border-style: dotted, + $pinned-border-color: green + );` + ); + }); + + it('should rename the grid-summary border width CSS custom properties and keep the styles', async () => { + const testFilePath = `/testSrc/appPrefix/component/test.component.scss`; + + appTree.create( + testFilePath, + `igx-grid { + --ig-grid-summary-border-width: 4px; + --ig-grid-summary-border-style: dashed; + --ig-grid-summary-pinned-border-width: 6px; + --ig-grid-summary-pinned-border-style: dotted; + --ig-grid-summary-pinned-border-color: green; + }` + ); + + const tree = await schematicRunner.runSchematic(migrationName, {}, appTree); + + expect(tree.readContent(testFilePath)).toEqual( + `igx-grid { + --ig-grid-header-border-width: 4px; + --ig-grid-summary-border-style: dashed; + --ig-grid-pinned-border-width: 6px; + --ig-grid-summary-pinned-border-style: dotted; + --ig-grid-summary-pinned-border-color: green; + }` + ); + }); + + it('should rename the CSS custom properties in plain CSS files and keep the --igx- prefix', async () => { + const testFilePath = `/testSrc/appPrefix/component/test.component.css`; + + appTree.create( + testFilePath, + `igx-grid { --igx-grid-summary-pinned-border-width: 6px; }` + ); + + const tree = await schematicRunner.runSchematic(migrationName, {}, appTree); + + expect(tree.readContent(testFilePath)).toEqual( + `igx-grid { --igx-grid-pinned-border-width: 6px; }` + ); + }); +}); diff --git a/projects/igniteui-angular/migrations/update-22_2_0_grid-summary-borders/index.ts b/projects/igniteui-angular/migrations/update-22_2_0_grid-summary-borders/index.ts new file mode 100644 index 00000000000..ce9a4bc33ec --- /dev/null +++ b/projects/igniteui-angular/migrations/update-22_2_0_grid-summary-borders/index.ts @@ -0,0 +1,73 @@ +import type { + FileVisitor, + Rule, + SchematicContext, + Tree +} from '@angular-devkit/schematics'; +import { UpdateChanges } from '../common/UpdateChanges'; + +const version = '22.2.0'; + +/** + * Summary cells now take their border width from the grid itself, so the `grid-summary-theme` + * width properties are gone. A value set through one of the removed CSS custom properties is + * moved to the grid property that now drives it - the grid-summary border styles and colors + * are unaffected. + */ +const cssPropRenames = new Map([ + ['grid-summary-border-width', 'grid-header-border-width'], + ['grid-summary-pinned-border-width', 'grid-pinned-border-width'] +]); + +const renameCssProps = (content: string): string => { + let result = content; + for (const [name, replacement] of cssPropRenames) { + result = result.replace( + new RegExp(String.raw`--(igx?)-${name}\b`, 'g'), + (_match, prefix: string) => `--${prefix}-${replacement}` + ); + } + return result; +}; + +export default (): Rule => async (host: Tree, context: SchematicContext) => { + context.logger.info(`Applying migration for Ignite UI for Angular to version ${version}`); + + const update = new UpdateChanges(__dirname, host, context); + update.applyChanges(); + + const migrated: string[] = []; + const visit: FileVisitor = (filePath) => { + if (!/\.(scss|sass|css)$/.test(filePath) || filePath.includes('node_modules') || filePath.includes('dist')) { + return; + } + + const content = host.read(filePath)?.toString(); + if (!content || !content.includes('-grid-summary-')) { + return; + } + + const result = renameCssProps(content); + if (result !== content) { + host.overwrite(filePath, result); + migrated.push(filePath); + } + }; + + host.visit(visit); + + if (migrated.length) { + context.logger.info( + 'The grid-summary border width is no longer themable on its own - a summary border always has ' + + 'the width of the grid column border it continues. The following files had their custom ' + + 'properties renamed to the grid properties that now drive them, which also affects the ' + + 'grid\'s own borders:' + ); + for (const [name, replacement] of cssPropRenames) { + context.logger.info(` - --ig-${name} -> --ig-${replacement}`); + } + for (const filePath of migrated) { + context.logger.info(` ✓ Migrated ${filePath}`); + } + } +}; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 70fa48524b2..1c01301100c 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -331,18 +331,13 @@ export class AppComponent implements OnInit { { link: '/gridColumnPinning', icon: 'view_column', - name: 'Grid Column Pinning' + name: 'Grid Column & Row Pinning' }, { link: '/gridColumnActions', icon: 'view_column', name: 'Grid Column Actions' }, - { - link: '/gridRowPinning', - icon: 'view_column', - name: 'Grid Row Pinning' - }, { link: '/gridRowAPI', icon: 'view_column', diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index a735384c980..1971a0af70a 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -110,7 +110,6 @@ import { GridSaveStateComponent } from './grid-state/grid-state.component'; import { AboutComponent } from './grid-state/about.component'; import { GridMasterDetailSampleComponent } from './grid-master-detail/grid-master-detail.sample'; import { DateTimeEditorSampleComponent } from './date-time-editor/date-time-editor.sample'; -import { GridRowPinningSampleComponent } from './grid-row-pinning/grid-row-pinning.sample'; import { GridRowReorderComponent } from './grid-row-reorder/grid-row-reorder.sample'; import { ReactiveFormSampleComponent } from './reactive-from/reactive-form-sample.component'; import { DateRangeSampleComponent } from './date-range/date-range.sample'; @@ -513,10 +512,6 @@ export const appRoutes: Routes = [ path: 'gridColumnActions', component: GridColumnActionsSampleComponent }, - { - path: 'gridRowPinning', - component: GridRowPinningSampleComponent - }, { path: 'gridRowAPI', component: GridRowAPISampleComponent diff --git a/src/app/grid-column-pinning/grid-column-pinning.sample.html b/src/app/grid-column-pinning/grid-column-pinning.sample.html index b1df3155ba0..0f859e14d93 100644 --- a/src/app/grid-column-pinning/grid-column-pinning.sample.html +++ b/src/app/grid-column-pinning/grid-column-pinning.sample.html @@ -1,48 +1,224 @@ -
-
-
-
- - - - - -
- - @if (showToolbar) { +

+ Columns pin to either edge of the grid, rows to the top or the bottom. pinning + sets the default areas, while a column's own pinningPosition overrides it - that + is how Both sides keeps Contact Title at the opposite edge. The panel drives + all three grids below. +

+ +
+ Pinned to start: {{ pinnedStartCount() }} + Pinned to end: {{ pinnedEndCount() }} + Pinned rows: {{ pinnedRowCount() }} +
+ + + + + Grid + + + @if (activeTab() === 0) { + + + + Customers + + @if (properties().showPinningUI) { + + } + + + + + @if (properties().rowPinningUI === 'icons') { + + + + + + + } + + @for (column of columns; track column.field) { + + + } + + @if (properties().rowPinningUI === 'strip') { + + + + } + + } + + + + + + Tree Grid + + + @if (activeTab() === 1) { +

+ A pinned child row is lifted out of its branch and shown at the root of the + pinned area, while the branch keeps a disabled placeholder in its place. +

+ + - Grid Toolbar + Employees - @if (pinningEnabled) { + @if (properties().showPinningUI) { } + - } - @for (c of columns; track c) { - - - } -
-
- - - - - - - - - -
-
- Toolbar - Column Pinning - Row Selectors - Right/Left Column Pinning toggle - Top/Bottom Row Pinning toggle -
-
+ + @if (properties().rowPinningUI === 'icons') { + + + + + + + } + + + + + + + + + @if (properties().rowPinningUI === 'strip') { + + + + } + + } + + + + + + Hierarchical Grid + + + @if (activeTab() === 2) { +

+ Every level pins on its own: the root grid below has two pinned columns and a + pinned row, and the child grid pins its own Company Name column. +

+ + + + Companies + + @if (properties().showPinningUI) { + + } + + + + + @if (properties().rowPinningUI === 'icons') { + + + + + + + } + + + + + + + + + + + + + + + @if (properties().rowPinningUI === 'strip') { + + + + } + + } +
+
+ + +{{ message() }} + + +
+ +
-
+ diff --git a/src/app/grid-column-pinning/grid-column-pinning.sample.scss b/src/app/grid-column-pinning/grid-column-pinning.sample.scss index c31ecc6bb0a..5ac4002af70 100644 --- a/src/app/grid-column-pinning/grid-column-pinning.sample.scss +++ b/src/app/grid-column-pinning/grid-column-pinning.sample.scss @@ -1,27 +1,54 @@ -.sample-buttons, -.sample-switches { - margin-top: 24px; +:host { + display: grid; + gap: var(--demos-gap); + + p { + max-width: 80ch; + margin-block-start: 0; + color: var(--ig-gray-600); + } + + code { + font-family: monospace; + color: var(--ig-gray-700); + } +} + +igx-tab-content { + padding: 16px; } -[igxButton] { - margin-right: 8px; - margin-bottom: 8px; + +.pinning-status { + display: flex; + flex-wrap: wrap; + gap: 24px; + color: var(--ig-gray-600); + + strong { + color: var(--ig-gray-800); + } } -igx-switch { - padding-right: 10px; +.tab-note { + color: var(--ig-gray-600); } .pin-icon { - text-align: center; -} + cursor: pointer; + color: var(--ig-gray-600); -.igx-action-strip { - background-color: #00000080; + &:hover { + color: var(--ig-primary-500); + } } -.actions { - margin: auto; - width: fit-content; - padding: 8px; +// Rendered inside the properties panel through the custom controls template. +.pinning-actions { + display: grid; + gap: 16px; + + [igxButton] { + width: 100%; + } } diff --git a/src/app/grid-column-pinning/grid-column-pinning.sample.ts b/src/app/grid-column-pinning/grid-column-pinning.sample.ts index 0997760be0d..e09b2bfe65a 100644 --- a/src/app/grid-column-pinning/grid-column-pinning.sample.ts +++ b/src/app/grid-column-pinning/grid-column-pinning.sample.ts @@ -1,103 +1,325 @@ -import { Component, OnInit, ViewChild, ChangeDetectionStrategy } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { ColumnPinningPosition, GridSelectionMode, IPinningConfig, IgxButtonDirective, IgxColumnComponent, IgxGridComponent, IgxGridToolbarActionsComponent, IgxGridToolbarComponent, IgxGridToolbarPinningComponent, IgxGridToolbarTitleComponent, IgxSwitchComponent, RowPinningPosition, RowType } from 'igniteui-angular'; -import { SAMPLE_DATA } from '../shared/sample-data'; - +import { AfterViewInit, ChangeDetectionStrategy, Component, DestroyRef, Injector, TemplateRef, afterNextRender, computed, inject, signal, viewChild } from '@angular/core'; +import { ColumnPinningPosition } from 'igniteui-angular/core'; +import { + IPinColumnCancellableEventArgs, + IPinRowEventArgs, + IPinningConfig, + IgxCellTemplateDirective, + IgxColumnComponent, + IgxGridPinningActionsComponent, + IgxGridToolbarActionsComponent, + IgxGridToolbarComponent, + IgxGridToolbarHidingComponent, + IgxGridToolbarPinningComponent, + IgxGridToolbarTitleComponent, + RowPinningPosition, + RowType +} from 'igniteui-angular/grids/core'; +import { IgxGridBaseDirective, IgxGridComponent } from 'igniteui-angular/grids/grid'; +import { IgxTreeGridComponent } from 'igniteui-angular/grids/tree-grid'; +import { IgxHierarchicalGridComponent, IgxRowIslandComponent } from 'igniteui-angular/grids/hierarchical-grid'; +import { IgxActionStripComponent } from 'igniteui-angular/action-strip'; +import { IgxIconComponent } from 'igniteui-angular/icon'; +import { IgxButtonDirective } from 'igniteui-angular/directives'; +import { IgxSnackbarComponent } from 'igniteui-angular/snackbar'; +import { IgxTabContentComponent, IgxTabHeaderComponent, IgxTabHeaderLabelDirective, IgxTabItemComponent, IgxTabsComponent } from 'igniteui-angular/tabs'; +import { Properties, PropertyChangeService, PropertyPanelConfig } from '../properties-panel/property-change.service'; +import { HIERARCHICAL_SAMPLE_DATA, SAMPLE_DATA } from '../shared/sample-data'; +interface PinnableColumn { + field: string; + header: string; + width: string; + pinned?: boolean; + /** Pins to the opposite edge from the rest through its own `pinningPosition`. */ + pinsToEnd?: boolean; +} @Component({ - providers: [], selector: 'app-grid-column-pinning-sample', styleUrls: ['grid-column-pinning.sample.scss'], templateUrl: 'grid-column-pinning.sample.html', changeDetection: ChangeDetectionStrategy.Eager, - imports: [IgxButtonDirective, IgxGridComponent, IgxGridToolbarComponent, IgxGridToolbarTitleComponent, IgxGridToolbarActionsComponent, IgxGridToolbarPinningComponent, IgxColumnComponent, IgxSwitchComponent, FormsModule] + imports: [ + IgxGridComponent, + IgxTreeGridComponent, + IgxHierarchicalGridComponent, + IgxRowIslandComponent, + IgxColumnComponent, + IgxCellTemplateDirective, + IgxGridToolbarComponent, + IgxGridToolbarTitleComponent, + IgxGridToolbarActionsComponent, + IgxGridToolbarPinningComponent, + IgxGridToolbarHidingComponent, + IgxActionStripComponent, + IgxGridPinningActionsComponent, + IgxTabsComponent, + IgxTabItemComponent, + IgxTabHeaderComponent, + IgxTabHeaderLabelDirective, + IgxTabContentComponent, + IgxIconComponent, + IgxButtonDirective, + IgxSnackbarComponent + ] }) -export class GridColumnPinningSampleComponent implements OnInit { - @ViewChild('grid1', { static: true }) - private grid1: IgxGridComponent; - - public pinningConfig: IPinningConfig = { columns: ColumnPinningPosition.End }; - public selectionMode; - public data: any[]; - public columns: any[]; - public showToolbar: true; - public pinningEnabled: true; - - public get rightPinning() { - return (this.pinningConfig.columns === ColumnPinningPosition.End); - } - public set rightPinning(rightPinning) { - if (this.pinningConfig.columns === ColumnPinningPosition.End) { - this.pinningConfig.columns = ColumnPinningPosition.Start; - } else { - this.pinningConfig.columns = ColumnPinningPosition.End; +export class GridColumnPinningSampleComponent implements AfterViewInit { + private readonly flatGrid = viewChild('flatGrid'); + private readonly treeGrid = viewChild('treeGrid'); + private readonly hierarchicalGrid = viewChild('hierarchicalGrid'); + private readonly snackbar = viewChild.required('snackbar'); + private readonly customControls = viewChild.required>('customControls'); + + private readonly pcs = inject(PropertyChangeService); + private readonly destroyRef = inject(DestroyRef); + private readonly injector = inject(Injector); + + public panelConfig: PropertyPanelConfig = { + columnArea: { + label: 'Pin columns to', + control: { + type: 'button-group', + options: [ + { label: 'Start', value: 'start' }, + { label: 'End', value: 'end' }, + { label: 'Both sides', value: 'both' } + ], + defaultValue: 'both' + } + }, + rowArea: { + label: 'Row pinning position', + control: { + type: 'button-group', + options: [{ label: 'Top', value: 'top' }, { label: 'Bottom', value: 'bottom' }], + defaultValue: 'top' + } + }, + rowPinningUI: { + label: 'Pin rows with', + control: { + type: 'button-group', + options: [{ label: 'Action strip', value: 'strip' }, { label: 'Cell icons', value: 'icons' }], + defaultValue: 'strip' + } + }, + showPinningUI: { + label: 'Column pinning UI', + control: { type: 'boolean', defaultValue: true } + }, + preventIdPinning: { + label: 'Prevent pinning the ID column', + control: { type: 'boolean', defaultValue: false } } + }; + + protected properties = signal( + Object.fromEntries( + Object.entries(this.panelConfig).map(([key, config]) => [key, config?.control?.defaultValue]) + ) as Properties + ); + + /** + * `columns` is the area a column lands in when it is pinned without an explicit position, + * and `rows` is the edge the pinned row area is docked to. Every grid below takes the + * same config - pinning works the same way in all of them. + */ + protected pinningConfig = computed(() => ({ + columns: this.properties().columnArea === 'end' ? ColumnPinningPosition.End : ColumnPinningPosition.Start, + rows: this.properties().rowArea === 'bottom' ? RowPinningPosition.Bottom : RowPinningPosition.Top + })); + + /** + * A single column can override `pinning.columns` with its own `pinningPosition`, which is how + * both pinned areas get populated at once. With one area selected there is nothing to + * override, so the column falls back to the grid default like every other one. + */ + protected endOverride = computed(() => + this.properties().columnArea === 'both' ? ColumnPinningPosition.End : undefined + ); + + /** Only the grid on the selected tab is mounted. */ + protected activeTab = signal(0); + + protected data = SAMPLE_DATA; + protected hierarchicalData = HIERARCHICAL_SAMPLE_DATA; + + protected columns: PinnableColumn[] = [ + { field: 'ID', header: 'ID', width: '120px' }, + { field: 'CompanyName', header: 'Company Name', width: '220px' }, + // Pinned to whichever area `pinning.columns` points at. + { field: 'ContactName', header: 'Contact Name', width: '180px', pinned: true }, + // Pinned to the end area while both sides are in use. + { field: 'ContactTitle', header: 'Contact Title', width: '200px', pinned: true, pinsToEnd: true }, + { field: 'Address', header: 'Address', width: '220px' }, + { field: 'City', header: 'City', width: '140px' }, + { field: 'Country', header: 'Country', width: '140px' }, + { field: 'PostalCode', header: 'Postal Code', width: '140px' }, + { field: 'Phone', header: 'Phone', width: '160px' }, + { field: 'Fax', header: 'Fax', width: '160px' } + ]; + + protected treeData = [ + { ID: 1, ParentID: -1, Name: 'Johnathan Winchester', Title: 'Development Manager', Country: 'UK', City: 'London', Phone: '020 7946 0958' }, + { ID: 2, ParentID: 1, Name: 'Michael Burke', Title: 'Senior Software Developer', Country: 'UK', City: 'Manchester', Phone: '0161 496 0245' }, + { ID: 3, ParentID: 1, Name: 'Thomas Anderson', Title: 'Software Developer', Country: 'UK', City: 'Leeds', Phone: '0113 496 0113' }, + { ID: 4, ParentID: 3, Name: 'Monica Reyes', Title: 'Software Developer', Country: 'Ireland', City: 'Dublin', Phone: '01 496 0142' }, + { ID: 5, ParentID: -1, Name: 'Yang Wang', Title: 'Sales Manager', Country: 'USA', City: 'Seattle', Phone: '(206) 555 0184' }, + { ID: 6, ParentID: 5, Name: 'Kelly Rodriguez', Title: 'Sales Representative', Country: 'USA', City: 'Portland', Phone: '(503) 555 0117' }, + { ID: 7, ParentID: 5, Name: 'Ana Sanders', Title: 'Sales Representative', Country: 'Canada', City: 'Vancouver', Phone: '(604) 555 0163' }, + { ID: 8, ParentID: -1, Name: 'Elizabeth Richards', Title: 'Support Manager', Country: 'Germany', City: 'Berlin', Phone: '030 0074 321' }, + { ID: 9, ParentID: 8, Name: 'Pedro Afonso', Title: 'Support Engineer', Country: 'Portugal', City: 'Lisbon', Phone: '213 555 0198' }, + { ID: 10, ParentID: 8, Name: 'Laurence Lebihan', Title: 'Support Engineer', Country: 'France', City: 'Marseille', Phone: '091 24 45 40' }, + { ID: 11, ParentID: 10, Name: 'Frédérique Citeaux', Title: 'Support Engineer', Country: 'France', City: 'Strasbourg', Phone: '088 60 15 31' }, + { ID: 12, ParentID: 10, Name: 'Martín Sommer', Title: 'Support Engineer', Country: 'Spain', City: 'Madrid', Phone: '(91) 555 22 82' } + ]; + + protected message = signal(''); + + /** + * The pinned columns are read from the grid whenever a pin state changes, rather than during + * change detection - the pinned collections are rebuilt while the grid renders. + */ + private pinnedColumns = signal({ start: 0, end: 0 }); + + /** + * The pinned rows are followed through the `rowPinned` output instead: `grid.pinnedRows` is a + * view query, so it throws while the grid is still rendering its first pinned row. The event + * covers every way a row can be pinned - the action strip, the cell icons or the API. + */ + private pinnedRowKeys = signal>(new Set()); + + protected pinnedStartCount = computed(() => this.pinnedColumns().start); + protected pinnedEndCount = computed(() => this.pinnedColumns().end); + protected pinnedRowCount = computed(() => this.pinnedRowKeys().size); + + private viewReady = false; + + constructor() { + this.pcs.setPanelConfig(this.panelConfig); + + const propertyChange = this.pcs.propertyChanges.subscribe((updated) => { + const previousArea = this.properties().columnArea; + + this.properties.set(Object.fromEntries( + Object.entries(this.panelConfig).map(([key, config]) => [ + key, + updated[key] !== undefined ? updated[key] : config?.control?.defaultValue + ]) + ) as Properties); + + if (this.viewReady && this.properties().columnArea !== previousArea) { + // The new config and overrides reach the grid with the next render, so the + // columns can only be re-pinned into their new areas after that. + this.afterRender(() => this.repinToConfiguredAreas()); + } + }); + + this.destroyRef.onDestroy(() => propertyChange.unsubscribe()); } - public onChange() { - if (this.pinningConfig.columns === ColumnPinningPosition.End) { - this.pinningConfig = { columns: ColumnPinningPosition.Start, rows: this.pinningConfig.rows }; - } else { - this.pinningConfig = { columns: ColumnPinningPosition.End, rows: this.pinningConfig.rows }; - } + public ngAfterViewInit(): void { + this.viewReady = true; + this.pcs.setPanelTitle('Grid Column & Row Pinning'); + this.pcs.setCustomControls(this.customControls()); + this.pinFirstRow(); } - public onRowChange() { - if (this.pinningConfig.rows === RowPinningPosition.Bottom) { - this.pinningConfig = { columns: this.pinningConfig.columns, rows: RowPinningPosition.Top }; + protected onTabChange(index: number): void { + // Leaving a tab destroys its grid, so the pinned state starts over on the new one. + this.pinnedRowKeys.set(new Set()); + this.activeTab.set(index); + // The grid of the selected tab is created with the next render. + this.afterRender(() => this.pinFirstRow()); + } + + protected refreshPinnedColumns(): void { + const grid = this.activeGrid(); + + this.pinnedColumns.set({ + start: grid?.pinnedStartColumns.length ?? 0, + end: grid?.pinnedEndColumns.length ?? 0 + }); + } + + protected onRowPinned(event: IPinRowEventArgs): void { + this.pinnedRowKeys.update((keys) => { + const next = new Set(keys); + if (event.isPinned) { + next.add(event.rowKey); + } else { + next.delete(event.rowKey); + } + return next; + }); + } + + /** Row pinning from a cell template, as opposed to the built-in action strip. */ + protected togglePinnedRow(row: RowType): void { + if (row.pinned) { + row.unpin(); } else { - this.pinningConfig = { columns: this.pinningConfig.columns, rows: RowPinningPosition.Bottom }; + row.pin(); } } - public ngOnInit(): void { - this.columns = [ - { field: 'ID', width: '200px', hidden: false }, - { field: 'CompanyName', width: '200px' }, - { field: 'ContactName', width: '200px', pinned: true, pinningPosition: ColumnPinningPosition.Start }, - { field: 'ContactTitle', width: '300px', pinned: true, pinningPosition: ColumnPinningPosition.End }, - { field: 'Address', width: '250px' }, - { field: 'City', width: '200px' }, - { field: 'Region', width: '300px' }, - { field: 'PostalCode', width: '150px' }, - { field: 'Phone', width: '200px' }, - { field: 'Fax', width: '200px' } - ]; - - this.data = SAMPLE_DATA; - this.selectionMode = GridSelectionMode.none; - } + protected unpinAllColumns(): void { + const grid = this.activeGrid(); - public toggleColumn(name: string) { - const col = this.grid1.getColumnByName(name); - col.pinned = !col.pinned; + for (const column of [...grid.pinnedStartColumns, ...grid.pinnedEndColumns]) { + column.unpin(); + } + + this.refreshPinnedColumns(); } - public toggleVisibility(name: string) { - const col = this.grid1.getColumnByName(name); - col.hidden = !col.hidden; + protected unpinAllRows(): void { + for (const key of this.pinnedRowKeys()) { + this.activeGrid().unpinRow(key); + } } - public togglePinRow(index) { - const rec = this.data[index]; - if (this.grid1.isRecordPinned(rec)) { - this.grid1.unpinRow(rec); - } else { - this.grid1.pinRow(rec); + /** `columnPin` runs before the column is pinned and can call the operation off. */ + protected onColumnPin(event: IPinColumnCancellableEventArgs): void { + if (this.properties().preventIdPinning && event.column.field === 'ID') { + event.cancel = true; + this.message.set('Pinning of the ID column was cancelled by the columnPin handler.'); + this.snackbar().open(); } } - public onSelectionModeChange() { - this.selectionMode = this.selectionMode === GridSelectionMode.none ? GridSelectionMode.multiple : GridSelectionMode.none; + private activeGrid(): IgxGridBaseDirective { + return [this.flatGrid(), this.treeGrid(), this.hierarchicalGrid()][this.activeTab()] as IgxGridBaseDirective; } - public doSomeAction(row?: RowType) { - if (this.grid1.isRecordPinned(row.data)) { - this.grid1.unpinRow(row.data); - } else { - this.grid1.pinRow(row.data); + /** Pinning a row from code takes the row key - the primaryKey value of the record. */ + private pinFirstRow(): void { + const grid = this.activeGrid(); + const keys = [this.data[0].ID, this.treeData[0].ID, this.hierarchicalData[0].ID]; + + grid.pinRow(keys[this.activeTab()]); + this.refreshPinnedColumns(); + } + + /** + * `pinning.columns` and `pinningPosition` decide where a column lands *when it gets pinned*, + * so changing them leaves the already pinned columns where they are. Re-pinning is what + * moves them: `pin()` with no arguments uses the column's own position when it has one and + * the grid default otherwise. + */ + private repinToConfiguredAreas(): void { + const grid = this.activeGrid(); + + for (const column of [...grid.pinnedStartColumns, ...grid.pinnedEndColumns]) { + column.unpin(); + column.pin(); } + + this.refreshPinnedColumns(); } + private afterRender(callback: () => void): void { + afterNextRender(callback, { injector: this.injector }); + } } diff --git a/src/app/grid-row-pinning/grid-row-pinning.sample.html b/src/app/grid-row-pinning/grid-row-pinning.sample.html deleted file mode 100644 index b2ea6f8a3df..00000000000 --- a/src/app/grid-row-pinning/grid-row-pinning.sample.html +++ /dev/null @@ -1,127 +0,0 @@ -
- - -
-

igxGrid

-
- - Current: {{size}} -
-
- - -
-
- Bottom Row Pinning toggle - Right Column Pinning toggle -
- - - - - - - - - -
-
Country: {{dataItem.Country}}
-
City: {{dataItem.City}}
-
Address: {{dataItem.Address}}
-
-
- - - - - - - - - - - - @for (c of columns; track c) { - - - } - -
-
-
-

igxHierarchicalGrid

- - - - - - - - - - - - {{cell.row.pinned ? 'lock' : 'lock_open'}} - - - - - - - - - @for (c of hColumns; track c) { - - - } - - - - - - -
- -
-

igxTreeGrid

- - - - - - - - Export to Excel 👌 - Export to CSV 👍 - - - - - - - {{cell.row.pinned ? 'lock' : 'lock_open'}} - - - - @for (c of treeColumns; track c) { - - - } - - - -
-
diff --git a/src/app/grid-row-pinning/grid-row-pinning.sample.scss b/src/app/grid-row-pinning/grid-row-pinning.sample.scss deleted file mode 100644 index fd7b04ece50..00000000000 --- a/src/app/grid-row-pinning/grid-row-pinning.sample.scss +++ /dev/null @@ -1,22 +0,0 @@ -.sample-buttons, -.sample-switches { - margin-top: 24px; -} - -[igxButton] + [igxButton] { - margin-left: 8px; -} - -.pin-icon { - cursor: pointer; - opacity: .8; - transition: opacity 240ms ease-in-out; -} - -.pin-icon:hover { - opacity: 1; -} - -.sample-column { - padding-bottom: 2rem; -} diff --git a/src/app/grid-row-pinning/grid-row-pinning.sample.ts b/src/app/grid-row-pinning/grid-row-pinning.sample.ts deleted file mode 100644 index 9781b871b40..00000000000 --- a/src/app/grid-row-pinning/grid-row-pinning.sample.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { Component, OnInit, ViewChild, AfterViewInit, ChangeDetectionStrategy } from '@angular/core'; - -import { - IgxGridComponent, - ColumnPinningPosition, - RowPinningPosition, - IgxGridStateDirective, - IgxExcelExporterService, - IgxExcelExporterOptions, - GridSelectionMode, - IPinningConfig, - IgxIconService, - RowType, - IGX_HIERARCHICAL_GRID_DIRECTIVES, - IgxTreeGridComponent, - IgxIconComponent, - IgxSwitchComponent, - IgxButtonDirective -} from 'igniteui-angular'; -import { pinLeft, unpinLeft } from '@igniteui/material-icons-extended'; -import { GridSearchBoxComponent } from '../grid-search-box/grid-search-box.component'; -import { SAMPLE_DATA } from '../shared/sample-data'; - -@Component({ - selector: 'app-grid-row-pinning-sample', - styleUrls: ['grid-row-pinning.sample.scss'], - templateUrl: 'grid-row-pinning.sample.html', - providers: [ - IgxIconService - ], - changeDetection: ChangeDetectionStrategy.Eager, - imports: [IGX_HIERARCHICAL_GRID_DIRECTIVES, IgxGridComponent, IgxTreeGridComponent, IgxIconComponent, GridSearchBoxComponent, IgxSwitchComponent, IgxButtonDirective] -}) - -export class GridRowPinningSampleComponent implements OnInit, AfterViewInit { - @ViewChild('grid1', { static: true }) - private grid1: IgxGridComponent; - - @ViewChild(IgxGridStateDirective, { static: true }) - private state: IgxGridStateDirective; - - public pinningConfig: IPinningConfig = { columns: ColumnPinningPosition.Start }; - - public options = { - cellSelection: true, - rowSelection: true, - filtering: true, - advancedFiltering: true, - paging: true, - sorting: true, - groupBy: true, - columns: false, - rowPinning: true, - pinningConfig: true - }; - public selectionMode; - public size = 'large'; - public data: any[]; - public hierarchicalData: any[]; - public columns: any[]; - public hColumns: any[]; - public treeColumns: any[]; - public treeData: any[]; - - constructor( - private iconService: IgxIconService, - private excelExportService: IgxExcelExporterService) { - } - - public onRowChange() { - if (this.pinningConfig.rows === RowPinningPosition.Bottom) { - this.pinningConfig = { columns: this.pinningConfig.columns, rows: RowPinningPosition.Top }; - } else { - this.pinningConfig = { columns: this.pinningConfig.columns, rows: RowPinningPosition.Bottom }; - } - } - - public onChange() { - if (this.pinningConfig.columns === ColumnPinningPosition.End) { - this.pinningConfig = { columns: ColumnPinningPosition.Start, rows: this.pinningConfig.rows }; - } else { - this.pinningConfig = { columns: ColumnPinningPosition.End, rows: this.pinningConfig.rows }; - } - } - - public ngOnInit(): void { - this.columns = [ - { field: 'ID', width: '200px', hidden: true }, - { field: 'CompanyName', width: '200px', groupable: true }, - { field: 'ContactName', width: '200px', pinned: false, groupable: true }, - { field: 'ContactTitle', width: '300px', pinned: false, groupable: true }, - { field: 'Address', width: '250px' }, - { field: 'City', width: '200px' }, - { field: 'Region', width: '300px' }, - { field: 'PostalCode', width: '150px' }, - { field: 'Phone', width: '200px' }, - { field: 'Fax', width: '200px' } - ]; - - this.hColumns = [ - { field: 'ID', width: '200px' }, - { field: 'ChildLevels', width: '200px' }, - { field: 'ProductName', width: '200px' }, - { field: 'Col1', width: '200px' }, - { field: 'Col2', width: '200px' }, - { field: 'Col3', width: '200px' }, - { field: 'childData', width: '200px' }, - { field: 'childData2', width: '200px' }, - { field: 'hasChild', width: '200px' } - ]; - - this.data = SAMPLE_DATA; - this.hierarchicalData = this.generateDataUneven(100, 3); - - // treegrid cols and data - this.treeColumns = [ - { field: 'employeeID', label: 'ID', width: 200, resizable: true, dataType: 'number', hasSummary: false }, - { field: 'Salary', label: 'Salary', width: 200, resizable: true, dataType: 'number', hasSummary: true }, - { field: 'firstName', label: 'First Name', width: 300, resizable: true, dataType: 'string', hasSummary: false }, - { field: 'lastName', label: 'Last Name', width: 150, resizable: true, dataType: 'string', hasSummary: false }, - { field: 'Title', label: 'Title', width: 200, resizable: true, dataType: 'string', hasSummary: true } - ]; - this.treeData = [ - { Salary: 2500, employeeID: 0, PID: -1, firstName: 'Andrew', lastName: 'Fuller', Title: 'Vice President, Sales' }, - { Salary: 3500, employeeID: 1, PID: -1, firstName: 'Jonathan', lastName: 'Smith', Title: 'Human resources' }, - { Salary: 1500, employeeID: 2, PID: -1, firstName: 'Nancy', lastName: 'Davolio', Title: 'CFO' }, - { Salary: 2500, employeeID: 3, PID: -1, firstName: 'Steven', lastName: 'Buchanan', Title: 'CTO' }, - // sub of ID 0 - { Salary: 2500, employeeID: 4, PID: 0, firstName: 'Janet', lastName: 'Leverling', Title: 'Sales Manager' }, - { Salary: 3500, employeeID: 5, PID: 0, firstName: 'Laura', lastName: 'Callahan', Title: 'Inside Sales Coordinator' }, - { Salary: 1500, employeeID: 6, PID: 0, firstName: 'Margaret', lastName: 'Peacock', Title: 'Sales Representative' }, - { Salary: 2500, employeeID: 7, PID: 0, firstName: 'Michael', lastName: 'Suyama', Title: 'Sales Representative' }, - // sub of ID 4 - { Salary: 2500, employeeID: 8, PID: 4, firstName: 'Anne', lastName: 'Dodsworth', Title: 'Sales Representative' }, - { Salary: 3500, employeeID: 9, PID: 4, firstName: 'Danielle', lastName: 'Davis', Title: 'Sales Representative' }, - { Salary: 1500, employeeID: 10, PID: 4, firstName: 'Robert', lastName: 'King', Title: 'Sales Representative' }, - // sub of ID 2 - { Salary: 2500, employeeID: 11, PID: 2, firstName: 'Peter', lastName: 'Lewis', Title: 'Chief Accountant' }, - { Salary: 3500, employeeID: 12, PID: 2, firstName: 'Ryder', lastName: 'Zenaida', Title: 'Accountant' }, - { Salary: 1500, employeeID: 13, PID: 2, firstName: 'Wang', lastName: 'Mercedes', Title: 'Accountant' }, - // sub of ID 3 - { Salary: 1500, employeeID: 14, PID: 3, firstName: 'Theodore', lastName: 'Zia', Title: 'Software Architect' }, - { Salary: 4500, employeeID: 15, PID: 3, firstName: 'Lacota', lastName: 'Mufutau', Title: 'Product Manager' }, - // sub of ID 16 - { Salary: 2500, employeeID: 16, PID: 15, firstName: 'Jin', lastName: 'Elliott', Title: 'Product Owner' }, - { Salary: 3500, employeeID: 17, PID: 15, firstName: 'Armand', lastName: 'Ross', Title: 'Product Owner' }, - { Salary: 1500, employeeID: 18, PID: 15, firstName: 'Dane', lastName: 'Rodriquez', Title: 'Team Leader' }, - // sub of ID 19 - { Salary: 2500, employeeID: 19, PID: 18, firstName: 'Declan', lastName: 'Lester', Title: 'Senior Software Developer' }, - { Salary: 3500, employeeID: 20, PID: 18, firstName: 'Bernard', lastName: 'Jarvis', Title: 'Senior Software Developer' }, - { Salary: 1500, employeeID: 21, PID: 18, firstName: 'Jason', lastName: 'Clark', Title: 'QA' }, - { Salary: 1500, employeeID: 22, PID: 18, firstName: 'Mark', lastName: 'Young', Title: 'QA' }, - // sub of ID 20 - { Salary: 1500, employeeID: 23, PID: 20, firstName: 'Jeremy', lastName: 'Donaldson', Title: 'Software Developer' } - ]; - this.selectionMode = GridSelectionMode.multiple; - } - - public ngAfterViewInit() { - this.iconService.addSvgIconFromText(pinLeft.name, pinLeft.value, 'imx-icons'); - this.iconService.addSvgIconFromText(unpinLeft.name, unpinLeft.value, 'imx-icons'); - } - - public togglePinRow(index) { - const rec = this.data[index]; - if (this.grid1.isRecordPinned(rec)) { - this.grid1.pinRow(this.data[index]); - } else { - this.grid1.unpinRow(this.data[index]); - } - } - - public togglePining(row: RowType, event) { - event.preventDefault(); - if (row.pinned) { - row.unpin(); - } else { - row.pin(); - } - } - - public clickUnpin() { - this.grid1.unpinRow('aaaa'); - } - - public generateDataUneven(count: number, level: number, parendID: string = null) { - const prods = []; - const currLevel = level; - let children; - for (let i = 0; i < count; i++) { - const rowID = parendID ? parendID + i : i.toString(); - if (level > 0) { - // Have child grids for row with even id less rows by not multiplying by 2 - children = this.generateDataUneven(((i % 2) + 1) * Math.round(count / 3), currLevel - 1, rowID); - } - prods.push({ - ID: rowID, - ChildLevels: currLevel, - ProductName: 'Product: A' + i, - Col1: i, - Col2: i, - Col3: i, - childData: children, - childData2: children, - hasChild: true - }); - } - return prods; - } - - public isPinned(cell) { - console.log(cell); - return true; - } - - public exportButtonHandler() { - this.excelExportService.export(this.grid1, new IgxExcelExporterOptions('ExportFileFromGrid')); - } - - public saveGridState() { - const state = this.state.getState() as string; - window.localStorage.setItem('grid1-state', state); - } - - public restoreGridState() { - const state = window.localStorage.getItem('grid1-state'); - this.state.setState(state); - } - - public toggleDensity() { - switch (this.size ) { - case 'large': this.size = 'small'; break; - case 'small': this.size ='medium'; break; - case 'medium': this.size = 'small'; break; - } - } -} diff --git a/src/app/grid-theme-builder/border-rule-editor.component.html b/src/app/grid-theme-builder/border-rule-editor.component.html index 368b050d89d..500483d9641 100644 --- a/src/app/grid-theme-builder/border-rule-editor.component.html +++ b/src/app/grid-theme-builder/border-rule-editor.component.html @@ -35,19 +35,21 @@
-
- Border Width - - - -
+ @if (supportsWidth()) { +
+ Border Width + + + +
+ }
Border Style !unavailableTargets.includes(option.value)); }); + /** The width input is hidden for targets that follow another component's width. */ + protected readonly supportsWidth = computed(() => + this.activeBorderTargets().every(target => !!this.targetSignals()[target].width) + ); + protected readonly canAddBorderRule = computed(() => { const activeIndex = this.activeBorderRuleIndex(); const activeTargets = this.activeBorderTargets(); @@ -77,7 +82,7 @@ export class BorderRuleEditorComponent { if (!previousTargets.length && targets.length) { const signals = this.targetSignals()[targets[0]]; this.draftBorderColor.set(signals.color()); - this.draftBorderWidth.set(signals.width() || BORDER_DEFAULTS[targets[0]].width); + this.draftBorderWidth.set(signals.width?.() || BORDER_DEFAULTS[targets[0]].width); this.draftBorderStyle.set(signals.style() || BORDER_DEFAULTS[targets[0]].style); } @@ -107,7 +112,7 @@ export class BorderRuleEditorComponent { for (const [target, snapshot] of this.borderEditorSnapshot) { const signals = this.targetSignals()[target]; signals.color.set(snapshot.color); - signals.width.set(snapshot.width); + signals.width?.set(snapshot.width); signals.style.set(snapshot.style); } this.closeBorderEditor(); @@ -124,7 +129,7 @@ export class BorderRuleEditorComponent { this.activeBorderRuleIndex.set(index); this.activeBorderTargets.set([...targets]); this.draftBorderColor.set(signals.color()); - this.draftBorderWidth.set(signals.width() || BORDER_DEFAULTS[targets[0]].width); + this.draftBorderWidth.set(signals.width?.() || BORDER_DEFAULTS[targets[0]].width); this.draftBorderStyle.set(signals.style() || BORDER_DEFAULTS[targets[0]].style); this.borderEditorOpen.set(true); } @@ -146,7 +151,7 @@ export class BorderRuleEditorComponent { const target = targets[0]; const signals = this.targetSignals()[target]; const defaults = BORDER_DEFAULTS[target]; - return `${signals.width() || defaults.width} ${signals.style() || defaults.style} ${signals.color() || 'auto'}`; + return `${signals.width?.() || defaults.width} ${signals.style() || defaults.style} ${signals.color() || 'auto'}`; } protected borderRuleColor(targets: BorderTarget[]): string { @@ -193,7 +198,7 @@ export class BorderRuleEditorComponent { for (const target of targets) { const signals = this.targetSignals()[target]; signals.color.set(this.draftBorderColor()); - signals.width.set(this.draftBorderWidth()); + signals.width?.set(this.draftBorderWidth()); signals.style.set(this.draftBorderStyle()); } } @@ -204,7 +209,7 @@ export class BorderRuleEditorComponent { const signals = this.targetSignals()[target]; this.borderEditorSnapshot.set(target, { color: signals.color(), - width: signals.width(), + width: signals.width?.() ?? '', style: signals.style() }); } @@ -215,7 +220,7 @@ export class BorderRuleEditorComponent { const signals = this.targetSignals()[target]; signals.color.set(snapshot.color); - signals.width.set(snapshot.width); + signals.width?.set(snapshot.width); signals.style.set(snapshot.style); } @@ -229,7 +234,7 @@ export class BorderRuleEditorComponent { private resetBorderTarget(target: BorderTarget): void { const signals = this.targetSignals()[target]; signals.color.set(''); - signals.width.set(''); + signals.width?.set(''); signals.style.set(''); } } diff --git a/src/app/grid-theme-builder/border-rule-editor.types.ts b/src/app/grid-theme-builder/border-rule-editor.types.ts index 7d2daa9a817..fa3a8bbeb13 100644 --- a/src/app/grid-theme-builder/border-rule-editor.types.ts +++ b/src/app/grid-theme-builder/border-rule-editor.types.ts @@ -4,8 +4,9 @@ export type BorderTarget = 'header' | 'row' | 'pinned' | 'summaryPinned' | 'summ export interface BorderSignals { color: WritableSignal; - width: WritableSignal; style: WritableSignal; + /** Omitted for targets whose width is not themable on its own, e.g. the summary borders. */ + width?: WritableSignal; } export interface BorderOption { @@ -17,8 +18,8 @@ export const BORDER_OPTIONS: BorderOption[] = [ { value: 'header', label: 'Header' }, { value: 'row', label: 'Rows' }, { value: 'pinned', label: 'Pinned columns' }, - { value: 'summaryPinned', label: 'Summary Pinned Border' }, - { value: 'summary', label: 'Summary Borders' }, + { value: 'summaryPinned', label: 'Summary pinned border' }, + { value: 'summary', label: 'Summary borders' }, { value: 'activeCell', label: 'Active cell' }, ]; @@ -26,7 +27,7 @@ export const BORDER_DEFAULTS: Record @if (getControlType(key) === 'button-group') { - + @for (option of getControlOptions(key); track option) {