diff --git a/src/material/icon/icon.spec.ts b/src/material/icon/icon.spec.ts index 2f6f041bdd36..2b03d343f3d7 100644 --- a/src/material/icon/icon.spec.ts +++ b/src/material/icon/icon.spec.ts @@ -5,8 +5,8 @@ import { HttpTestingController, TestRequest, } from '@angular/common/http/testing'; -import {Component, ErrorHandler, ViewChild} from '@angular/core'; -import {MatIconModule, MAT_ICON_LOCATION} from './index'; +import {Component, ErrorHandler, Provider, Type, ViewChild} from '@angular/core'; +import {MAT_ICON_DEFAULT_OPTIONS, MAT_ICON_LOCATION, MatIconModule} from './index'; import {MatIconRegistry, getMatIconNoHttpProviderError} from './icon-registry'; import {FAKE_SVGS} from './fake-svgs'; import {wrappedErrorMessage} from '../../cdk/testing/private'; @@ -41,6 +41,19 @@ function verifyPathChildElement(element: Element, attributeValue: string): void expect(pathElement.getAttribute('name')).toBe(attributeValue); } +/** Creates a test component fixture. */ +function createComponent(component: Type, providers: Provider[] = []) { + TestBed.configureTestingModule({ + imports: [MatIconModule], + declarations: [component], + providers: [...providers], + }); + + TestBed.compileComponents(); + + return TestBed.createComponent(component); +} + describe('MatIcon', () => { let fakePath: string; let errorHandler: jasmine.SpyObj; @@ -1237,6 +1250,71 @@ describe('MatIcon without HttpClientModule', () => { }); }); +describe('MatIcon with default options', () => { + it('should be able to configure color globally', fakeAsync(() => { + const fixture = createComponent(IconWithLigature, [ + {provide: MAT_ICON_DEFAULT_OPTIONS, useValue: {color: 'accent'}}, + ]); + const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon'); + fixture.detectChanges(); + expect(iconElement.classList).not.toContain('mat-icon-no-color'); + expect(iconElement.classList).toContain('mat-accent'); + })); + + it('should use passed color rather then color provided', fakeAsync(() => { + const fixture = createComponent(IconWithColor, [ + {provide: MAT_ICON_DEFAULT_OPTIONS, useValue: {color: 'warn'}}, + ]); + const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon'); + fixture.detectChanges(); + expect(iconElement.classList).not.toContain('mat-warn'); + expect(iconElement.classList).toContain('mat-primary'); + })); + + it('should use default color if no color passed', fakeAsync(() => { + const fixture = createComponent(IconWithColor, [ + {provide: MAT_ICON_DEFAULT_OPTIONS, useValue: {color: 'accent'}}, + ]); + const component = fixture.componentInstance; + const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon'); + component.iconColor = ''; + fixture.detectChanges(); + expect(iconElement.classList).not.toContain('mat-icon-no-color'); + expect(iconElement.classList).not.toContain('mat-primary'); + expect(iconElement.classList).toContain('mat-accent'); + })); + + it('should be able to configure font set globally', fakeAsync(() => { + const fixture = createComponent(IconWithLigature, [ + {provide: MAT_ICON_DEFAULT_OPTIONS, useValue: {fontSet: 'custom-font-set'}}, + ]); + const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon'); + fixture.detectChanges(); + expect(iconElement.classList).toContain('custom-font-set'); + })); + + it('should use passed fontSet rather then default one', fakeAsync(() => { + const fixture = createComponent(IconWithCustomFontCss, [ + {provide: MAT_ICON_DEFAULT_OPTIONS, useValue: {fontSet: 'default-font-set'}}, + ]); + const component = fixture.componentInstance; + const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon'); + component.fontSet = 'custom-font-set'; + fixture.detectChanges(); + expect(iconElement.classList).not.toContain('default-font-set'); + expect(iconElement.classList).toContain('custom-font-set'); + })); + + it('should use passed empty fontSet rather then default one', fakeAsync(() => { + const fixture = createComponent(IconWithCustomFontCss, [ + {provide: MAT_ICON_DEFAULT_OPTIONS, useValue: {fontSet: 'default-font-set'}}, + ]); + const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon'); + fixture.detectChanges(); + expect(iconElement.classList).not.toContain('default-font-set'); + })); +}); + @Component({template: `{{iconName}}`}) class IconWithLigature { iconName = ''; diff --git a/src/material/icon/icon.ts b/src/material/icon/icon.ts index 7fd9f00cdecc..af99f5f379b8 100644 --- a/src/material/icon/icon.ts +++ b/src/material/icon/icon.ts @@ -21,9 +21,10 @@ import { Input, OnDestroy, OnInit, + Optional, ViewEncapsulation, } from '@angular/core'; -import {CanColor, mixinColor} from '@angular/material/core'; +import {CanColor, ThemePalette, mixinColor} from '@angular/material/core'; import {Subscription} from 'rxjs'; import {take} from 'rxjs/operators'; @@ -37,6 +38,19 @@ const _MatIconBase = mixinColor( }, ); +/** Default options for `mat-icon`. */ +export interface MatIconDefaultOptions { + /** Default color of the icon. */ + color?: ThemePalette; + /** Font set that the icon is a part of. */ + fontSet?: string; +} + +/** Injection token to be used to override the default options for `mat-icon`. */ +export const MAT_ICON_DEFAULT_OPTIONS = new InjectionToken( + 'MAT_ICON_DEFAULT_OPTIONS', +); + /** * Injection token used to provide the current location to `MatIcon`. * Used to handle server-side rendering and to stub out during unit tests. @@ -216,9 +230,22 @@ export class MatIcon extends _MatIconBase implements OnInit, AfterViewChecked, C @Attribute('aria-hidden') ariaHidden: string, @Inject(MAT_ICON_LOCATION) private _location: MatIconLocation, private readonly _errorHandler: ErrorHandler, + @Optional() + @Inject(MAT_ICON_DEFAULT_OPTIONS) + defaults?: MatIconDefaultOptions, ) { super(elementRef); + if (defaults) { + if (defaults.color) { + this.color = this.defaultColor = defaults.color; + } + + if (defaults.fontSet) { + this.fontSet = defaults.fontSet; + } + } + // If the user has not explicitly set aria-hidden, mark the icon as hidden, as this is // the right thing to do for the majority of icon use-cases. if (!ariaHidden) { diff --git a/tools/public_api_guard/material/icon.md b/tools/public_api_guard/material/icon.md index d38d4d2603a1..b25bfd90a7bf 100644 --- a/tools/public_api_guard/material/icon.md +++ b/tools/public_api_guard/material/icon.md @@ -22,6 +22,7 @@ import { OnInit } from '@angular/core'; import { Optional } from '@angular/core'; import { SafeHtml } from '@angular/platform-browser'; import { SafeResourceUrl } from '@angular/platform-browser'; +import { ThemePalette } from '@angular/material/core'; // @public export function getMatIconFailedToSanitizeLiteralError(literal: SafeHtml): Error; @@ -54,6 +55,9 @@ export interface IconOptions { // @public export type IconResolver = (name: string, namespace: string) => SafeResourceUrl | SafeResourceUrlWithIconOptions | null; +// @public +export const MAT_ICON_DEFAULT_OPTIONS: InjectionToken; + // @public export const MAT_ICON_LOCATION: InjectionToken; @@ -62,7 +66,7 @@ export function MAT_ICON_LOCATION_FACTORY(): MatIconLocation; // @public export class MatIcon extends _MatIconBase implements OnInit, AfterViewChecked, CanColor, OnDestroy { - constructor(elementRef: ElementRef, _iconRegistry: MatIconRegistry, ariaHidden: string, _location: MatIconLocation, _errorHandler: ErrorHandler); + constructor(elementRef: ElementRef, _iconRegistry: MatIconRegistry, ariaHidden: string, _location: MatIconLocation, _errorHandler: ErrorHandler, defaults?: MatIconDefaultOptions); get fontIcon(): string; set fontIcon(value: string); get fontSet(): string; @@ -86,7 +90,13 @@ export class MatIcon extends _MatIconBase implements OnInit, AfterViewChecked, C // (undocumented) static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: i0.ɵɵFactoryDeclaration; +} + +// @public +export interface MatIconDefaultOptions { + color?: ThemePalette; + fontSet?: string; } // @public