Skip to content
Open
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
38 changes: 36 additions & 2 deletions src/material/button/_m2-icon-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
$system: m2-utils.get-system($theme);
$density-scale: theming.clamp-density(map.get($system, density-scale), -3);
$touch-target-display: block;
$disabled: m3-utils.color-with-opacity(map.get($system, on-surface), 38%);
$disabled-container: m3-utils.color-with-opacity(map.get($system, on-surface), 12%);

@if ($density-scale < -1) {
$touch-target-display: none;
Expand All @@ -19,8 +21,30 @@
icon-button-touch-target-size: 48px,
),
color: (
icon-button-disabled-icon-color:
m3-utils.color-with-opacity(map.get($system, on-surface), 38%),
icon-button-container-color: transparent,
icon-button-disabled-icon-color: $disabled,
icon-button-filled-container-color: map.get($system, surface),
icon-button-filled-disabled-container-color: $disabled-container,
icon-button-filled-disabled-icon-color: $disabled,
icon-button-filled-disabled-state-layer-color: map.get($system, on-surface-variant),
icon-button-filled-focus-state-layer-opacity: map.get($system, focus-state-layer-opacity),
icon-button-filled-hover-state-layer-opacity: map.get($system, hover-state-layer-opacity),
icon-button-filled-icon-color: map.get($system, on-surface),
icon-button-filled-pressed-state-layer-opacity: map.get($system, pressed-state-layer-opacity),
icon-button-filled-ripple-color: m3-utils.color-with-opacity(
map.get($system, on-surface), map.get($system, pressed-state-layer-opacity)),
icon-button-filled-state-layer-color: map.get($system, on-surface),
icon-button-tonal-container-color: map.get($system, surface),
icon-button-tonal-disabled-container-color: $disabled-container,
icon-button-tonal-disabled-icon-color: $disabled,
icon-button-tonal-disabled-state-layer-color: map.get($system, on-surface-variant),
icon-button-tonal-focus-state-layer-opacity: map.get($system, focus-state-layer-opacity),
icon-button-tonal-hover-state-layer-opacity: map.get($system, hover-state-layer-opacity),
icon-button-tonal-icon-color: map.get($system, on-surface),
icon-button-tonal-pressed-state-layer-opacity: map.get($system, pressed-state-layer-opacity),
icon-button-tonal-ripple-color: m3-utils.color-with-opacity(
map.get($system, on-surface), map.get($system, pressed-state-layer-opacity)),
icon-button-tonal-state-layer-color: map.get($system, on-surface),
icon-button-disabled-state-layer-color: map.get($system, on-surface-variant),
icon-button-focus-state-layer-opacity: map.get($system, focus-state-layer-opacity),
icon-button-hover-state-layer-opacity: map.get($system, hover-state-layer-opacity),
Expand Down Expand Up @@ -51,6 +75,16 @@
$system: m3-utils.replace-colors-with-variant($system, primary, $color-variant);

@return (
icon-button-filled-container-color: map.get($system, primary),
icon-button-filled-icon-color: map.get($system, on-primary),
icon-button-filled-ripple-color: m3-utils.color-with-opacity(
map.get($system, on-primary), map.get($system, pressed-state-layer-opacity)),
icon-button-filled-state-layer-color: map.get($system, on-primary),
icon-button-tonal-container-color: map.get($system, primary),
icon-button-tonal-icon-color: map.get($system, on-primary),
icon-button-tonal-ripple-color: m3-utils.color-with-opacity(
map.get($system, on-primary), map.get($system, pressed-state-layer-opacity)),
icon-button-tonal-state-layer-color: map.get($system, on-primary),
icon-button-icon-color: map.get($system, primary),
icon-button-state-layer-color: map.get($system, primary),
icon-button-ripple-color: m3-utils.color-with-opacity(
Expand Down
27 changes: 27 additions & 0 deletions src/material/button/_m3-icon-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,35 @@
icon-button-touch-target-size: 48px,
),
color: (
icon-button-container-color: transparent,
icon-button-disabled-icon-color:
m3-utils.color-with-opacity(map.get($system, on-surface), 38%),
icon-button-filled-container-color: map.get($system, primary),
icon-button-filled-disabled-container-color:
m3-utils.color-with-opacity(map.get($system, on-surface), 12%),
icon-button-filled-disabled-icon-color:
m3-utils.color-with-opacity(map.get($system, on-surface), 38%),
icon-button-filled-disabled-state-layer-color: map.get($system, on-surface-variant),
icon-button-filled-focus-state-layer-opacity: map.get($system, focus-state-layer-opacity),
icon-button-filled-hover-state-layer-opacity: map.get($system, hover-state-layer-opacity),
icon-button-filled-icon-color: map.get($system, on-primary),
icon-button-filled-pressed-state-layer-opacity: map.get($system, pressed-state-layer-opacity),
icon-button-filled-ripple-color: m3-utils.color-with-opacity(
map.get($system, on-primary), map.get($system, pressed-state-layer-opacity)),
icon-button-filled-state-layer-color: map.get($system, on-primary),
icon-button-tonal-container-color: map.get($system, secondary-container),
icon-button-tonal-disabled-container-color:
m3-utils.color-with-opacity(map.get($system, on-surface), 12%),
icon-button-tonal-disabled-icon-color:
m3-utils.color-with-opacity(map.get($system, on-surface), 38%),
icon-button-tonal-disabled-state-layer-color: map.get($system, on-surface-variant),
icon-button-tonal-focus-state-layer-opacity: map.get($system, focus-state-layer-opacity),
icon-button-tonal-hover-state-layer-opacity: map.get($system, hover-state-layer-opacity),
icon-button-tonal-icon-color: map.get($system, on-secondary-container),
icon-button-tonal-pressed-state-layer-opacity: map.get($system, pressed-state-layer-opacity),
icon-button-tonal-ripple-color: m3-utils.color-with-opacity(
map.get($system, on-secondary-container), map.get($system, pressed-state-layer-opacity)),
icon-button-tonal-state-layer-color: map.get($system, on-secondary-container),
icon-button-disabled-state-layer-color: map.get($system, on-surface-variant),
icon-button-focus-state-layer-opacity: map.get($system, focus-state-layer-opacity),
icon-button-hover-state-layer-opacity: map.get($system, hover-state-layer-opacity),
Expand Down
3 changes: 3 additions & 0 deletions src/material/button/button.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ attribute, for example `matButton="outlined"`:
| `outlined` | Medium-emphasis buttons often used for actions that need attention but aren't the primary action. |
| `elevated` | Medium-emphasis buttons often used when a button requires visual separation from a patterned background. |

The `matIconButton` supports filled and tonal appearances, for example
`matIconButton="filled"` and `matIconButton="tonal"`.


### Extended FAB buttons
Traditional floating action buttons (FAB) buttons are circular and only have space for a single
Expand Down
60 changes: 60 additions & 0 deletions src/material/button/button.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MatButtonConfig,
MatButtonModule,
MatFabDefaultOptions,
MatIconButtonAppearance,
} from './index';

describe('MatButton', () => {
Expand Down Expand Up @@ -119,6 +120,49 @@ describe('MatButton', () => {
expect(button.classList).toContain('mat-mdc-outlined-button');
});

it('should apply the icon button appearance classes', () => {
const fixture = TestBed.createComponent(TestApp);
fixture.detectChanges();

const defaultIconButton = fixture.nativeElement.querySelector('.default-icon-button');
const filledIconButton = fixture.nativeElement.querySelector('.filled-icon-button');
const legacyIconButton = fixture.nativeElement.querySelector('.legacy-icon-button');
const tonalIconAnchor = fixture.nativeElement.querySelector('.tonal-icon-anchor');

expect(defaultIconButton.classList).not.toContain('mat-mdc-icon-button-filled');
expect(defaultIconButton.classList).not.toContain('mat-mdc-icon-button-tonal');
expect(filledIconButton.classList).toContain('mat-mdc-icon-button-filled');
expect(legacyIconButton.classList).toContain('mat-mdc-icon-button-filled');
expect(tonalIconAnchor.classList).toContain('mat-mdc-icon-button-tonal');
});

it('should be able to change the icon button appearance dynamically', () => {
const fixture = TestBed.createComponent(TestApp);
const iconButton = fixture.nativeElement.querySelector('.dynamic-icon-button') as HTMLElement;
fixture.detectChanges();

expect(iconButton.classList).not.toContain('mat-mdc-icon-button-filled');
expect(iconButton.classList).not.toContain('mat-mdc-icon-button-tonal');

fixture.componentInstance.iconButtonAppearance = 'filled';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(iconButton.classList).toContain('mat-mdc-icon-button-filled');
expect(iconButton.classList).not.toContain('mat-mdc-icon-button-tonal');

fixture.componentInstance.iconButtonAppearance = 'tonal';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(iconButton.classList).not.toContain('mat-mdc-icon-button-filled');
expect(iconButton.classList).toContain('mat-mdc-icon-button-tonal');

fixture.componentInstance.iconButtonAppearance = '';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(iconButton.classList).not.toContain('mat-mdc-icon-button-filled');
expect(iconButton.classList).not.toContain('mat-mdc-icon-button-tonal');
});

describe('button[mat-fab]', () => {
it('should have accent palette by default', () => {
const fixture = TestBed.createComponent(TestApp);
Expand Down Expand Up @@ -540,6 +584,21 @@ describe('MatFabDefaultOptions', () => {
</button>
<button class="dynamic" [matButton]="appearance">Dynamic button</button>
<button class="default-appearance" matButton>Dynamic button</button>
<button class="default-icon-button" matIconButton>
Default icon button
</button>
<button class="filled-icon-button" matIconButton="filled">
Filled icon button
</button>
<button class="legacy-icon-button" mat-icon-button="filled">
Legacy icon button
</button>
<a class="tonal-icon-anchor" href="https://www.google.com" matIconButton="tonal">
Tonal icon anchor
</a>
<button class="dynamic-icon-button" [matIconButton]="iconButtonAppearance">
Dynamic icon button
</button>
`,
imports: [MatButtonModule],
changeDetection: ChangeDetectionStrategy.Eager,
Expand All @@ -553,6 +612,7 @@ class TestApp {
extended = false;
disabledInteractive = false;
appearance: MatButtonAppearance = 'text';
iconButtonAppearance: MatIconButtonAppearance | '' = '';
showProgress = false;

increment() {
Expand Down
36 changes: 34 additions & 2 deletions src/material/button/icon-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ $fallbacks: m3-icon-button.get-tokens();
box-sizing: border-box;
border: none;
outline: none;
background-color: transparent;
background-color: token-utils.slot(
icon-button-container-color, $fallbacks, $fallback: transparent);
fill: currentColor;
text-decoration: none;
cursor: pointer;
Expand Down Expand Up @@ -59,7 +60,38 @@ $fallbacks: m3-icon-button.get-tokens();
@include button-base.mat-private-button-disabled {
color: token-utils.slot(icon-button-disabled-icon-color, $fallbacks);
}
;

&.mat-mdc-icon-button-filled {
background-color: token-utils.slot(icon-button-filled-container-color, $fallbacks);
color: token-utils.slot(icon-button-filled-icon-color, $fallbacks);

@include button-base.mat-private-button-ripple(
icon-button-filled-ripple-color, icon-button-filled-state-layer-color,
icon-button-filled-disabled-state-layer-color,
icon-button-filled-hover-state-layer-opacity, icon-button-filled-focus-state-layer-opacity,
icon-button-filled-pressed-state-layer-opacity, $fallbacks);

@include button-base.mat-private-button-disabled {
background-color: token-utils.slot(icon-button-filled-disabled-container-color, $fallbacks);
color: token-utils.slot(icon-button-filled-disabled-icon-color, $fallbacks);
}
}

&.mat-mdc-icon-button-tonal {
background-color: token-utils.slot(icon-button-tonal-container-color, $fallbacks);
color: token-utils.slot(icon-button-tonal-icon-color, $fallbacks);

@include button-base.mat-private-button-ripple(
icon-button-tonal-ripple-color, icon-button-tonal-state-layer-color,
icon-button-tonal-disabled-state-layer-color,
icon-button-tonal-hover-state-layer-opacity, icon-button-tonal-focus-state-layer-opacity,
icon-button-tonal-pressed-state-layer-opacity, $fallbacks);

@include button-base.mat-private-button-disabled {
background-color: token-utils.slot(icon-button-tonal-disabled-container-color, $fallbacks);
color: token-utils.slot(icon-button-tonal-disabled-icon-color, $fallbacks);
}
}

img,
svg {
Expand Down
52 changes: 51 additions & 1 deletion src/material/button/icon-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {Component, ViewEncapsulation} from '@angular/core';
import {Component, Input, ViewEncapsulation} from '@angular/core';
import {MatButtonBase} from './button-base';

/** Possible appearances for a `MatIconButton`. */
export type MatIconButtonAppearance = 'filled' | 'tonal';

/** Classes that need to be set for each appearance of the icon button. */
const APPEARANCE_CLASSES: Map<MatIconButtonAppearance, readonly string[]> = new Map([
['filled', ['mat-mdc-icon-button-filled']],
['tonal', ['mat-mdc-icon-button-tonal']],
]);

/**
* Material Design icon button component. This type of button displays a single interactive icon for
* users to perform an action.
Expand All @@ -25,10 +34,51 @@ import {MatButtonBase} from './button-base';
encapsulation: ViewEncapsulation.None,
})
export class MatIconButton extends MatButtonBase {
/** Appearance of the icon button. */
@Input('matIconButton')
get appearance(): MatIconButtonAppearance | null {
return this._appearance;
}
set appearance(value: MatIconButtonAppearance | '') {
this.setAppearance(value || null);
}
private _appearance: MatIconButtonAppearance | null = null;

/** Same as `appearance`, but using the legacy `mat-icon-button` attribute selector. */
@Input('mat-icon-button')
set _legacyAppearance(value: MatIconButtonAppearance | '') {
this.setAppearance(value || null);
}

constructor() {
super();
this._rippleLoader.configureRipple(this._elementRef.nativeElement, {centered: true});
}

/** Programmatically sets the appearance of the icon button. */
setAppearance(appearance: MatIconButtonAppearance | null): void {
if (appearance === this._appearance) {
return;
}

const classList = this._elementRef.nativeElement.classList;
const previousClasses = this._appearance ? APPEARANCE_CLASSES.get(this._appearance) : null;
const newClasses = appearance ? APPEARANCE_CLASSES.get(appearance) : null;

if ((typeof ngDevMode === 'undefined' || ngDevMode) && appearance && !newClasses) {
throw new Error(`Unsupported MatIconButton appearance "${appearance}"`);
}

if (previousClasses) {
classList.remove(...previousClasses);
}

if (newClasses) {
classList.add(...newClasses);
}

this._appearance = appearance;
}
}

// tslint:disable:variable-name
Expand Down
20 changes: 16 additions & 4 deletions src/material/button/testing/button-harness.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('MatButtonHarness', () => {

it('should load all button harnesses', async () => {
const buttons = await loader.getAllHarnesses(MatButtonHarness);
expect(buttons.length).toBe(22);
expect(buttons.length).toBe(24);
});

it('should load button with exact text', async () => {
Expand All @@ -40,7 +40,7 @@ describe('MatButtonHarness', () => {
it('should filter by whether a button is disabled', async () => {
const enabledButtons = await loader.getAllHarnesses(MatButtonHarness.with({disabled: false}));
const disabledButtons = await loader.getAllHarnesses(MatButtonHarness.with({disabled: true}));
expect(enabledButtons.length).toBe(20);
expect(enabledButtons.length).toBe(22);
expect(disabledButtons.length).toBe(2);
});

Expand Down Expand Up @@ -134,6 +134,8 @@ describe('MatButtonHarness', () => {
'basic',
'icon',
'icon',
'icon',
'icon',
'fab',
'mini-fab',
'basic',
Expand Down Expand Up @@ -164,6 +166,8 @@ describe('MatButtonHarness', () => {
'tonal',
null,
null,
'filled',
'tonal',
null,
null,
'text',
Expand All @@ -188,8 +192,10 @@ describe('MatButtonHarness', () => {
});

it('should be able to filter buttons based on their appearance', async () => {
const button = await loader.getHarness(MatButtonHarness.with({appearance: 'filled'}));
expect(await button.getText()).toBe('Filled button');
const buttons = await loader.getAllHarnesses(MatButtonHarness.with({appearance: 'filled'}));
const texts = await parallel(() => buttons.map(button => button.getText()));

expect(texts).toEqual(['Filled button', 'add', 'Filled anchor']);
});

it('should get the appearance of a button with a dynamic appearance', async () => {
Expand Down Expand Up @@ -243,6 +249,12 @@ describe('MatButtonHarness', () => {
<button id="favorite-icon" type="button" matIconButton>
<mat-icon>favorite</mat-icon>
</button>
<button id="filled-icon" type="button" matIconButton="filled">
<mat-icon>add</mat-icon>
</button>
<button id="tonal-icon" type="button" matIconButton="tonal">
<mat-icon>bookmark</mat-icon>
</button>
<button id="fab" type="button" matFab>Fab button</button>
<button id="mini-fab" type="button" matMiniFab>Mini Fab button</button>
<button id="submit" type="submit" matButton>Submit button</button>
Expand Down
8 changes: 8 additions & 0 deletions src/material/button/testing/button-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ export class MatButtonHarness extends ContentContainerComponentHarness {
async getAppearance(): Promise<ButtonAppearance | null> {
const host = await this.host();

if (await host.hasClass('mat-mdc-icon-button-filled')) {
return 'filled';
}

if (await host.hasClass('mat-mdc-icon-button-tonal')) {
return 'tonal';
}

if (await host.hasClass('mat-mdc-outlined-button')) {
return 'outlined';
}
Expand Down
Loading