Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,13 @@
class="igx-checkbox__composite-wrapper"
>
<span #label class="igx-checkbox__composite">
@if (theme === 'indigo') {
<svg class="igx-checkbox__composite-mark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect width="18" height="4" x="2" y="9" rx="1.85"/>
<path d="M19.033 5a1.966 1.966 0 0 0-1.418.586l-8.479 8.577-2.753-2.77a1.971 1.971 0 0 0-2.8 0 1.998 1.998 0 0 0 0 2.822l4.155 4.196a1.955 1.955 0 0 0 2.8 0l9.879-9.99a1.998 1.998 0 0 0 0-2.821 1.966 1.966 0 0 0-1.384-.6Z"/>
</svg>
} @else {
<svg class="igx-checkbox__composite-mark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M4.1,12.7 9,17.6 20.3,6.3"/>
</svg>
}
<svg class="igx-checkbox__composite-mark igx-checkbox__composite-mark--indigo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect width="18" height="4" x="2" y="9" rx="1.85"/>
<path d="M19.033 5a1.966 1.966 0 0 0-1.418.586l-8.479 8.577-2.753-2.77a1.971 1.971 0 0 0-2.8 0 1.998 1.998 0 0 0 0 2.822l4.155 4.196a1.955 1.955 0 0 0 2.8 0l9.879-9.99a1.998 1.998 0 0 0 0-2.821 1.966 1.966 0 0 0-1.384-.6Z"/>
</svg>
<svg class="igx-checkbox__composite-mark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M4.1,12.7 9,17.6 20.3,6.3"/>
</svg>
</span>

<div class="igx-checkbox__ripple"></div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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();
Expand Down Expand Up @@ -412,6 +446,28 @@ describe('IgxCheckbox', () => {
});
});

@Component({
template: `<igx-checkbox #rootCb>Root</igx-checkbox>
<div #indigoWrapper style="--ig-theme: indigo">
<igx-checkbox #indigoCb>Indigo scope</igx-checkbox>
<div #materialWrapper style="--ig-theme: material">
<igx-checkbox #materialCb>Material scope nested in indigo</igx-checkbox>
</div>
</div>`,
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: `<igx-checkbox #cb>Init</igx-checkbox>`,
changeDetection: ChangeDetectionStrategy.Eager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +29,9 @@
}

%igx-ripple-wrapper {
--ig-theme: inherit;
--ig-theme-variant: inherit;

overflow: hidden;
}
}
Expand Down
11 changes: 10 additions & 1 deletion projects/igniteui-angular/core/src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading
Loading