Skip to content

Commit 0f25d65

Browse files
committed
feat(icon): add input for inline styling of icons
1 parent 73bc948 commit 0f25d65

File tree

4 files changed

+73
-28
lines changed

4 files changed

+73
-28
lines changed

src/demo-app/icon/icon-demo.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,8 @@
3232
Custom icon font and CSS:
3333
<mat-icon fontSet="fontawesome" fontIcon="fa-birthday-cake"></mat-icon>
3434
</p>
35+
36+
<p>
37+
Inline styling <mat-icon inline="true">favorite</mat-icon> icons appear as
38+
the same <mat-icon inline="true">directions_car</mat-icon>size as text around them.</p>
3539
</div>

src/lib/icon/icon.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ $mat-icon-size: 24px !default;
1010
fill: currentColor;
1111
height: $mat-icon-size;
1212
width: $mat-icon-size;
13+
14+
&.mat-icon-inline {
15+
font-size: inherit;
16+
height: inherit;
17+
line-height: inherit;
18+
width: inherit;
19+
}
1320
}
1421

1522
.mat-form-field:not(.mat-form-field-appearance-legacy) {

src/lib/icon/icon.spec.ts

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import {inject, async, fakeAsync, tick, TestBed} from '@angular/core/testing';
2-
import {SafeResourceUrl, DomSanitizer} from '@angular/platform-browser';
3-
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
4-
import {Component} from '@angular/core';
5-
import {MatIconModule} from './index';
6-
import {MatIconRegistry, getMatIconNoHttpProviderError} from './icon-registry';
7-
import {FAKE_SVGS} from './fake-svgs';
8-
import {wrappedErrorMessage} from '@angular/cdk/testing';
1+
import { inject, async, fakeAsync, tick, TestBed } from '@angular/core/testing';
2+
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser';
3+
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
4+
import { Component } from '@angular/core';
5+
import { MatIconModule } from './index';
6+
import { MatIconRegistry, getMatIconNoHttpProviderError } from './icon-registry';
7+
import { FAKE_SVGS } from './fake-svgs';
8+
import { wrappedErrorMessage } from '@angular/cdk/testing';
99

1010

1111
/** Returns the CSS classes assigned to an element as a sorted array. */
@@ -50,6 +50,7 @@ describe('MatIcon', () => {
5050
IconFromSvgName,
5151
IconWithAriaHiddenFalse,
5252
IconWithBindingAndNgIf,
53+
InlineIcon,
5354
]
5455
});
5556

@@ -82,14 +83,26 @@ describe('MatIcon', () => {
8283
const fixture = TestBed.createComponent(IconWithLigature);
8384
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
8485
expect(iconElement.getAttribute('aria-hidden'))
85-
.toBe('true', 'Expected the mat-icon element has aria-hidden="true" by default');
86+
.toBe('true', 'Expected the mat-icon element has aria-hidden="true" by default');
8687
});
8788

8889
it('should not override a user-provided aria-hidden attribute', () => {
8990
const fixture = TestBed.createComponent(IconWithAriaHiddenFalse);
9091
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
9192
expect(iconElement.getAttribute('aria-hidden'))
92-
.toBe('false', 'Expected the mat-icon element has the user-provided aria-hidden value');
93+
.toBe('false', 'Expected the mat-icon element has the user-provided aria-hidden value');
94+
});
95+
96+
it('should apply inline styling', () => {
97+
const fixture = TestBed.createComponent(InlineIcon);
98+
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
99+
expect(iconElement.classList.contains('mat-icon-inline'))
100+
.toBeFalsy('Expected the mat-icon element to not include the inline styling class');
101+
102+
fixture.debugElement.componentInstance.inline = true;
103+
fixture.detectChanges();
104+
expect(iconElement.classList.contains('mat-icon-inline'))
105+
.toBeTruthy('Expected the mat-icon element to include the inline styling class');
93106
});
94107

