diff --git a/src/components-examples/material/icon/icon-harness/icon-harness-example.html b/src/components-examples/material/icon/icon-harness/icon-harness-example.html index 5769529ec1e6..57af4c464de2 100644 --- a/src/components-examples/material/icon/icon-harness/icon-harness-example.html +++ b/src/components-examples/material/icon/icon-harness/icon-harness-example.html @@ -1,3 +1,4 @@ ligature_icon + diff --git a/src/components-examples/material/icon/icon-harness/icon-harness-example.spec.ts b/src/components-examples/material/icon/icon-harness/icon-harness-example.spec.ts index 4b6f1c9d3dd1..c4cd4674cb7a 100644 --- a/src/components-examples/material/icon/icon-harness/icon-harness-example.spec.ts +++ b/src/components-examples/material/icon/icon-harness/icon-harness-example.spec.ts @@ -32,24 +32,24 @@ describe('IconHarnessExample', () => { it('should load all icon harnesses', async () => { const icons = await loader.getAllHarnesses(MatIconHarness); - expect(icons.length).toBe(3); + expect(icons.length).toBe(4); }); it('should get the name of an icon', async () => { const icons = await loader.getAllHarnesses(MatIconHarness); const names = await parallel(() => icons.map(icon => icon.getName())); - expect(names).toEqual(['fontIcon', 'svgIcon', 'ligature_icon']); + expect(names).toEqual(['fontIcon', 'svgIcon', 'ligature_icon', 'ligature_icon_by_attribute']); }); it('should get the namespace of an icon', async () => { const icons = await loader.getAllHarnesses(MatIconHarness); const namespaces = await parallel(() => icons.map(icon => icon.getNamespace())); - expect(namespaces).toEqual(['fontIcons', 'svgIcons', null]); + expect(namespaces).toEqual(['fontIcons', 'svgIcons', null, null]); }); it('should get whether an icon is inline', async () => { const icons = await loader.getAllHarnesses(MatIconHarness); const inlineStates = await parallel(() => icons.map(icon => icon.isInline())); - expect(inlineStates).toEqual([false, false, true]); + expect(inlineStates).toEqual([false, false, true, true]); }); }); diff --git a/src/components-examples/material/icon/icon-overview/icon-overview-example.html b/src/components-examples/material/icon/icon-overview/icon-overview-example.html index ac069beed3fd..3dd06c0c3017 100644 --- a/src/components-examples/material/icon/icon-overview/icon-overview-example.html +++ b/src/components-examples/material/icon/icon-overview/icon-overview-example.html @@ -1 +1 @@ -home + diff --git a/src/dev-app/icon/icon-demo.html b/src/dev-app/icon/icon-demo.html index aff0f54360d7..e8cfdd913a13 100644 --- a/src/dev-app/icon/icon-demo.html +++ b/src/dev-app/icon/icon-demo.html @@ -38,7 +38,12 @@

- Ligature from Material Icons font: + Ligature from Material Icons font by attribute: + +

+ +

+ Ligature from Material Icons font by content: home

