From 6dbb283d1e5783f048feeb43e1d0f925f8ee3f3d Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 22 Oct 2026 14:21:16 +0300 Subject: [PATCH 1/4] perf(grid): split multi-argument :where() theme selectors Chromium (and WebKit) only bucket a rule by a :where()/:is() subject when it holds a single selector; multi-argument lists land in the universal bucket and are matched against every element on each style recalculation. Wrap each selector of the scope() list in its own :where(), and split the header item rule the same way. Specificity and matching are unchanged. Closes #17793 (cherry picked from commit 24ab05e248ba5d4455ffcb7e7c0b2ad418af073d) --- CHANGELOG.md | 2 ++ .../src/core/styles/spec/_mixins.spec.scss | 22 +++++++++++++++++++ .../core/src/core/styles/themes/_scoping.scss | 18 +++++++++++---- .../igniteui-angular/grids/themes/_base.scss | 4 +++- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cadad248587..59b58459603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes for each version of this project will be documented in this - `IgxDialogComponent` - Fixed `open()` leaving dialogs hidden when called through an `OnPush` child component from a parent or an asynchronous callback, in both zoneless and ZoneJS applications. +- `IgxGridComponent`, `IgxTreeGridComponent`, `IgxHierarchicalGridComponent`, `IgxPivotGridComponent` + - Restored the style recalculation performance of 21.2.x in Chromium-based browsers. The grid, Excel-style filtering and query builder themes no longer use multi-selector `:where()` lists as rule subjects, which Chromium matched against every element on each style recalculation. The styles are otherwise unchanged. - `IgxGridExcelStyleFilteringComponent` - Menu spacing now follows `--ig-size` when `minHeight` or `maxHeight` is set, including when rendered in an external outlet or outside the grid. Previously it could fall back to large spacing (#17589). - **Forms** diff --git a/projects/igniteui-angular/core/src/core/styles/spec/_mixins.spec.scss b/projects/igniteui-angular/core/src/core/styles/spec/_mixins.spec.scss index c8e626e7d03..d71c0a9b41e 100644 --- a/projects/igniteui-angular/core/src/core/styles/spec/_mixins.spec.scss +++ b/projects/igniteui-angular/core/src/core/styles/spec/_mixins.spec.scss @@ -32,6 +32,28 @@ } } +// stylelint-disable max-nesting-depth +@include describe('Scope') { + @include it('should wrap each selector of a list in its own :where()') { + @include assert() { + @include output() { + @include scoping.scope(test, '.a, b, .c .d', ()) using ($schema) { + color: red; + } + } + + @include expect() { + @layer ig.test { + :where(.a), :where(b), :where(.c .d) { + color: red; + } + } + } + } + } +} +// stylelint-enable max-nesting-depth + @include describe('Generate color class') { @include it('should concatenate the name, variant, prefix, and suffix in the correct order.') { $name: primary; diff --git a/projects/igniteui-angular/core/src/core/styles/themes/_scoping.scss b/projects/igniteui-angular/core/src/core/styles/themes/_scoping.scss index 987dc83939c..dfb0dc6b53c 100644 --- a/projects/igniteui-angular/core/src/core/styles/themes/_scoping.scss +++ b/projects/igniteui-angular/core/src/core/styles/themes/_scoping.scss @@ -1,4 +1,6 @@ +@use 'sass:list'; @use 'sass:map'; +@use 'sass:selector'; @use 'igniteui-theming/sass/bem' as *; @use 'igniteui-theming/sass/themes' as *; @forward 'igniteui-theming/sass/themes'; @@ -89,8 +91,10 @@ } /// Wraps the passed content in a named cascade layer and scopes it to the given -/// selector through `:where()`, keeping specificity at zero. The active schema is -/// yielded to the content block so callers can resolve themes against it. +/// selector through `:where()`, keeping specificity at zero. Each selector of a list +/// gets its own `:where()`, as Chromium can't bucket multi-argument `:where()` subjects +/// and would match them against every element. The active schema is yielded to the +/// content block so callers can resolve themes against it. /// @access private /// @param {String} $layer - The cascade layer name (the `ig.` prefix is added by `layer`). /// @param {String} $selector - The selector the emitted styles are scoped to. @@ -98,10 +102,16 @@ /// @content The declarations to emit; receives `$schema` as its single argument, /// so include it with `using ($schema)`. /// @requires {mixin} layer -/// @output The content wrapped in `@layer ig.#{$layer}` inside a `:where(#{$selector})` rule. +/// @output The content wrapped in `@layer ig.#{$layer}` inside a `:where(a), :where(b)` rule. @mixin scope($layer, $selector, $schema) { + $_scoped: (); + + @each $complex in selector.parse($selector) { + $_scoped: list.append($_scoped, ':where(#{$complex})', $separator: comma); + } + @include layer($layer) { - :where(#{$selector}) { + #{$_scoped} { @content($schema); } } diff --git a/projects/igniteui-angular/grids/themes/_base.scss b/projects/igniteui-angular/grids/themes/_base.scss index 5f53c341efd..84e7a799420 100644 --- a/projects/igniteui-angular/grids/themes/_base.scss +++ b/projects/igniteui-angular/grids/themes/_base.scss @@ -2564,7 +2564,9 @@ flex-flow: column nowrap; position: relative; - :where(.igx-grid-th, .igx-grid-thead__group) { + // Split :where()s, a multi-argument subject defeats Chromium's rule bucketing. + :where(.igx-grid-th), + :where(.igx-grid-thead__group) { flex: 1 1 auto; } From cd687baa1293028e00ea5607fd54f05366acc0e0 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 22 Oct 2026 18:58:26 +0300 Subject: [PATCH 2/4] perf(grid): retain styles of components churned by data operations Angular removes a ViewEncapsulation.None component's styles with its last instance and re-inserts them with the next, restyling the page. Add IgxStylesRegistrar.retain() to keep them via one detached instance per app, and retain the components the grid creates and destroys while filtering, grouping, editing and loading. Refs #17794 (cherry picked from commit f24054ae75cf41249300a991b53af2ef40be07b3) --- CHANGELOG.md | 1 + .../styles/styles-registrar.service.spec.ts | 141 ++++++++++++++++++ .../styles/styles-registrar.service.ts | 38 ++++- .../grids/grid/src/grid-base.directive.ts | 13 +- 4 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 projects/igniteui-angular/core/src/services/styles/styles-registrar.service.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b58459603..ac99b00c38b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes for each version of this project will be documented in this - Fixed `open()` leaving dialogs hidden when called through an `OnPush` child component from a parent or an asynchronous callback, in both zoneless and ZoneJS applications. - `IgxGridComponent`, `IgxTreeGridComponent`, `IgxHierarchicalGridComponent`, `IgxPivotGridComponent` - Restored the style recalculation performance of 21.2.x in Chromium-based browsers. The grid, Excel-style filtering and query builder themes no longer use multi-selector `:where()` lists as rule subjects, which Chromium matched against every element on each style recalculation. The styles are otherwise unchanged. + - Filtering, grouping, editing and toggling `isLoading` no longer add and remove the stylesheets of the badge, chip, checkbox, input group, date picker and circular progress bar components, which forced a restyle of the whole page each time. The grid now keeps these stylesheets for the application's lifetime. As a result, the auto-generated `id` of these components starts from `1` instead of `0` on pages with a grid. - `IgxGridExcelStyleFilteringComponent` - Menu spacing now follows `--ig-size` when `minHeight` or `maxHeight` is set, including when rendered in an external outlet or outside the grid. Previously it could fall back to large spacing (#17589). - **Forms** diff --git a/projects/igniteui-angular/core/src/services/styles/styles-registrar.service.spec.ts b/projects/igniteui-angular/core/src/services/styles/styles-registrar.service.spec.ts new file mode 100644 index 00000000000..f0ce598df72 --- /dev/null +++ b/projects/igniteui-angular/core/src/services/styles/styles-registrar.service.spec.ts @@ -0,0 +1,141 @@ +import { Component, ElementRef, inject, OnDestroy, ViewEncapsulation } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { PlatformUtil } from '../../core/utils'; +import { IgxStylesRegistrar } from './styles-registrar.service'; + +let instances: ElementRef[] = []; +let destroyed = 0; + +@Component({ + selector: 'igx-retain-test', + template: '', + styles: ['.igx-retain-test { color: red; }'], + encapsulation: ViewEncapsulation.None +}) +class RetainTestComponent implements OnDestroy { + constructor() { + instances.push(inject(ElementRef)); + } + + public ngOnDestroy(): void { + destroyed++; + } +} + +@Component({ + selector: 'igx-plain-test', + template: '', + styles: ['.igx-plain-test { color: blue; }'], + encapsulation: ViewEncapsulation.None +}) +class PlainTestComponent { } + +// Compiled styles are reformatted, so match on the class name. +const hasStyle = (className: string) => + Array.from(document.head.querySelectorAll('style')).some(style => style.textContent?.includes(className)); + +describe('IgxStylesRegistrar', () => { + describe('register', () => { + let initialSheets: CSSStyleSheet[]; + + beforeEach(() => { + initialSheets = [...document.adoptedStyleSheets]; + }); + + afterEach(() => { + document.adoptedStyleSheets = initialSheets; + document.head.querySelectorAll('style[data-registrar-test]').forEach(style => style.remove()); + }); + + it('should adopt a stylesheet once per id', () => { + const registrar = TestBed.inject(IgxStylesRegistrar); + const id = Symbol('once'); + + registrar.register(id, '.igx-register-once { color: red; }'); + registrar.register(id, '.igx-register-once { color: red; }'); + + const added = document.adoptedStyleSheets.slice(initialSheets.length); + expect(added.length).toBe(1); + expect(added[0].cssRules[0].cssText).toContain('.igx-register-once'); + }); + + it('should adopt a separate stylesheet for each id', () => { + const registrar = TestBed.inject(IgxStylesRegistrar); + + registrar.register(Symbol('first'), '.igx-register-first { color: red; }'); + registrar.register(Symbol('second'), '.igx-register-second { color: red; }'); + + expect(document.adoptedStyleSheets.length - initialSheets.length).toBe(2); + }); + + it('should register an id once per document across injectors', () => { + const id = Symbol('per-document'); + + TestBed.inject(IgxStylesRegistrar).register(id, '.igx-register-doc { color: red; }'); + TestBed.resetTestingModule(); + TestBed.inject(IgxStylesRegistrar).register(id, '.igx-register-doc { color: red; }'); + + expect(document.adoptedStyleSheets.length - initialSheets.length).toBe(1); + }); + + it('should append a style element outside the browser', () => { + TestBed.configureTestingModule({ + providers: [{ provide: PlatformUtil, useValue: { isBrowser: false } }] + }); + + TestBed.inject(IgxStylesRegistrar).register(Symbol('server'), '.igx-register-server { color: red; }'); + + const style = Array.from(document.head.querySelectorAll('style')) + .find(s => s.textContent?.includes('.igx-register-server')); + style?.setAttribute('data-registrar-test', ''); + expect(style).toBeDefined(); + expect(document.adoptedStyleSheets.length).toBe(initialSheets.length); + }); + }); + + describe('retain', () => { + // Angular tears down the previous test's app after afterEach hooks run. + beforeEach(() => { + instances = []; + destroyed = 0; + }); + + it('should keep a component\'s styles after its last instance is destroyed', () => { + TestBed.inject(IgxStylesRegistrar).retain(RetainTestComponent); + + const fixture = TestBed.createComponent(RetainTestComponent); + fixture.destroy(); + + expect(hasStyle('igx-retain-test')).toBe(true); + }); + + it('should not keep the styles of components that are not retained', () => { + const fixture = TestBed.createComponent(PlainTestComponent); + expect(hasStyle('igx-plain-test')).toBe(true); + + fixture.destroy(); + + expect(hasStyle('igx-plain-test')).toBe(false); + }); + + it('should create a single detached instance per component', () => { + const registrar = TestBed.inject(IgxStylesRegistrar); + + registrar.retain(RetainTestComponent); + registrar.retain(RetainTestComponent, RetainTestComponent); + + expect(instances.length).toBe(1); + expect(instances[0].nativeElement.isConnected).toBe(false); + }); + + it('should destroy the retained instances when the application is destroyed', () => { + TestBed.inject(IgxStylesRegistrar).retain(RetainTestComponent); + expect(destroyed).toBe(0); + + TestBed.resetTestingModule(); + + expect(destroyed).toBe(1); + expect(hasStyle('igx-retain-test')).toBe(false); + }); + }); +}); diff --git a/projects/igniteui-angular/core/src/services/styles/styles-registrar.service.ts b/projects/igniteui-angular/core/src/services/styles/styles-registrar.service.ts index ce8adab84e2..fa7e45cb37a 100644 --- a/projects/igniteui-angular/core/src/services/styles/styles-registrar.service.ts +++ b/projects/igniteui-angular/core/src/services/styles/styles-registrar.service.ts @@ -1,4 +1,4 @@ -import { inject, Injectable } from '@angular/core'; +import { ApplicationRef, ComponentRef, createComponent, EnvironmentInjector, inject, Injectable, Type } from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { PlatformUtil } from '../../core/utils'; @@ -27,10 +27,14 @@ import { PlatformUtil } from '../../core/utils'; */ const _registries = new WeakMap>(); +/** Keyed by application, as Angular shares component styles per application. */ +const _retained = new WeakMap, ComponentRef>>(); + @Injectable({ providedIn: 'root' }) export class IgxStylesRegistrar { private readonly _document = inject(DOCUMENT); private readonly _platform = inject(PlatformUtil); + private readonly _injector = inject(EnvironmentInjector); /** * Registers a stylesheet identified by `id`. Subsequent calls with the same @@ -62,4 +66,36 @@ export class IgxStylesRegistrar { this._document.head?.appendChild(style); } } + + /** + * Keeps the components' styles in the document for the application's lifetime, through a + * detached instance of each. Otherwise Angular removes them with the last instance and + * re-inserts them with the next one, restyling the page each time. + * + * @param components - The components whose styles to retain. + * + * @example + * ```typescript + * inject(IgxStylesRegistrar).retain(IgxBadgeComponent, IgxChipComponent); + * ``` + */ + public retain(...components: Type[]): void { + const appRef = this._injector.get(ApplicationRef); + let refs = _retained.get(appRef); + + if (!refs) { + refs = new Map(); + _retained.set(appRef, refs); + appRef.onDestroy(() => { + _retained.get(appRef)?.forEach(ref => ref.destroy()); + _retained.delete(appRef); + }); + } + + for (const type of components) { + if (!refs.has(type)) { + refs.set(type, createComponent(type, { environmentInjector: this._injector })); + } + } + } } diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts index 5792702e28c..2977216fcaa 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -118,6 +118,12 @@ import { import { IgxGridGroupByAreaComponent } from './grouping/grid-group-by-area.component'; import { IgxPaginatorToken, type IgxPaginatorComponent } from 'igniteui-angular/paginator'; import { IgxSnackbarComponent } from 'igniteui-angular/snackbar'; +import { IgxBadgeComponent } from 'igniteui-angular/badge'; +import { IgxCheckboxComponent } from 'igniteui-angular/checkbox'; +import { IgxChipComponent } from 'igniteui-angular/chips'; +import { IgxDatePickerComponent } from 'igniteui-angular/date-picker'; +import { IgxInputGroupComponent } from 'igniteui-angular/input-group'; +import { IgxCircularProgressBarComponent } from 'igniteui-angular/progressbar'; import { CellType, CharSeparatedValueData, DropPosition, FilterMode, getUUID, GridCellMergeMode, GridKeydownTargetType, GridPagingMode, GridSelectionMode, GridServiceType, GridSummaryPosition, GridType, GridValidationTrigger, IActiveNode, IActiveNodeChangeEventArgs, ICellPosition, IClipboardOptions, IColumnMovingEndEventArgs, IColumnMovingEventArgs, IColumnMovingStartEventArgs, IColumnResizeEventArgs, IColumnSelectionEventArgs, IColumnVisibilityChangedEventArgs, IColumnVisibilityChangingEventArgs, IFilteringEventArgs, IGridCellEventArgs, IGridClipboardEvent, IGridContextMenuEventArgs, IGridEditDoneEventArgs, IGridEditEventArgs, IGridFormGroupCreatedEventArgs, IGridKeydownEventArgs, IGridRowEventArgs, IGridScrollEventArgs, IGridToolbarExportEventArgs, IGridValidationStatusEventArgs, IGX_GRID_SERVICE_BASE, IgxAdvancedFilteringDialogComponent, IgxCell, IgxColumnComponent, IgxColumnGroupComponent, IgxColumnResizingService, IgxDragIndicatorIconDirective, IgxEditRow, IgxExcelStyleHeaderIconDirective, IgxExcelStyleLoadingValuesTemplateDirective, IgxFilteringService, IgxGridBodyDirective, IgxGridCellComponent, IgxGridColumnResizerComponent, IgxGridEmptyTemplateContext, IgxGridEmptyTemplateDirective, IgxGridExcelStyleFilteringComponent, IgxGridFilteringCellComponent, IgxGridFilteringRowComponent, IgxGridHeaderComponent, IgxGridHeaderGroupComponent, IgxGridHeaderRowComponent, IgxGridHeaderTemplateContext, IgxGridLoadingTemplateDirective, IgxGridNavigationService, IgxGridPinningActionsComponent, IgxGridRowDragGhostContext, IgxGridRowEditActionsTemplateContext, IgxGridRowEditTemplateContext, IgxGridRowEditTextTemplateContext, IgxGridRowTemplateContext, IgxGridSelectionService, IgxGridSummaryService, IgxGridTemplateContext, IgxGridToolbarComponent, IgxGridValidationService, IgxHeaderCollapsedIndicatorDirective, IgxHeaderExpandedIndicatorDirective, IgxHeadSelectorDirective, IgxHeadSelectorTemplateContext, IgxRowAddTextDirective, IgxRowCollapsedIndicatorDirective, IgxRowDirective, IgxRowDragGhostDirective, IgxRowEditActionsDirective, IgxRowEditTabStopDirective, IgxRowEditTemplateDirective, IgxRowEditTextDirective, IgxRowExpandedIndicatorDirective, IgxRowSelectorDirective, IgxRowSelectorTemplateContext, IgxSortAscendingHeaderIconDirective, IgxSortDescendingHeaderIconDirective, IgxSortHeaderIconDirective, IgxSummaryRowComponent, IgxToolbarToken, IPinColumnCancellableEventArgs, IPinColumnEventArgs, IPinningConfig, IPinRowEventArgs, IRowDataCancelableEventArgs, IRowDataEventArgs, IRowDragEndEventArgs, IRowDragStartEventArgs, IRowSelectionEventArgs, IRowToggleEventArgs, ISearchInfo, ISizeInfo, ISortingEventArgs, RowEditPositionStrategy, RowPinningPosition, RowType, WatchChanges } from 'igniteui-angular/grids/core'; import { getCurrentI18n, getNumberFormatter, IResourceChangeEventArgs, } from 'igniteui-i18n-core'; import { I18N_FORMATTER, IgxStylesRegistrar } from 'igniteui-angular/core'; @@ -3467,7 +3473,12 @@ export abstract class IgxGridBaseDirective implements GridType, } constructor() { - inject(IgxStylesRegistrar).register(GRID_STYLES_ID, GRID_BASE_CSS); + const styles = inject(IgxStylesRegistrar); + styles.register(GRID_STYLES_ID, GRID_BASE_CSS); + styles.retain( + IgxBadgeComponent, IgxChipComponent, IgxCheckboxComponent, + IgxInputGroupComponent, IgxDatePickerComponent, IgxCircularProgressBarComponent + ); this.initLocale(); this._transactions = this.transactionFactory.create(TRANSACTION_TYPE.None); this._transactions.cloneStrategy = this.dataCloneStrategy; From 5929ceff7f6d0197890eedfc686d3ee0060e2833 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 28 Oct 2026 14:02:45 +0200 Subject: [PATCH 3/4] fix(input-group): guard file-input getters when no input is projected The detached input group kept alive by IgxStylesRegistrar.retain() has no projected igxInput, so a runtime THEME_TOKEN change ran detectChanges() on it and threw from isFileType. Make the getters null-safe. --- .../input-group/input-group.component.spec.ts | 23 +++++++++++++++++-- .../src/input-group/input-group.component.ts | 6 ++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/projects/igniteui-angular/input-group/src/input-group/input-group.component.spec.ts b/projects/igniteui-angular/input-group/src/input-group/input-group.component.spec.ts index daff72f3cae..93e7bb80e3b 100644 --- a/projects/igniteui-angular/input-group/src/input-group/input-group.component.spec.ts +++ b/projects/igniteui-angular/input-group/src/input-group/input-group.component.spec.ts @@ -1,11 +1,12 @@ -import { Component, ViewChild, ElementRef, inject, ChangeDetectionStrategy } from '@angular/core'; +import { Component, ViewChild, ElementRef, inject, ChangeDetectionStrategy, createComponent, EnvironmentInjector } from '@angular/core'; import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { IgxInputGroupComponent } from './input-group.component'; import { UIInteractions } from '../../../test-utils/ui-interactions.spec'; import { IgxInputDirective, IgxPrefixDirective, IgxSuffixDirective } from '../public_api'; import { IGX_INPUT_GROUP_TYPE, IgxInputGroupType } from './inputGroupType'; -import { InputResourceStringsEN, changei18n } from 'igniteui-angular/core'; +import { InputResourceStringsEN, changei18n, THEME_TOKEN } from 'igniteui-angular/core'; +import { config } from 'rxjs'; const INPUT_GROUP_CSS_CLASS = 'igx-input-group'; const INPUT_GROUP_BOX_CSS_CLASS = 'igx-input-group--box'; @@ -259,6 +260,24 @@ describe('IgxInputGroup', () => { expect(fixture.componentInstance.materialGroup.isTypeIndigo).toBe(false); })); + it('should not throw on theme change when no igxInput is projected', async () => { + const errors: unknown[] = []; + const previous = config.onUnhandledError; + config.onUnhandledError = error => errors.push(error); + const token = TestBed.inject(THEME_TOKEN); + // Mirrors the detached instance IgxStylesRegistrar.retain() creates + const ref = createComponent(IgxInputGroupComponent, { environmentInjector: TestBed.inject(EnvironmentInjector) }); + + try { + token.set(token.theme === 'fluent' ? 'material' : 'fluent'); + await new Promise(resolve => setTimeout(resolve)); + expect(errors).toEqual([]); + } finally { + config.onUnhandledError = previous; + ref.destroy(); + } + }); + describe('Resource Strings', () => { it('should return full resource strings when partial resourceStrings are set', () => { const fix = TestBed.createComponent(InputGroupComponent); diff --git a/projects/igniteui-angular/input-group/src/input-group/input-group.component.ts b/projects/igniteui-angular/input-group/src/input-group/input-group.component.ts index 10f082748e7..661d74c5270 100644 --- a/projects/igniteui-angular/input-group/src/input-group/input-group.component.ts +++ b/projects/igniteui-angular/input-group/src/input-group/input-group.component.ts @@ -390,13 +390,13 @@ export class IgxInputGroupComponent implements IgxInputGroupBase, AfterContentCh /** @hidden @internal */ @HostBinding('class.igx-input-group--file') public get isFileType() { - return this.input.type === 'file'; + return this.input?.type === 'file'; } /** @hidden @internal */ @HostBinding('class.igx-file-input') public get isFileInput() { - return this.input.type === 'file'; + return this.input?.type === 'file'; } /** @hidden @internal */ @@ -419,7 +419,7 @@ export class IgxInputGroupComponent implements IgxInputGroupBase, AfterContentCh /** @hidden @internal */ public get fileNames() { - return this.input.fileNames || this.resourceStrings.igx_input_file_placeholder; + return this.input?.fileNames || this.resourceStrings.igx_input_file_placeholder; } /** From 87365d9cf1709b23f3483caa9bf28f9532b5348f Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 28 Oct 2026 14:30:57 +0200 Subject: [PATCH 4/4] docs(changelog): clarify id shift from retained grid components --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca8be112616..32ff7de5370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ All notable changes for each version of this project will be documented in this - Fixed `open()` leaving dialogs hidden when called through an `OnPush` child component from a parent or an asynchronous callback, in both zoneless and ZoneJS applications. - `IgxGridComponent`, `IgxTreeGridComponent`, `IgxHierarchicalGridComponent`, `IgxPivotGridComponent` - Restored the style recalculation performance of 21.2.x in Chromium-based browsers. The grid, Excel-style filtering and query builder themes no longer use multi-selector `:where()` lists as rule subjects, which Chromium matched against every element on each style recalculation. The styles are otherwise unchanged. - - Filtering, grouping, editing and toggling `isLoading` no longer add and remove the stylesheets of the badge, chip, checkbox, input group, date picker and circular progress bar components, which forced a restyle of the whole page each time. The grid now keeps these stylesheets for the application's lifetime. As a result, the auto-generated `id` of these components starts from `1` instead of `0` on pages with a grid. + - Filtering, grouping, editing and toggling `isLoading` no longer add and remove the stylesheets of the badge, chip, checkbox, input group, date picker and circular progress bar components, which forced a restyle of the whole page each time. The grid now keeps these stylesheets for the application's lifetime. As a result, the first grid in an application uses up one auto-generated `id` of the badge, chip, checkbox, date picker and circular progress bar (including `igx-circular-gradient-N`), so instances created after it get an `id` one higher than before. Later grids do not shift the ids further. - `IgxGridExcelStyleFilteringComponent` - Menu spacing now follows `--ig-size` when `minHeight` or `maxHeight` is set, including when rendered in an external outlet or outside the grid. Previously it could fall back to large spacing (#17589). - **Forms**