diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c785ceede4..d4962e7a8c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,13 @@ All notable changes for each version of this project will be documented in this
- **Theming** - Scrollbar arrow buttons cannot be styled or enabled through the standard properties, and `scrollbar-width: thin` removes them where the platform draws them.
- **Firefox** - The `scrollbar-color` and `scrollbar-width` properties are not supported on Firefox versions prior to 64, so the scrollbars in those versions will render with the platform default colors and size.
+### Bug Fixes
+
+- `IgxCheckboxComponent`
+ - Fixed the tick-mark icon rendering with the Indigo shape (rounded rect + custom path) inside CSS-scoped subtrees that use a different design system than the application's global theme, e.g. a `material`-themed widget nested inside an `indigo`-themed app. Both tick-mark variants are now always rendered and toggled purely via CSS (`@container style(--ig-theme: indigo)`), removing the dependency on JS-side theme detection that could go stale in nested/multi-theme scenarios (#15021).
+- **Ripple**
+ - Fixed `[igxRipple]` unconditionally stamping `--ig-theme`/`--ig-theme-variant` (from its own compile-time schema) onto its host element, which broke runtime theme inheritance for any content nested inside a ripple host (e.g. a checkbox's tick mark) when that content sat in a differently CSS-scoped theme than the app's global one.
+
## 22.1.0
### New Features
diff --git a/projects/igniteui-angular/calendar/src/calendar/days-view/days-view.component.ts b/projects/igniteui-angular/calendar/src/calendar/days-view/days-view.component.ts
index c849582c50f..31463d42cb0 100644
--- a/projects/igniteui-angular/calendar/src/calendar/days-view/days-view.component.ts
+++ b/projects/igniteui-angular/calendar/src/calendar/days-view/days-view.component.ts
@@ -12,8 +12,7 @@ import {
ChangeDetectorRef,
ChangeDetectionStrategy,
ViewEncapsulation,
- inject,
- AfterContentChecked
+ inject
} from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { TitleCasePipe } from '@angular/common';
@@ -28,11 +27,7 @@ import {
getNextActiveDate,
getPreviousActiveDate,
intoChunks,
- isDateInRanges,
- getComponentTheme,
- IgxTheme,
- THEME_TOKEN,
- ThemeToken
+ isDateInRanges
} from 'igniteui-angular/core';
import { IgxCalendarBaseDirective } from '../calendar-base';
import { IViewChangingEventArgs } from './days-view.interface';
@@ -58,10 +53,9 @@ let NEXT_ID = 0;
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [IgxDayItemComponent, TitleCasePipe, DayDigitPipe]
})
-export class IgxDaysViewComponent extends IgxCalendarBaseDirective implements AfterContentChecked {
+export class IgxDaysViewComponent extends IgxCalendarBaseDirective {
protected el = inject(ElementRef);
public override cdr = inject(ChangeDetectorRef);
- private themeToken: ThemeToken = inject(THEME_TOKEN);
#standalone = true;
/**
@@ -210,65 +204,10 @@ export class IgxDaysViewComponent extends IgxCalendarBaseDirective implements Af
private _hideLeadingDays!: boolean;
private _hideTrailingDays!: boolean;
private _showActiveDay!: boolean;
- private _theme: IgxTheme;
@HostBinding('class.igx-days-view')
public defaultClass = true;
- // Theme-specific classes
- @HostBinding('class.igx-days-view--material')
- protected get isMaterial(): boolean {
- return this._theme === 'material';
- }
-
- @HostBinding('class.igx-days-view--fluent')
- protected get isFluent(): boolean {
- return this._theme === 'fluent';
- }
-
- @HostBinding('class.igx-days-view--bootstrap')
- protected get isBootstrap(): boolean {
- return this._theme === 'bootstrap';
- }
-
- @HostBinding('class.igx-days-view--indigo')
- protected get isIndigo(): boolean {
- return this._theme === 'indigo';
- }
-
- /**
- * @hidden
- */
- constructor() {
- super();
- this._theme = this.themeToken.theme;
-
- const themeChange = this.themeToken.onChange((theme) => {
- if (this._theme !== theme) {
- this._theme = theme;
- this.cdr.detectChanges();
- }
- });
-
- this._destroyRef.onDestroy(() => themeChange.unsubscribe());
- }
-
- private setComponentTheme() {
- // allow DOM theme override (same pattern as input-group)
- if (!this.themeToken.preferToken) {
- const theme = getComponentTheme(this.el.nativeElement);
-
- if (theme && theme !== this._theme) {
- this._theme = theme;
- this.cdr.markForCheck();
- }
- }
- }
-
- public ngAfterContentChecked() {
- this.setComponentTheme();
- }
-
/**
* @hidden
*/
diff --git a/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.html b/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.html
index 9bab0879680..a2fce07ba00 100644
--- a/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.html
+++ b/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.html
@@ -25,16 +25,13 @@
class="igx-checkbox__composite-wrapper"
>
- @if (theme === 'indigo') {
-
- } @else {
-
- }
+
+
diff --git a/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.spec.ts b/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.spec.ts
index 80db7e6bce1..87c8e7d894b 100644
--- a/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.spec.ts
+++ b/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.spec.ts
@@ -1,4 +1,4 @@
-import { Component, ViewChild, inject, ChangeDetectionStrategy } from '@angular/core';
+import { Component, ViewChild, ElementRef, inject, ChangeDetectionStrategy } from '@angular/core';
import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { UntypedFormBuilder, FormsModule, ReactiveFormsModule, Validators, NgForm } from '@angular/forms';
import { By } from '@angular/platform-browser';
@@ -21,11 +21,45 @@ describe('IgxCheckbox', () => {
CheckboxDisabledTransitionsComponent,
CheckboxFormComponent,
CheckboxFormGroupComponent,
+ CheckboxNestedThemeScopeComponent,
IgxCheckboxComponent
]
}).compileComponents();
}));
+ /**
+ * Regression tests for https://github.com/IgniteUI/igniteui-angular/issues/15021
+ */
+ describe('Theming (issue #15021)', () => {
+ const getMarks = (host: HTMLElement) => ({
+ indigoMark: host.querySelector('.igx-checkbox__composite-mark--indigo') as HTMLElement,
+ defaultMark: host.querySelector('.igx-checkbox__composite-mark:not(.igx-checkbox__composite-mark--indigo)') as HTMLElement
+ });
+ const isVisible = (el: HTMLElement) => getComputedStyle(el).display !== 'none';
+
+ it('renders both tick-mark SVGs, with only one visible per the active --ig-theme', () => {
+ const fixture = TestBed.createComponent(CheckboxNestedThemeScopeComponent);
+ fixture.detectChanges();
+
+ const root = getMarks(fixture.componentInstance.rootCbHost.nativeElement);
+ expect(isVisible(root.defaultMark)).toBe(true);
+ expect(isVisible(root.indigoMark)).toBe(false);
+
+ const indigoScoped = getMarks(fixture.componentInstance.indigoCbHost.nativeElement);
+ expect(isVisible(indigoScoped.indigoMark)).toBe(true);
+ expect(isVisible(indigoScoped.defaultMark)).toBe(false);
+ });
+
+ it('shows the material tick mark for a checkbox in a material-scoped wrapper nested inside an indigo-scoped one', () => {
+ const fixture = TestBed.createComponent(CheckboxNestedThemeScopeComponent);
+ fixture.detectChanges();
+
+ const nested = getMarks(fixture.componentInstance.materialCbHost.nativeElement);
+ expect(isVisible(nested.defaultMark)).toBe(true);
+ expect(isVisible(nested.indigoMark)).toBe(false);
+ });
+ });
+
it('Initializes a checkbox', () => {
const fixture = TestBed.createComponent(InitCheckboxComponent);
fixture.detectChanges();
@@ -412,6 +446,28 @@ describe('IgxCheckbox', () => {
});
});
+@Component({
+ template: `Root
+
+
Indigo scope
+
+ Material scope nested in indigo
+
+
`,
+ changeDetection: ChangeDetectionStrategy.Eager,
+ imports: [IgxCheckboxComponent]
+})
+class CheckboxNestedThemeScopeComponent {
+ @ViewChild('rootCb', { static: true }) public rootCb: IgxCheckboxComponent;
+ @ViewChild('indigoCb', { static: true }) public indigoCb: IgxCheckboxComponent;
+ @ViewChild('materialCb', { static: true }) public materialCb: IgxCheckboxComponent;
+ @ViewChild('rootCb', { static: true, read: ElementRef }) public rootCbHost: ElementRef;
+ @ViewChild('indigoCb', { static: true, read: ElementRef }) public indigoCbHost: ElementRef;
+ @ViewChild('materialCb', { static: true, read: ElementRef }) public materialCbHost: ElementRef;
+ @ViewChild('indigoWrapper', { static: true }) public indigoWrapper: ElementRef;
+ @ViewChild('materialWrapper', { static: true }) public materialWrapper: ElementRef;
+}
+
@Component({
template: `Init`,
changeDetection: ChangeDetectionStrategy.Eager,
diff --git a/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.ts b/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.ts
index 3b37db76114..074fd84d7b9 100644
--- a/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.ts
+++ b/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.ts
@@ -63,58 +63,6 @@ export class IgxCheckboxComponent
@HostBinding('class.igx-checkbox')
public override cssClass = 'igx-checkbox';
- /**
- * Returns if the component is of type `material`.
- *
- * @example
- * ```typescript
- * let checkbox = this.checkbox.material;
- * ```
- */
- @HostBinding('class.igx-checkbox--material')
- protected get material() {
- return this.theme === 'material';
- }
-
- /**
- * Returns if the component is of type `indigo`.
- *
- * @example
- * ```typescript
- * let checkbox = this.checkbox.indigo;
- * ```
- */
- @HostBinding('class.igx-checkbox--indigo')
- protected get indigo() {
- return this.theme === 'indigo';
- }
-
- /**
- * Returns if the component is of type `bootstrap`.
- *
- * @example
- * ```typescript
- * let checkbox = this.checkbox.bootstrap;
- * ```
- */
- @HostBinding('class.igx-checkbox--bootstrap')
- protected get bootstrap() {
- return this.theme === 'bootstrap';
- }
-
- /**
- * Returns if the component is of type `fluent`.
- *
- * @example
- * ```typescript
- * let checkbox = this.checkbox.fluent;
- * ```
- */
- @HostBinding('class.igx-checkbox--fluent')
- protected get fluent() {
- return this.theme === 'fluent';
- }
-
/**
* Sets/gets whether the checkbox component is on focus.
* Default value is `false`.
diff --git a/projects/igniteui-angular/checkbox/src/checkbox/themes/_base.scss b/projects/igniteui-angular/checkbox/src/checkbox/themes/_base.scss
index f922ab3eb16..fd17bac5884 100644
--- a/projects/igniteui-angular/checkbox/src/checkbox/themes/_base.scss
+++ b/projects/igniteui-angular/checkbox/src/checkbox/themes/_base.scss
@@ -174,6 +174,10 @@ $_theme: digest-schema($material-checkbox);
z-index: 1;
}
+ @include e(composite-mark, $m: indigo) {
+ display: none;
+ }
+
@include e(ripple) {
--_ripple-size: #{rem(40px)};
diff --git a/projects/igniteui-angular/checkbox/src/checkbox/themes/shared/_indigo.scss b/projects/igniteui-angular/checkbox/src/checkbox/themes/shared/_indigo.scss
index c8cb47722ea..462b6e2f750 100644
--- a/projects/igniteui-angular/checkbox/src/checkbox/themes/shared/_indigo.scss
+++ b/projects/igniteui-angular/checkbox/src/checkbox/themes/shared/_indigo.scss
@@ -17,7 +17,12 @@ $_theme: digest-schema($indigo-checkbox);
}
}
- @include e(composite-mark) {
+ @include e(composite-mark, $not: indigo) {
+ display: none;
+ }
+
+ @include e(composite-mark, $m: indigo) {
+ display: initial;
stroke: unset;
stroke-linecap: unset;
stroke-width: unset;
@@ -86,7 +91,7 @@ $_theme: digest-schema($indigo-checkbox);
}
}
- @include e(composite-mark) {
+ @include e(composite-mark, $m: indigo) {
fill: none;
stroke-dashoffset: unset;
rotate: none;
@@ -152,13 +157,13 @@ $_theme: digest-schema($indigo-checkbox);
}
@include mx(disabled, checked) {
- @include e(composite-mark) {
+ @include e(composite-mark, $m: indigo) {
fill: var-get($_theme, 'disabled-tick-color');
}
}
@include mx(disabled, indeterminate) {
- @include e(composite-mark) {
+ @include e(composite-mark, $m: indigo) {
stroke: unset;
fill: none;
diff --git a/projects/igniteui-angular/core/src/core/styles/components/ripple/_ripple-theme.scss b/projects/igniteui-angular/core/src/core/styles/components/ripple/_ripple-theme.scss
index 36992d9b067..d48e764ce20 100644
--- a/projects/igniteui-angular/core/src/core/styles/components/ripple/_ripple-theme.scss
+++ b/projects/igniteui-angular/core/src/core/styles/components/ripple/_ripple-theme.scss
@@ -8,6 +8,11 @@
@mixin ripple($theme) {
@include tokens($theme, $mode: 'scoped', $scope: '[igxRipple]');
+ [igxRipple] {
+ --ig-theme: inherit;
+ --ig-theme-variant: inherit;
+ }
+
@include layer(base) {
%igx-ripple-display {
display: block;
@@ -24,6 +29,9 @@
}
%igx-ripple-wrapper {
+ --ig-theme: inherit;
+ --ig-theme-variant: inherit;
+
overflow: hidden;
}
}
diff --git a/projects/igniteui-angular/core/src/core/utils.ts b/projects/igniteui-angular/core/src/core/utils.ts
index 588bae542f6..e1883918ce4 100644
--- a/projects/igniteui-angular/core/src/core/utils.ts
+++ b/projects/igniteui-angular/core/src/core/utils.ts
@@ -681,10 +681,19 @@ export function normalizeURI(path: string) {
return path?.split('/').map(encodeURI).join('/');
}
+/**
+ * Reads the theme that is actually in effect at a given DOM position, based on the
+ * live `--ig-theme` custom property (see `themes/_scoping.scss`'s `themed()` mixin,
+ * the single source of truth for coupling emitted CSS to the runtime theme signal).
+ * `--ig-theme` inherits, so this reflects the nearest CSS-scoped `theme(...)` call
+ * (if any), not necessarily the app's root/global theme.
+ *
+ * @param el - The element to read the effective theme for.
+ */
export function getComponentTheme(el: Element) {
return globalThis.window
?.getComputedStyle(el)
- .getPropertyValue('--theme')
+ .getPropertyValue('--ig-theme')
.trim() as IgxTheme;
}
diff --git a/projects/igniteui-angular/directives/src/directives/checkbox/checkbox-base.directive.ts b/projects/igniteui-angular/directives/src/directives/checkbox/checkbox-base.directive.ts
index 7ac42387b17..07c1aa393e9 100644
--- a/projects/igniteui-angular/directives/src/directives/checkbox/checkbox-base.directive.ts
+++ b/projects/igniteui-angular/directives/src/directives/checkbox/checkbox-base.directive.ts
@@ -1,13 +1,8 @@
-import { Directive, EventEmitter, HostListener, HostBinding, Input, Output, ViewChild, ElementRef, ChangeDetectorRef, booleanAttribute, inject, DestroyRef, AfterViewInit } from '@angular/core';
+import { Directive, EventEmitter, HostListener, HostBinding, Input, Output, ViewChild, ElementRef, ChangeDetectorRef, booleanAttribute, inject, AfterViewInit } from '@angular/core';
import { NgControl, Validators } from '@angular/forms';
-import { IBaseEventArgs, getComponentTheme } from 'igniteui-angular/core';
+import { IBaseEventArgs } from 'igniteui-angular/core';
import { noop, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
-import {
- IgxTheme,
- THEME_TOKEN,
- ThemeToken,
-} from 'igniteui-angular/core';
export const LabelPosition = {
BEFORE: 'before',
@@ -25,7 +20,6 @@ let nextId = 0;
@Directive()
export class CheckboxBaseDirective implements AfterViewInit {
protected cdr = inject(ChangeDetectorRef);
- protected themeToken = inject(THEME_TOKEN);
public ngControl = inject(NgControl, { optional: true, self: true });
/**
@@ -238,17 +232,6 @@ export class CheckboxBaseDirective implements AfterViewInit {
if (this.ngControl !== null) {
this.ngControl.valueAccessor = this;
}
-
- this.theme = this.themeToken.theme;
-
- const themeChange = this.themeToken.onChange((theme) => {
- if (this.theme !== theme) {
- this.theme = theme;
- this.cdr.detectChanges();
- }
- });
-
- this.destroyRef.onDestroy(() => themeChange.unsubscribe());
}
/**
@@ -294,8 +277,6 @@ export class CheckboxBaseDirective implements AfterViewInit {
this.cdr.detectChanges();
}
}
-
- this.setComponentTheme();
}
/**
@@ -320,30 +301,11 @@ export class CheckboxBaseDirective implements AfterViewInit {
*/
protected _checked = false;
- /**
- * @hidden
- * @internal
- */
- protected theme: IgxTheme;
-
/**
* @hidden
* @internal
*/
public _required = false;
- private elRef = inject(ElementRef);
- protected destroyRef = inject(DestroyRef);
-
- private setComponentTheme() {
- if (!this.themeToken.preferToken) {
- const theme = getComponentTheme(this.elRef.nativeElement);
-
- if (theme && theme !== this.theme) {
- this.theme = theme;
- this.cdr.markForCheck();
- }
- }
- }
/** @hidden @internal */
@HostListener('keyup', ['$event'])
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 b10d4829a94..0822c2f92a8 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
@@ -22,7 +22,8 @@ describe('IgxInputGroup', () => {
InputGroupFileComponent,
InputGroupDisabledComponent,
InputGroupDisabledByDefaultComponent,
- InputGroupDisabledWithoutValueComponent
+ InputGroupDisabledWithoutValueComponent,
+ InputGroupNestedThemeScopeComponent
]
}).compileComponents();
}));
@@ -239,8 +240,42 @@ describe('IgxInputGroup', () => {
inputGroupDebugElement.triggerEventHandler('click', pointerEvent);
expect(document.activeElement).toEqual(input.nativeElement);
});
+
+ /**
+ * Regression coverage for the `getComponentTheme` fix (was reading the dead `--theme`
+ * custom property; now reads `--ig-theme`, matching what `themes/_scoping.scss` emits).
+ * Verifies an input-group correctly reflects the CSS-scoped `--ig-theme` in effect at
+ * its own DOM position rather than the app-wide/global theme.
+ */
+ it('reflects the CSS-scoped --ig-theme at its own DOM position, not the global theme', fakeAsync(() => {
+ const fixture = TestBed.createComponent(InputGroupNestedThemeScopeComponent);
+ fixture.detectChanges();
+ tick();
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.rootGroup.isTypeIndigo).toBe(false);
+ expect(fixture.componentInstance.indigoGroup.isTypeIndigo).toBe(true);
+ expect(fixture.componentInstance.materialGroup.isTypeIndigo).toBe(false);
+ }));
});
+@Component({
+ template: `
+ `,
+ changeDetection: ChangeDetectionStrategy.Eager,
+ imports: [IgxInputGroupComponent, IgxInputDirective]
+})
+class InputGroupNestedThemeScopeComponent {
+ @ViewChild('rootGroup', { static: true }) public rootGroup: IgxInputGroupComponent;
+ @ViewChild('indigoGroup', { static: true }) public indigoGroup: IgxInputGroupComponent;
+ @ViewChild('materialGroup', { static: true }) public materialGroup: IgxInputGroupComponent;
+}
+
@Component({
template: `
PREFIX