From 5693a07b924910157b5408fa181a6dcb4ff40d1d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov <crisbeto@abv.bg> Date: Tue, 18 Mar 2025 08:41:38 +0100 Subject: [PATCH] feat(material/button): add support for tonal button Adds support for the tonal button appearance from the spec. It can be enabled by setting `matButton="tonal"` on the button. Fixes #28809. --- .../button-overview-example.css | 13 +-- .../button-overview-example.html | 88 +++++++------- src/dev-app/button/button-demo.ts | 2 +- src/material/button/_button-theme.scss | 52 +++++++++ src/material/button/button-base.ts | 2 +- src/material/button/button-high-contrast.scss | 1 + src/material/button/button.md | 10 +- src/material/button/button.scss | 42 ++++++- src/material/button/button.ts | 1 + .../button/testing/button-harness-filters.ts | 2 +- .../button/testing/button-harness.spec.ts | 7 +- src/material/button/testing/button-harness.ts | 4 + src/material/core/tokens/_density.scss | 3 + .../core/tokens/_token-definition.scss | 3 +- src/material/core/tokens/m2/_index.scss | 2 + .../core/tokens/m2/mat/_tonal-button.scss | 110 ++++++++++++++++++ src/material/core/tokens/m3/_index.scss | 2 + .../core/tokens/m3/definitions/_index.scss | 2 +- .../_md-comp-filled-tonal-button.scss | 0 .../core/tokens/m3/mat/_tonal-button.scss | 101 ++++++++++++++++ .../material/button-testing.md | 2 +- tools/public_api_guard/material/button.md | 2 +- 22 files changed, 379 insertions(+), 72 deletions(-) create mode 100644 src/material/core/tokens/m2/mat/_tonal-button.scss rename src/material/core/tokens/m3/definitions/{unused => }/_md-comp-filled-tonal-button.scss (100%) create mode 100644 src/material/core/tokens/m3/mat/_tonal-button.scss diff --git a/src/components-examples/material/button/button-overview/button-overview-example.css b/src/components-examples/material/button/button-overview/button-overview-example.css index 0706c1b66c1d..06631239a9bb 100644 --- a/src/components-examples/material/button/button-overview/button-overview-example.css +++ b/src/components-examples/material/button/button-overview/button-overview-example.css @@ -1,16 +1,15 @@ section { - display: table; + display: flex; + align-items: center; } .example-label { - display: table-cell; font-size: 14px; - margin-left: 8px; + margin: 0 16px 0 8px; min-width: 120px; } .example-button-row { - display: table-cell; max-width: 600px; } @@ -23,9 +22,3 @@ section { justify-content: space-between; flex-wrap: wrap; } - -.example-button-container { - display: flex; - justify-content: center; - width: 140px; -} diff --git a/src/components-examples/material/button/button-overview/button-overview-example.html b/src/components-examples/material/button/button-overview/button-overview-example.html index a8e7332c497d..f2dee64faf0f 100644 --- a/src/components-examples/material/button/button-overview/button-overview-example.html +++ b/src/components-examples/material/button/button-overview/button-overview-example.html @@ -6,7 +6,7 @@ <a matButton href="https://www.google.com/" target="_blank">Link</a> </div> </section> -<mat-divider></mat-divider> +<mat-divider/> <section> <div class="example-label">Elevated</div> <div class="example-button-row"> @@ -15,7 +15,7 @@ <a matButton="elevated" href="https://www.google.com/" target="_blank">Link</a> </div> </section> -<mat-divider></mat-divider> +<mat-divider/> <section> <div class="example-label">Outlined</div> <div class="example-button-row"> @@ -24,16 +24,25 @@ <a matButton="outlined" href="https://www.google.com/" target="_blank">Link</a> </div> </section> -<mat-divider></mat-divider> +<mat-divider/> <section> <div class="example-label">Filled</div> <div class="example-button-row"> - <button matButton="filled" >Basic</button> - <button matButton="filled" disabled>Disabled</button> + <button matButton="filled">Basic</button> + <button matButton="filled" disabled>Disabled</button> <a matButton="filled" href="https://www.google.com/" target="_blank">Link</a> </div> </section> -<mat-divider></mat-divider> +<mat-divider/> +<section> + <div class="example-label">Tonal</div> + <div class="example-button-row"> + <button matButton="tonal" >Basic</button> + <button matButton="tonal" disabled>Disabled</button> + <a matButton="tonal" href="https://www.google.com/" target="_blank">Link</a> + </div> +</section> +<mat-divider/> <section> <div class="example-label">Icon</div> <div class="example-button-row"> @@ -47,64 +56,51 @@ </div> </div> </section> -<mat-divider></mat-divider> +<mat-divider/> <section> <div class="example-label">Floating Action Button (FAB)</div> <div class="example-button-row"> <div class="example-flex-container"> - <div class="example-button-container"> - <button matFab aria-label="Example icon button with a delete icon"> - <mat-icon>delete</mat-icon> - </button> - </div> - <div class="example-button-container"> - <button matFab disabled aria-label="Example icon button with a heart icon"> - <mat-icon>favorite</mat-icon> - </button> - </div> + <button matFab aria-label="Example icon button with a delete icon"> + <mat-icon>delete</mat-icon> + </button> + <button matFab disabled aria-label="Example icon button with a heart icon"> + <mat-icon>favorite</mat-icon> + </button> </div> </div> </section> -<mat-divider></mat-divider> +<mat-divider/> <section> <div class="example-label">Mini FAB</div> <div class="example-button-row"> <div class="example-flex-container"> - <div class="example-button-container"> - <button matMiniFab aria-label="Example icon button with a menu icon"> - <mat-icon>menu</mat-icon> - </button> - </div> - <div class="example-button-container"> - <button matMiniFab disabled aria-label="Example icon button with a home icon"> - <mat-icon>home</mat-icon> - </button> - </div> + <button matMiniFab aria-label="Example icon button with a menu icon"> + <mat-icon>menu</mat-icon> + </button> + <button matMiniFab disabled aria-label="Example icon button with a home icon"> + <mat-icon>home</mat-icon> + </button> </div> </div> </section> +<mat-divider/> <section> <div class="example-label">Extended FAB</div> <div class="example-button-row"> <div class="example-flex-container"> - <div class="example-button-container"> - <button matFab extended> - <mat-icon>favorite</mat-icon> - Basic - </button> - </div> - <div class="example-button-container"> - <button matFab extended disabled> - <mat-icon>favorite</mat-icon> - Disabled - </button> - </div> - <div class="example-button-container"> - <a matFab extended routerLink="."> - <mat-icon>favorite</mat-icon> - Link - </a> - </div> + <button matFab extended> + <mat-icon>favorite</mat-icon> + Basic + </button> + <button matFab extended disabled> + <mat-icon>favorite</mat-icon> + Disabled + </button> + <a matFab extended routerLink="."> + <mat-icon>favorite</mat-icon> + Link + </a> </div> </div> </section> diff --git a/src/dev-app/button/button-demo.ts b/src/dev-app/button/button-demo.ts index 809a1214564e..b8b8fa4b42f3 100644 --- a/src/dev-app/button/button-demo.ts +++ b/src/dev-app/button/button-demo.ts @@ -49,5 +49,5 @@ export class ButtonDemo { toggleDisable = false; tooltipText = 'This is a button tooltip!'; disabledInteractive = false; - appearances: MatButtonAppearance[] = ['text', 'elevated', 'outlined', 'filled']; + appearances: MatButtonAppearance[] = ['text', 'elevated', 'outlined', 'filled', 'tonal']; } diff --git a/src/material/button/_button-theme.scss b/src/material/button/_button-theme.scss index 37aeb9f029d3..5ba48ebb0fbf 100644 --- a/src/material/button/_button-theme.scss +++ b/src/material/button/_button-theme.scss @@ -11,6 +11,7 @@ @use '../core/tokens/m2/mat/protected-button' as tokens-mat-protected-button; @use '../core/tokens/m2/mdc/text-button' as tokens-mdc-text-button; @use '../core/tokens/m2/mat/text-button' as tokens-mat-text-button; +@use '../core/tokens/m2/mat/tonal-button' as tokens-mat-tonal-button; @use '../core/style/sass-utils'; @mixin _text-button-variant($theme, $palette) { @@ -81,6 +82,15 @@ @include token-utils.create-token-values(tokens-mat-outlined-button.$prefix, $mat-tokens); } +@mixin _tonal-button-variant($theme, $palette) { + @include token-utils.create-token-values(tokens-mat-tonal-button.$prefix, if( + $palette, + tokens-mat-tonal-button.private-get-color-palette-color-tokens($theme, $palette), + tokens-mat-tonal-button.get-color-tokens($theme) + )); +} + + @mixin _theme-from-tokens($tokens, $options...) { @include validation.selector-defined( 'Calls to Angular Material theme mixins with an M3 theme must be wrapped in a selector' @@ -125,6 +135,11 @@ tokens-mat-outlined-button.$prefix, $options... ); + $mat-tonal-button-tokens: token-utils.get-tokens-for( + $tokens, + tokens-mat-tonal-button.$prefix, + $options... + ); @include token-utils.create-token-values(tokens-mdc-text-button.$prefix, $mdc-text-button-tokens); @include token-utils.create-token-values( @@ -152,6 +167,10 @@ tokens-mat-outlined-button.$prefix, $mat-outlined-button-tokens ); + @include token-utils.create-token-values( + tokens-mat-tonal-button.$prefix, + $mat-tonal-button-tokens + ); } /// Outputs base theme styles (styles not dependent on the color, typography, or density settings) @@ -195,6 +214,10 @@ tokens-mat-outlined-button.$prefix, tokens-mat-outlined-button.get-unthemable-tokens() ); + @include token-utils.create-token-values( + tokens-mat-tonal-button.$prefix, + tokens-mat-tonal-button.get-unthemable-tokens() + ); } } } @@ -211,6 +234,7 @@ @include sass-utils.current-selector-or-root() { @include _text-button-variant($theme, null); @include _filled-button-variant($theme, null); + @include _tonal-button-variant($theme, null); @include _protected-button-variant($theme, null); @include _outlined-button-variant($theme, null); } @@ -270,6 +294,20 @@ @include _outlined-button-variant($theme, warn); } } + + .mat-tonal-button { + &.mat-primary { + @include _tonal-button-variant($theme, primary); + } + + &.mat-accent { + @include _tonal-button-variant($theme, accent); + } + + &.mat-warn { + @include _tonal-button-variant($theme, warn); + } + } } } @@ -313,6 +351,10 @@ tokens-mat-outlined-button.$prefix, tokens-mat-outlined-button.get-typography-tokens($theme) ); + @include token-utils.create-token-values( + tokens-mat-tonal-button.$prefix, + tokens-mat-tonal-button.get-typography-tokens($theme) + ); } } } @@ -357,6 +399,10 @@ tokens-mat-outlined-button.$prefix, tokens-mat-outlined-button.get-density-tokens($theme) ); + @include token-utils.create-token-values( + tokens-mat-tonal-button.$prefix, + tokens-mat-tonal-button.get-density-tokens($theme) + ); } } } @@ -371,6 +417,7 @@ $mat-protected-button-tokens: tokens-mat-protected-button.get-token-slots(); $mdc-text-button-tokens: tokens-mdc-text-button.get-token-slots(); $mat-text-button-tokens: tokens-mat-text-button.get-token-slots(); + $mat-tonal-button-tokens: tokens-mat-tonal-button.get-token-slots(); @return ( ( @@ -413,6 +460,11 @@ tokens: $mat-text-button-tokens, prefix: 'text-', ), + ( + namespace: tokens-mat-tonal-button.$prefix, + tokens: $mat-tonal-button-tokens, + prefix: 'tonal-', + ), ); } diff --git a/src/material/button/button-base.ts b/src/material/button/button-base.ts index 1514044ace80..271044546c36 100644 --- a/src/material/button/button-base.ts +++ b/src/material/button/button-base.ts @@ -28,7 +28,7 @@ import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; * Possible appearances for a `MatButton`. * See https://m3.material.io/components/buttons/overview */ -export type MatButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined'; +export type MatButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal'; /** Object that can be used to configure the default options for the button component. */ export interface MatButtonConfig { diff --git a/src/material/button/button-high-contrast.scss b/src/material/button/button-high-contrast.scss index d8a19f270dc0..5dbb5164e2c1 100644 --- a/src/material/button/button-high-contrast.scss +++ b/src/material/button/button-high-contrast.scss @@ -4,6 +4,7 @@ .mat-mdc-unelevated-button:not(.mdc-button--outlined), .mat-mdc-raised-button:not(.mdc-button--outlined), .mat-mdc-outlined-button:not(.mdc-button--outlined), +.mat-mdc-button-base.mat-tonal-button, .mat-mdc-icon-button.mat-mdc-icon-button, .mat-mdc-outlined-button .mdc-button__ripple { @include cdk.high-contrast { diff --git a/src/material/button/button.md b/src/material/button/button.md index 5f57cea1c3d5..76b83b2f4743 100644 --- a/src/material/button/button.md +++ b/src/material/button/button.md @@ -20,13 +20,13 @@ There are several button variants, each applied as an attribute: Additionally, the `matButton` has several appearances that can be set using the `matButton` attribute, for example `matButton="outlined"`: - | Appearance | Description | |--------------|----------------------------------------------------------------------------------| -| `text` | Default appearance. Does not have a background until the user interacts with it. | -| `elevated` | Has a background color, elevation and rounded corners. | -| `filled` | Has a flat appearance with rounded corners and no elevation. | -| `outlined` | Has an outline, rounded corners and a transparent background. | +| `text` | Default appearance. Text buttons are used for the lowest priority actions, especially when presenting multiple options. | +| `filled` | High-emphasis buttons used for final or unblocking actions in a flow, such as saving or confirming. | +| `tonal` | Medium-emphasis buttons often used for final or unblocking actions in a flow, but with less visual emphasis than a filled button. | +| `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. | ### Extended FAB buttons diff --git a/src/material/button/button.scss b/src/material/button/button.scss index e82f5305d9d1..c52a44a78d09 100644 --- a/src/material/button/button.scss +++ b/src/material/button/button.scss @@ -11,6 +11,7 @@ @use '../core/tokens/m2/mat/protected-button' as tokens-mat-protected-button; @use '../core/tokens/m2/mdc/text-button' as tokens-mdc-text-button; @use '../core/tokens/m2/mat/text-button' as tokens-mat-text-button; +@use '../core/tokens/m2/mat/tonal-button' as tokens-mat-tonal-button; .mat-mdc-button-base { text-decoration: none; @@ -267,10 +268,48 @@ } } +.mat-tonal-button { + $slots: tokens-mat-tonal-button.get-token-slots(); + transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + + @include token-utils.use-tokens(tokens-mat-tonal-button.$prefix, $slots) { + @include token-utils.create-token-slot(height, container-height); + @include token-utils.create-token-slot(font-family, label-text-font); + @include token-utils.create-token-slot(font-size, label-text-size); + @include token-utils.create-token-slot(letter-spacing, label-text-tracking); + @include token-utils.create-token-slot(text-transform, label-text-transform); + @include token-utils.create-token-slot(font-weight, label-text-weight); + padding: 0 #{token-utils.get-token-variable(horizontal-padding, true)}; + + &:not(:disabled) { + @include token-utils.create-token-slot(color, label-text-color); + @include token-utils.create-token-slot(background-color, container-color); + } + + &, .mdc-button__ripple { + @include token-utils.create-token-slot(border-radius, container-shape); + } + + // We need to re-apply the disabled tokens since MDC uses + // `:disabled` which doesn't apply to anchors. + @include button-base.mat-private-button-disabled { + @include token-utils.create-token-slot(color, disabled-label-text-color); + @include token-utils.create-token-slot(background-color, disabled-container-color); + } + } + + @include button-base.mat-private-button-horizontal-layout(tokens-mat-tonal-button.$prefix, + $slots, false); + @include button-base.mat-private-button-ripple(tokens-mat-tonal-button.$prefix, $slots); + @include button-base.mat-private-button-touch-target(false, tokens-mat-tonal-button.$prefix, + $slots); +} + .mat-mdc-button, .mat-mdc-unelevated-button, .mat-mdc-raised-button, -.mat-mdc-outlined-button { +.mat-mdc-outlined-button, +.mat-tonal-button { @include button-base.mat-private-button-interactive(); @include style-private.private-animation-noop(); @@ -306,6 +345,7 @@ // For the button element, default inset/offset values are necessary to ensure that // the focus indicator is sufficiently contrastive and renders appropriately. .mat-mdc-unelevated-button, +.mat-tonal-button, .mat-mdc-raised-button { .mat-focus-indicator::before { $default-border-width: focus-indicators-private.$default-border-width; diff --git a/src/material/button/button.ts b/src/material/button/button.ts index 8883e1d27664..f6146519acc3 100644 --- a/src/material/button/button.ts +++ b/src/material/button/button.ts @@ -18,6 +18,7 @@ const APPEARANCE_CLASSES: Map<MatButtonAppearance, readonly string[]> = new Map( ['filled', ['mdc-button--unelevated', 'mat-mdc-unelevated-button']], ['elevated', ['mdc-button--raised', 'mat-mdc-raised-button']], ['outlined', ['mdc-button--outlined', 'mat-mdc-outlined-button']], + ['tonal', ['mat-tonal-button']], ]); /** diff --git a/src/material/button/testing/button-harness-filters.ts b/src/material/button/testing/button-harness-filters.ts index 3ecba00636b2..7f053c133ad0 100644 --- a/src/material/button/testing/button-harness-filters.ts +++ b/src/material/button/testing/button-harness-filters.ts @@ -12,7 +12,7 @@ import {BaseHarnessFilters} from '@angular/cdk/testing'; export type ButtonVariant = 'basic' | 'icon' | 'fab' | 'mini-fab'; /** Possible button appearances. */ -export type ButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined'; +export type ButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal'; /** A set of criteria that can be used to filter a list of button harness instances. */ export interface ButtonHarnessFilters extends BaseHarnessFilters { diff --git a/src/material/button/testing/button-harness.spec.ts b/src/material/button/testing/button-harness.spec.ts index fe13608d7a4a..7f3ccda51ef8 100644 --- a/src/material/button/testing/button-harness.spec.ts +++ b/src/material/button/testing/button-harness.spec.ts @@ -22,7 +22,7 @@ describe('MatButtonHarness', () => { it('should load all button harnesses', async () => { const buttons = await loader.getAllHarnesses(MatButtonHarness); - expect(buttons.length).toBe(15); + expect(buttons.length).toBe(16); }); it('should load button with exact text', async () => { @@ -41,7 +41,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(13); + expect(enabledButtons.length).toBe(14); expect(disabledButtons.length).toBe(2); }); @@ -118,6 +118,7 @@ describe('MatButtonHarness', () => { 'basic', 'basic', 'basic', + 'basic', 'icon', 'icon', 'fab', @@ -141,6 +142,7 @@ describe('MatButtonHarness', () => { 'filled', 'elevated', 'outlined', + 'tonal', null, null, null, @@ -178,6 +180,7 @@ describe('MatButtonHarness', () => { </button> <button id="raised" type="button" matButton="elevated">Elevated button</button> <button id="stroked" type="button" matButton="outlined">Outlined button</button> + <button id="tonal" type="button" matButton="tonal">Tonal button</button> <button id="home-icon" type="button" matIconButton> <mat-icon>home</mat-icon> </button> diff --git a/src/material/button/testing/button-harness.ts b/src/material/button/testing/button-harness.ts index b711387e7fd5..e99545d493bb 100644 --- a/src/material/button/testing/button-harness.ts +++ b/src/material/button/testing/button-harness.ts @@ -140,6 +140,10 @@ export class MatButtonHarness extends ContentContainerComponentHarness { return 'text'; } + if (await host.hasClass('mat-tonal-button')) { + return 'tonal'; + } + return null; } } diff --git a/src/material/core/tokens/_density.scss b/src/material/core/tokens/_density.scss index 5bc573138c0c..11500bcc4157 100644 --- a/src/material/core/tokens/_density.scss +++ b/src/material/core/tokens/_density.scss @@ -38,6 +38,9 @@ $_density-tokens: ( (mdc, filled-button): ( container-height: (40px, 36px, 32px, 28px), ), + (mat, tonal-button): ( + container-height: (40px, 36px, 32px, 28px), + ), (mdc, outlined-button): ( container-height: (40px, 36px, 32px, 28px), ), diff --git a/src/material/core/tokens/_token-definition.scss b/src/material/core/tokens/_token-definition.scss index aa4803556ca6..102d7aba5684 100644 --- a/src/material/core/tokens/_token-definition.scss +++ b/src/material/core/tokens/_token-definition.scss @@ -173,8 +173,7 @@ $_system-fallbacks: null; @if(meta.type-of($color) == 'color') { $result: map.remove($result, $opacity-key); $result: map.set($result, $color-key, rgba($color, $opacity)); - } - @else if($color != null) { + } @else if($color != null) { $result: map.remove($result, $opacity-key); $combined-color: #{color-mix(in srgb, #{$color} #{($opacity * 100) + '%'}, transparent)}; $result: map.set($result, $color-key, $combined-color); diff --git a/src/material/core/tokens/m2/_index.scss b/src/material/core/tokens/m2/_index.scss index 85c8c7195b50..ada2482ff850 100644 --- a/src/material/core/tokens/m2/_index.scss +++ b/src/material/core/tokens/m2/_index.scss @@ -45,6 +45,7 @@ @use './mat/toolbar' as tokens-mat-toolbar; @use './mat/tree' as tokens-mat-tree; @use './mat/timepicker' as tokens-mat-timepicker; +@use './mat/tonal-button' as tokens-mat-tonal-button; @use './mdc/checkbox' as tokens-mdc-checkbox; @use './mdc/text-button' as tokens-mdc-text-button; @use './mdc/protected-button' as tokens-mdc-protected-button; @@ -158,6 +159,7 @@ _get-tokens-for-module($theme, tokens-mat-toolbar), _get-tokens-for-module($theme, tokens-mat-tree), _get-tokens-for-module($theme, tokens-mat-timepicker), + _get-tokens-for-module($theme, tokens-mat-tonal-button), _get-tokens-for-module($theme, tokens-mdc-checkbox), _get-tokens-for-module($theme, tokens-mdc-chip), _get-tokens-for-module($theme, tokens-mdc-circular-progress), diff --git a/src/material/core/tokens/m2/mat/_tonal-button.scss b/src/material/core/tokens/m2/mat/_tonal-button.scss new file mode 100644 index 000000000000..d7df7064bbfd --- /dev/null +++ b/src/material/core/tokens/m2/mat/_tonal-button.scss @@ -0,0 +1,110 @@ +@use 'sass:map'; +@use '../../token-definition'; +@use '../../../theming/theming'; +@use '../../../theming/inspection'; +@use '../../../style/sass-utils'; + +// The prefix used to generate the fully qualified name for tokens in this file. +$prefix: (mat, tonal-button); + +// Tokens that can't be configured through Angular Material's current theming API, +// but may be in a future version of the theming API. +@function get-unthemable-tokens() { + @return ( + // Shape of the container element. + container-shape: 4px, + + // Start/end padding of the button. + horizontal-padding: 16px, + + // Space between the icon and the button's main content. + icon-spacing: 8px, + + // Amount by which to offset the icon so that its presence + // doesn't increase throw off the horizontal padding. + icon-offset: -4px, + ); +} + +// Tokens that can be configured through Angular Material's color theming API. +@function get-color-tokens($theme) { + $is-dark: inspection.get-theme-type($theme) == dark; + + @return ( + container-color: inspection.get-theme-color($theme, background, card), + label-text-color: inspection.get-theme-color($theme, foreground, text, 1), + disabled-container-color: inspection.get-theme-color($theme, foreground, disabled-button, + 0.12), + disabled-label-text-color: inspection.get-theme-color($theme, foreground, disabled-button, + if($is-dark, 0.5, 0.38)), + + // Color of the element that shows the hover, focus and pressed states. + state-layer-color: inspection.get-theme-color($theme, foreground, base), + + // Color of the element that shows the hover, focus and pressed states while disabled. + disabled-state-layer-color: inspection.get-theme-color($theme, foreground, base), + + // Color of the ripple element. + ripple-color: inspection.get-theme-color($theme, foreground, base, 0.1), + + // Opacity of the ripple when the button is hovered. + hover-state-layer-opacity: if($is-dark, 0.08, 0.04), + + // Opacity of the ripple when the button is focused. + focus-state-layer-opacity: if($is-dark, 0.24, 0.12), + + // Opacity of the ripple when the button is pressed. + pressed-state-layer-opacity: if($is-dark, 0.24, 0.12), + ); +} + +// Generates the mapping for the properties that change based on the button palette color. +@function private-get-color-palette-color-tokens($theme, $palette-name) { + @return ( + container-color: inspection.get-theme-color($theme, $palette-name, default), + label-text-color: inspection.get-theme-color($theme, $palette-name, default-contrast, 1), + state-layer-color: inspection.get-theme-color($theme, $palette-name, default-contrast, 1), + ripple-color: inspection.get-theme-color($theme, $palette-name, default-contrast, 0.1), + ); +} + +// Tokens that can be configured through Angular Material's typography theming API. +@function get-typography-tokens($theme) { + @return ( + label-text-font: inspection.get-theme-typography($theme, button, font-family), + label-text-size: inspection.get-theme-typography($theme, button, font-size), + label-text-tracking: inspection.get-theme-typography($theme, button, letter-spacing), + label-text-weight: inspection.get-theme-typography($theme, button, font-weight), + label-text-transform: none, + ); +} + +// Tokens that can be configured through Angular Material's density theming API. +@function get-density-tokens($theme) { + $scale: theming.clamp-density(inspection.get-theme-density($theme), -3); + + @return ( + touch-target-display: if($scale < -1, none, block), + container-height: + map.get( + ( + 0: 36px, + -1: 32px, + -2: 28px, + -3: 24px, + ), + $scale + ) + ); +} + +// Combines the tokens generated by the above functions into a single map with placeholder values. +// This is used to create token slots. +@function get-token-slots() { + @return sass-utils.deep-merge-all( + get-unthemable-tokens(), + get-color-tokens(token-definition.$placeholder-color-config), + get-typography-tokens(token-definition.$placeholder-typography-config), + get-density-tokens(token-definition.$placeholder-density-config) + ); +} diff --git a/src/material/core/tokens/m3/_index.scss b/src/material/core/tokens/m3/_index.scss index 4d85b20f002a..d7c8835c1586 100644 --- a/src/material/core/tokens/m3/_index.scss +++ b/src/material/core/tokens/m3/_index.scss @@ -43,6 +43,7 @@ @use './mat/toolbar' as tokens-mat-toolbar; @use './mat/tree' as tokens-mat-tree; @use './mat/timepicker' as tokens-mat-timepicker; +@use './mat/tonal-button' as tokens-mat-tonal-button; @use './mdc/checkbox' as tokens-mdc-checkbox; @use './mdc/text-button' as tokens-mdc-text-button; @use './mdc/protected-button' as tokens-mdc-protected-button; @@ -85,6 +86,7 @@ $_module-names: ( tokens-mat-fab, tokens-mat-fab-small, tokens-mat-filled-button, + tokens-mat-tonal-button, tokens-mat-form-field, tokens-mat-grid-list, tokens-mat-icon-button, diff --git a/src/material/core/tokens/m3/definitions/_index.scss b/src/material/core/tokens/m3/definitions/_index.scss index bd05e31547b5..60eb8adf886f 100644 --- a/src/material/core/tokens/m3/definitions/_index.scss +++ b/src/material/core/tokens/m3/definitions/_index.scss @@ -14,6 +14,7 @@ @forward './md-comp-fab-tertiary-small' as md-comp-fab-tertiary-small-*; @forward './md-comp-fab-tertiary' as md-comp-fab-tertiary-*; @forward './md-comp-filled-button' as md-comp-filled-button-*; +@forward './md-comp-filled-tonal-button' as md-comp-filled-tonal-button-*; @forward './md-comp-filled-card' as md-comp-filled-card-*; @forward './md-comp-filled-icon-button' as md-comp-filled-icon-button-*; @forward './md-comp-filled-text-field' as md-comp-filled-text-field-*; @@ -63,7 +64,6 @@ // @forward './unused/md-comp-filled-autocomplete' as md-comp-filled-autocomplete-*; // @forward './unused/md-comp-filled-menu-button' as md-comp-filled-menu-button-*; // @forward './unused/md-comp-filled-select' as md-comp-filled-select-*; -// @forward './unused/md-comp-filled-tonal-button' as md-comp-filled-tonal-button-*; // @forward './unused/md-comp-filled-tonal-icon-button' as md-comp-filled-tonal-icon-button-*; // @forward './unused/md-comp-filter-chip' as md-comp-filter-chip-*; // @forward './unused/md-comp-full-screen-dialog' as md-comp-full-screen-dialog-*; diff --git a/src/material/core/tokens/m3/definitions/unused/_md-comp-filled-tonal-button.scss b/src/material/core/tokens/m3/definitions/_md-comp-filled-tonal-button.scss similarity index 100% rename from src/material/core/tokens/m3/definitions/unused/_md-comp-filled-tonal-button.scss rename to src/material/core/tokens/m3/definitions/_md-comp-filled-tonal-button.scss diff --git a/src/material/core/tokens/m3/mat/_tonal-button.scss b/src/material/core/tokens/m3/mat/_tonal-button.scss new file mode 100644 index 000000000000..0069c240db12 --- /dev/null +++ b/src/material/core/tokens/m3/mat/_tonal-button.scss @@ -0,0 +1,101 @@ +@use 'sass:map'; +@use 'sass:meta'; +@use '../../../style/sass-utils'; +@use '../../token-definition'; + +// The prefix used to generate the fully qualified name for tokens in this file. +$prefix: (mat, tonal-button); + +/// Generates custom tokens for the mat-flat-button. +/// @param {Map} $systems The MDC system tokens +/// @param {Boolean} $exclude-hardcoded Whether to exclude hardcoded token values +/// @param {Map} $token-slots Possible token slots +/// @return {Map} A set of custom tokens for the mat-flat-button +@function get-tokens($systems, $exclude-hardcoded, $token-slots) { + $mdc-tokens: token-definition.get-mdc-tokens('filled-tonal-button', $systems, $exclude-hardcoded); + + $tokens: map.merge($mdc-tokens, ( + horizontal-padding: token-definition.hardcode(24px, $exclude-hardcoded), + icon-spacing: token-definition.hardcode(8px, $exclude-hardcoded), + icon-offset: token-definition.hardcode(-8px, $exclude-hardcoded), + state-layer-color: map.get($systems, md-sys-color, on-secondary-container), + disabled-state-layer-color: map.get($systems, md-sys-color, on-surface-variant), + ripple-color: sass-utils.safe-color-change( + map.get($systems, md-sys-color, on-secondary-container), + $alpha: map.get($systems, md-sys-state, pressed-state-layer-opacity) + ), + hover-state-layer-opacity: map.get($systems, md-sys-state, hover-state-layer-opacity), + focus-state-layer-opacity: map.get($systems, md-sys-state, focus-state-layer-opacity), + pressed-state-layer-opacity: map.get($systems, md-sys-state, pressed-state-layer-opacity), + )); + + $variant-tokens: ( + // Color variants: + primary: (), + secondary: (), + tertiary: ( + container-color: map.get($systems, md-sys-color, tertiary-container), + focus-label-text-color: map.get($systems, md-sys-color, on-tertiary-container), + focus-state-layer-color: map.get($systems, md-sys-color, on-tertiary-container), + hover-label-text-color: map.get($systems, md-sys-color, on-tertiary-container), + hover-state-layer-color: map.get($systems, md-sys-color, on-tertiary-container), + label-text-color: map.get($systems, md-sys-color, on-tertiary-container), + pressed-label-text-color: map.get($systems, md-sys-color, on-tertiary-container), + pressed-state-layer-color: map.get($systems, md-sys-color, on-tertiary-container), + with-icon-focus-icon-color: map.get($systems, md-sys-color, on-tertiary-container), + with-icon-hover-icon-color: map.get($systems, md-sys-color, on-tertiary-container), + with-icon-icon-color: map.get($systems, md-sys-color, on-tertiary-container), + with-icon-pressed-icon-color: map.get($systems, md-sys-color, on-tertiary-container), + state-layer-color: map.get($systems, md-sys-color, on-tertiary-container), + ripple-color: sass-utils.safe-color-change( + map.get($systems, md-sys-color, on-tertiary-container), + $alpha: map.get($systems, md-sys-state, pressed-state-layer-opacity) + ), + ), + error: ( + container-color: map.get($systems, md-sys-color, error-container), + focus-label-text-color: map.get($systems, md-sys-color, on-error-container), + focus-state-layer-color: map.get($systems, md-sys-color, on-error-container), + hover-label-text-color: map.get($systems, md-sys-color, on-error-container), + hover-state-layer-color: map.get($systems, md-sys-color, on-error-container), + label-text-color: map.get($systems, md-sys-color, on-error-container), + pressed-label-text-color: map.get($systems, md-sys-color, on-error-container), + pressed-state-layer-color: map.get($systems, md-sys-color, on-error-container), + with-icon-focus-icon-color: map.get($systems, md-sys-color, on-error-container), + with-icon-hover-icon-color: map.get($systems, md-sys-color, on-error-container), + with-icon-icon-color: map.get($systems, md-sys-color, on-error-container), + with-icon-pressed-icon-color: map.get($systems, md-sys-color, on-error-container), + state-layer-color: map.get($systems, md-sys-color, on-error-container), + ripple-color: sass-utils.safe-color-change( + map.get($systems, md-sys-color, on-error-container), + $alpha: map.get($systems, md-sys-state, pressed-state-layer-opacity) + ), + ) + ); + + @return token-definition.namespace-tokens($prefix, ( + _fix-tokens($tokens), + token-definition.map-values($variant-tokens, meta.get-function(_fix-tokens)) + ), $token-slots); +} + + +/// Fixes inconsistent values in the tonal button tokens so that they can produce valid styles. +/// @param {Map} $initial-tokens Map of tonal button tokens currently being generated. +/// @return {Map} The given tokens, with the invalid values replaced with valid ones. +@function _fix-tokens($initial-tokens) { + // Need to get the hardcoded values, because they include opacities that are used for the disabled + // state. + $hardcoded-tokens: token-definition.get-mdc-tokens('filled-tonal-button', (), false); + + @return token-definition.combine-color-tokens($initial-tokens, $hardcoded-tokens, ( + ( + color: disabled-label-text-color, + opacity: disabled-label-text-opacity, + ), + ( + color: disabled-container-color, + opacity: disabled-container-opacity, + ) + )); +} diff --git a/tools/public_api_guard/material/button-testing.md b/tools/public_api_guard/material/button-testing.md index 7f7bf13f38db..b8d5746b1f94 100644 --- a/tools/public_api_guard/material/button-testing.md +++ b/tools/public_api_guard/material/button-testing.md @@ -10,7 +10,7 @@ import { ContentContainerComponentHarness } from '@angular/cdk/testing'; import { HarnessPredicate } from '@angular/cdk/testing'; // @public -export type ButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined'; +export type ButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal'; // @public export interface ButtonHarnessFilters extends BaseHarnessFilters { diff --git a/tools/public_api_guard/material/button.md b/tools/public_api_guard/material/button.md index 508b149edf41..30516a1be890 100644 --- a/tools/public_api_guard/material/button.md +++ b/tools/public_api_guard/material/button.md @@ -44,7 +44,7 @@ export class MatButton extends MatButtonBase { } // @public -export type MatButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined'; +export type MatButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal'; // @public export interface MatButtonConfig {