diff --git a/src/material/icon/icon.scss b/src/material/icon/icon.scss index 66a37a418388..da684252489b 100644 --- a/src/material/icon/icon.scss +++ b/src/material/icon/icon.scss @@ -21,6 +21,10 @@ $size: 24px !default; line-height: inherit; width: inherit; } + + &[icon]::before { + content: attr(icon); + } } // Icons that will be mirrored in RTL. diff --git a/src/material/icon/icon.spec.ts b/src/material/icon/icon.spec.ts index 541644d464b8..c012106799bf 100644 --- a/src/material/icon/icon.spec.ts +++ b/src/material/icon/icon.spec.ts @@ -1,5 +1,5 @@ -import {inject, waitForAsync, fakeAsync, tick, TestBed} from '@angular/core/testing'; -import {SafeResourceUrl, DomSanitizer, SafeHtml} from '@angular/platform-browser'; +import {fakeAsync, inject, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import {DomSanitizer, SafeHtml, SafeResourceUrl} from '@angular/platform-browser'; import { HttpClientTestingModule, HttpTestingController, @@ -7,7 +7,7 @@ import { } from '@angular/common/http/testing'; 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 {getMatIconNoHttpProviderError, MatIconRegistry} from './icon-registry'; import {FAKE_SVGS} from './fake-svgs'; import {wrappedErrorMessage} from '../../cdk/testing/private'; import {MatIcon} from './icon'; @@ -58,40 +58,43 @@ describe('MatIcon', () => { let fakePath: string; let errorHandler: jasmine.SpyObj; - beforeEach(waitForAsync(() => { - // The $ prefix tells Karma not to try to process the - // request so that we don't get warnings in our logs. - fakePath = '/$fake-path'; - errorHandler = jasmine.createSpyObj('errorHandler', ['handleError']); - - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, MatIconModule], - declarations: [ - IconWithColor, - IconWithLigature, - IconWithCustomFontCss, - IconFromSvgName, - IconWithAriaHiddenFalse, - IconWithBindingAndNgIf, - InlineIcon, - SvgIconWithUserContent, - IconWithLigatureAndSvgBinding, - BlankIcon, - ], - providers: [ - { - provide: MAT_ICON_LOCATION, - useValue: {getPathname: () => fakePath}, - }, - { - provide: ErrorHandler, - useValue: errorHandler, - }, - ], - }); + beforeEach( + waitForAsync(() => { + // The $ prefix tells Karma not to try to process the + // request so that we don't get warnings in our logs. + fakePath = '/$fake-path'; + errorHandler = jasmine.createSpyObj('errorHandler', ['handleError']); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, MatIconModule], + declarations: [ + IconWithColor, + IconWithLigature, + IconWithLigatureByAttribute, + IconWithCustomFontCss, + IconFromSvgName, + IconWithAriaHiddenFalse, + IconWithBindingAndNgIf, + InlineIcon, + SvgIconWithUserContent, + IconWithLigatureAndSvgBinding, + BlankIcon, + ], + providers: [ + { + provide: MAT_ICON_LOCATION, + useValue: {getPathname: () => fakePath}, + }, + { + provide: ErrorHandler, + useValue: errorHandler, + }, + ], + }); - TestBed.compileComponents(); - })); + TestBed.compileComponents(); + }), + ); let iconRegistry: MatIconRegistry; let http: HttpTestingController; @@ -241,6 +244,58 @@ describe('MatIcon', () => { }); }); + describe('Ligature icons by attribute', () => { + it('should add material-icons class by default', () => { + const fixture = TestBed.createComponent(IconWithLigatureByAttribute); + + const testComponent = fixture.componentInstance; + const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon'); + testComponent.iconName = 'home'; + fixture.detectChanges(); + expect(sortedClassNames(matIconElement)).toEqual([ + 'mat-icon', + 'mat-icon-no-color', + 'material-icons', + 'notranslate', + ]); + }); + + it('should use alternate icon font if set', () => { + iconRegistry.setDefaultFontSetClass('myfont'); + + const fixture = TestBed.createComponent(IconWithLigatureByAttribute); + + const testComponent = fixture.componentInstance; + const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon'); + testComponent.iconName = 'home'; + fixture.detectChanges(); + expect(sortedClassNames(matIconElement)).toEqual([ + 'mat-icon', + 'mat-icon-no-color', + 'myfont', + 'notranslate', + ]); + }); + + it('should be able to provide multiple alternate icon set classes', () => { + iconRegistry.setDefaultFontSetClass('myfont', 'myfont-48x48'); + + let fixture = TestBed.createComponent(IconWithLigatureByAttribute); + + const testComponent = fixture.componentInstance; + const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon'); + testComponent.iconName = 'home'; + fixture.detectChanges(); + expect(sortedClassNames(matIconElement)).toEqual([ + 'mat-icon', + 'mat-icon-no-color', + 'myfont', + 'myfont-48x48', + 'notranslate', + ]); + }); + }); + describe('Icons from URLs', () => { it('should register icon URLs by name', fakeAsync(() => { iconRegistry.addSvgIcon('fluffy', trustUrl('cat.svg')); @@ -1213,14 +1268,16 @@ describe('MatIcon without HttpClientModule', () => { let iconRegistry: MatIconRegistry; let sanitizer: DomSanitizer; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [MatIconModule], - declarations: [IconFromSvgName], - }); + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [MatIconModule], + declarations: [IconFromSvgName], + }); - TestBed.compileComponents(); - })); + TestBed.compileComponents(); + }), + ); beforeEach(inject([MatIconRegistry, DomSanitizer], (mir: MatIconRegistry, ds: DomSanitizer) => { iconRegistry = mir; @@ -1311,6 +1368,11 @@ class IconWithLigature { iconName = ''; } +@Component({template: ``}) +class IconWithLigatureByAttribute { + iconName = ''; +} + @Component({template: `{{iconName}}`}) class IconWithColor { iconName = ''; diff --git a/src/material/icon/icon.ts b/src/material/icon/icon.ts index a7f6a1d8d733..b892fdbb86ad 100644 --- a/src/material/icon/icon.ts +++ b/src/material/icon/icon.ts @@ -24,7 +24,7 @@ import { Optional, ViewEncapsulation, } from '@angular/core'; -import {CanColor, ThemePalette, mixinColor} from '@angular/material/core'; +import {CanColor, mixinColor, ThemePalette} from '@angular/material/core'; import {Subscription} from 'rxjs'; import {take} from 'rxjs/operators'; @@ -114,13 +114,16 @@ const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/; * ` * ` * - * - Use a font ligature as an icon by putting the ligature text in the content of the `` - * component. By default the Material icons font is used as described at + * - Use a font ligature as an icon by putting the ligature text in the `icon` attribute or the + * content of the `` component. It is recommended to use the attribute alternative + * to prevent the ligature text to be selectable and to appear in search engine results. + * By default the Material icons font is used as described at * http://google.github.io/material-design-icons/#icon-font-for-the-web. You can specify an * alternate font by setting the fontSet input to either the CSS class to apply to use the * desired font, or to an alias previously registered with MatIconRegistry.registerFontClassAlias. - * Examples: - * `home + * ` + * + * home * sun` * * - Specify a font glyph to be included via CSS rules by setting the fontSet input to specify the @@ -140,7 +143,7 @@ const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/; 'role': 'img', 'class': 'mat-icon notranslate', '[attr.data-mat-icon-type]': '_usingFontIcon() ? "font" : "svg"', - '[attr.data-mat-icon-name]': '_svgName || fontIcon', + '[attr.data-mat-icon-name]': '_svgName || fontIcon || icon', '[attr.data-mat-icon-namespace]': '_svgNamespace || fontSet', '[class.mat-icon-inline]': 'inline', '[class.mat-icon-no-color]': 'color !== "primary" && color !== "accent" && color !== "warn"', @@ -162,6 +165,13 @@ export class MatIcon extends _MatIconBase implements OnInit, AfterViewChecked, C } private _inline: boolean = false; + /** + * Name of an icon within a font set that use ligatures, such as the + * [Material icons font](http://google.github.io/material-design-icons/#icon-font-for-the-web). + */ + @Input() + icon: string; + /** Name of the icon in the SVG icon set. */ @Input() get svgIcon(): string { @@ -194,7 +204,10 @@ export class MatIcon extends _MatIconBase implements OnInit, AfterViewChecked, C } private _fontSet: string; - /** Name of an icon within a font set. */ + /** + * Name of an icon within a font set that use CSS class for each icon glyph, such as + * [FontAwesome](https://fortawesome.github.io/Font-Awesome/examples/). + */ @Input() get fontIcon(): string { return this._fontIcon; diff --git a/src/material/icon/testing/shared.spec.ts b/src/material/icon/testing/shared.spec.ts index 36c6c5ee9874..1a302fe04d61 100644 --- a/src/material/icon/testing/shared.spec.ts +++ b/src/material/icon/testing/shared.spec.ts @@ -37,7 +37,7 @@ export function runHarnessTests( it('should load all icon harnesses', async () => { const icons = await loader.getAllHarnesses(iconHarness); - expect(icons.length).toBe(3); + expect(icons.length).toBe(4); }); it('should filter icon harnesses based on their type', async () => { @@ -47,7 +47,7 @@ export function runHarnessTests( ]); expect(svgIcons.length).toBe(1); - expect(fontIcons.length).toBe(2); + expect(fontIcons.length).toBe(3); }); it('should filter icon harnesses based on their name', async () => { @@ -69,31 +69,31 @@ export function runHarnessTests( expect(regexFilterResults.length).toBe(1); expect(stringFilterResults.length).toBe(1); - expect(nullFilterResults.length).toBe(1); + expect(nullFilterResults.length).toBe(2); }); it('should get the type of each icon', async () => { const icons = await loader.getAllHarnesses(iconHarness); const types = await parallel(() => icons.map(icon => icon.getType())); - expect(types).toEqual([IconType.FONT, IconType.SVG, IconType.FONT]); + expect(types).toEqual([IconType.FONT, IconType.SVG, IconType.FONT, IconType.FONT]); }); it('should get the name of an icon', async () => { const icons = await loader.getAllHarnesses(iconHarness); const names = await parallel(() => icons.map(icon => icon.getName())); - expect(names).toEqual(['fontIcon', 'svgIcon', 'ligature_icon']); + expect(names).toEqual(['fontIcon', 'svgIcon', 'ligature_icon', 'ligature_icon_by_attribute']); }); it('should get the namespace of an icon', async () => { const icons = await loader.getAllHarnesses(iconHarness); const namespaces = await parallel(() => icons.map(icon => icon.getNamespace())); - expect(namespaces).toEqual(['fontIcons', 'svgIcons', null]); + expect(namespaces).toEqual(['fontIcons', 'svgIcons', null, null]); }); it('should get whether an icon is inline', async () => { const icons = await loader.getAllHarnesses(iconHarness); const inlineStates = await parallel(() => icons.map(icon => icon.isInline())); - expect(inlineStates).toEqual([false, false, true]); + expect(inlineStates).toEqual([false, false, true, true]); }); } @@ -102,6 +102,7 @@ export function runHarnessTests( ligature_icon + `, }) class IconHarnessTest {} diff --git a/tools/public_api_guard/material/icon.md b/tools/public_api_guard/material/icon.md index 2387653cabe6..0ce5ed6fa162 100644 --- a/tools/public_api_guard/material/icon.md +++ b/tools/public_api_guard/material/icon.md @@ -71,6 +71,7 @@ export class MatIcon extends _MatIconBase implements OnInit, AfterViewChecked, C set fontIcon(value: string); get fontSet(): string; set fontSet(value: string); + icon: string; get inline(): boolean; set inline(inline: BooleanInput); // (undocumented) @@ -88,7 +89,7 @@ export class MatIcon extends _MatIconBase implements OnInit, AfterViewChecked, C // (undocumented) _usingFontIcon(): boolean; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; }