From 24ab05e248ba5d4455ffcb7e7c0b2ad418af073d 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 --- 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 7c951577823..04cf4779a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ All notable changes for each version of this project will be documented in this - Touch pans no longer leave slides partially moved or visible behind the current slide, for example when a pan is cancelled, reversed past the first or last slide of a non-looping carousel, released as a tap, or interrupted by removing slides or changing `gesturesSupport` or `vertical`. - Panning a carousel without slides no longer throws an error, and panning a carousel with a single slide no longer moves the slide against itself. - A slide activated through its `active` property now animates in the direction of its position, or in its own `direction` when one is set. +- `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. - `IgxNavigationToggleDirective`, `IgxNavigationCloseDirective` - The `igxNavToggle` and `igxNavClose` inputs are now public, so they can be set in applications compiled with `strictInputAccessModifiers`. Previously the inputs were private and setting them failed the template type check. - `IgxStepperComponent` 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 f24054ae75cf41249300a991b53af2ef40be07b3 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 --- CHANGELOG.md | 1 + .../styles/styles-registrar.service.spec.ts | 142 ++++++++++++++++++ .../styles/styles-registrar.service.ts | 38 ++++- .../grids/grid/src/grid-base.directive.ts | 13 +- 4 files changed, 192 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 04cf4779a3a..cbcbcddbea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All notable changes for each version of this project will be documented in this - A slide activated through its `active` property now animates in the direction of its position, or in its own `direction` when one is set. - `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. - `IgxNavigationToggleDirective`, `IgxNavigationCloseDirective` - The `igxNavToggle` and `igxNavClose` inputs are now public, so they can be set in applications compiled with `strictInputAccessModifiers`. Previously the inputs were private and setting them failed the template type check. - `IgxStepperComponent` 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..0a054c676fa --- /dev/null +++ b/projects/igniteui-angular/core/src/services/styles/styles-registrar.service.spec.ts @@ -0,0 +1,142 @@ +import { Component, ElementRef, inject, OnDestroy, ViewEncapsulation } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +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 bfaf59cbae1..9b0424fd5f0 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'; @@ -3454,7 +3460,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 ef5f12b0ce9cadeaf4362b1c1eb67af60adea6b7 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 28 Oct 2026 14:02:41 +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 914454f171c..78ff4a050df 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 { TestBed } 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'; import { describe, it, expect, beforeEach, vi } from 'vitest'; const INPUT_GROUP_CSS_CLASS = 'igx-input-group'; @@ -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 57831fce2d12a8e917c874f2ac83bb65847c3f5c Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 28 Oct 2026 14:30:53 +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 f307858be04..34745cfb2b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ All notable changes for each version of this project will be documented in this - Autosizing a child column of a column layout now fits its content, instead of keeping the width of its layout area. - `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. - `IgxNavigationToggleDirective`, `IgxNavigationCloseDirective` - The `igxNavToggle` and `igxNavClose` inputs are now public, so they can be set in applications compiled with `strictInputAccessModifiers`. Previously the inputs were private and setting them failed the template type check. - `IgxPivotGridComponent`