95108
describe('Ligature icons', () => {
@@ -216,8 +229,8 @@ describe('MatIcon', () => {
216229
iconRegistry.addSvgIconSetInNamespace('farm', trust('farm-set-1.svg'));
217230

218231
// Requests for icons must be subscribed to in order for requests to be made.
219-
iconRegistry.getNamedSvgIcon('pig', 'farm').subscribe(() => {});
220-
iconRegistry.getNamedSvgIcon('cow', 'farm').subscribe(() => {});
232+
iconRegistry.getNamedSvgIcon('pig', 'farm').subscribe(() => { });
233+
iconRegistry.getNamedSvgIcon('cow', 'farm').subscribe(() => { });
221234

222235
http.expectOne('farm-set-1.svg').flush(FAKE_SVGS.farmSet1);
223236

@@ -462,7 +475,7 @@ describe('MatIcon without HttpClientModule', () => {
462475
sanitizer = ds;
463476
}));
464477

465-
it('should throw an error when trying to load a remote icon', async() => {
478+
it('should throw an error when trying to load a remote icon', async () => {
466479
const expectedError = wrappedErrorMessage(getMatIconNoHttpProviderError());
467480

468481
expect(() => {
@@ -477,33 +490,38 @@ describe('MatIcon without HttpClientModule', () => {
477490
});
478491

479492

480-
@Component({template: `<mat-icon>{{iconName}}</mat-icon>`})
493+
@Component({ template: `<mat-icon>{{iconName}}</mat-icon>` })
481494
class IconWithLigature {
482495
iconName = '';
483496
}
484497

485-
@Component({template: `<mat-icon [color]="iconColor">{{iconName}}</mat-icon>`})
498+
@Component({ template: `<mat-icon [color]="iconColor">{{iconName}}</mat-icon>` })
486499
class IconWithColor {
487500
iconName = '';
488501
iconColor = 'primary';
489502
}
490503

491-
@Component({template: `<mat-icon [fontSet]="fontSet" [fontIcon]="fontIcon"></mat-icon>`})
504+
@Component({ template: `<mat-icon [fontSet]="fontSet" [fontIcon]="fontIcon"></mat-icon>` })
492505
class IconWithCustomFontCss {
493506
fontSet = '';
494507
fontIcon = '';
495508
}
496509

497-
@Component({template: `<mat-icon [svgIcon]="iconName"></mat-icon>`})
510+
@Component({ template: `<mat-icon [svgIcon]="iconName"></mat-icon>` })
498511
class IconFromSvgName {
499512
iconName: string | undefined = '';
500513
}
501514

502-
@Component({template: '<mat-icon aria-hidden="false">face</mat-icon>'})
515+
@Component({ template: '<mat-icon aria-hidden="false">face</mat-icon>' })
503516
class IconWithAriaHiddenFalse { }
504517

505-
@Component({template: `<mat-icon [svgIcon]="iconName" *ngIf="showIcon">{{iconName}}</mat-icon>`})
518+
@Component({ template: `<mat-icon [svgIcon]="iconName" *ngIf="showIcon">{{iconName}}</mat-icon>` })
506519
class IconWithBindingAndNgIf {
507520
iconName = 'fluffy';
508521
showIcon = true;
509522
}
523+
524+
@Component({ template: `<mat-icon [inline]="inline">{{iconName}}</mat-icon>` })
525+
class InlineIcon {
526+
inline = false;
527+
}

src/lib/icon/icon.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {take} from 'rxjs/operators/take';
9+
import { take } from 'rxjs/operators/take';
1010
import {
1111
Attribute,
1212
ChangeDetectionStrategy,
@@ -18,14 +18,15 @@ import {
1818
SimpleChanges,
1919
ViewEncapsulation,
2020
} from '@angular/core';
21-
import {CanColor, mixinColor} from '@angular/material/core';
22-
import {MatIconRegistry} from './icon-registry';
21+
import { CanColor, mixinColor } from '@angular/material/core';
22+
import { coerceBooleanProperty } from '@angular/cdk/coercion';
23+
import { MatIconRegistry } from './icon-registry';
2324

2425

2526
// Boilerplate for applying mixins to MatIcon.
2627
/** @docs-private */
2728
export class MatIconBase {
28-
constructor(public _elementRef: ElementRef) {}
29+
constructor(public _elementRef: ElementRef) { }
2930
}
3031
export const _MatIconMixinBase = mixinColor(MatIconBase);
3132

@@ -67,13 +68,28 @@ export const _MatIconMixinBase = mixinColor(MatIconBase);
6768
host: {
6869
'role': 'img',
6970
'class': 'mat-icon',
71+
'[class.mat-icon-inline]': 'inline',
7072
},
7173
encapsulation: ViewEncapsulation.None,
7274
preserveWhitespaces: false,
7375
changeDetection: ChangeDetectionStrategy.OnPush,
7476
})
7577
export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, CanColor {
7678

79+
/**
80+
* Whether the icon should be inlined, automatically sizing the icon to match the font size of
81+
* the element the icon is contained in.
82+
*/
83+
@Input()
84+
get inline(): boolean {
85+
return this._inline;
86+
}
87+
set inline(inline: boolean) {
88+
this._inline = coerceBooleanProperty(inline);
89+
}
90+
private _inline: boolean = false;
91+
92+
7793
/** Name of the icon in the SVG icon set. */
7894
@Input() svgIcon: string;
7995

@@ -97,9 +113,9 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
97113
private _previousFontIconClass: string;
98114

99115
constructor(
100-
elementRef: ElementRef,
101-
private _iconRegistry: MatIconRegistry,
102-
@Attribute('aria-hidden') ariaHidden: string) {
116+
elementRef: ElementRef,
117+
private _iconRegistry: MatIconRegistry,
118+
@Attribute('aria-hidden') ariaHidden: string) {
103119
super(elementRef);
104120

105121
// If the user has not explicitly set aria-hidden, mark the icon as hidden, as this is
@@ -189,8 +205,8 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
189205

190206
const elem: HTMLElement = this._elementRef.nativeElement;
191207
const fontSetClass = this.fontSet ?
192-
this._iconRegistry.classNameForFontAlias(this.fontSet) :
193-
this._iconRegistry.getDefaultFontSetClass();
208+
this._iconRegistry.classNameForFontAlias(this.fontSet) :
209+
this._iconRegistry.getDefaultFontSetClass();
194210

195211
if (fontSetClass != this._previousFontSetClass) {
196212
if (this._previousFontSetClass) {

0 commit comments

Comments
 (0)