From 227a741c659a17f58b22cd79d47031df2ce12641 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 9 Jun 2023 18:58:12 +0000 Subject: [PATCH] feat(material-experimental/theming): Introduce a facade layer between user-facing customizable keys and actual MDC token names (#27219) * feat(material-experimental/theming): Introduce a facade layer between user-facing customizable keys and actual MDC token names This allows us to expose easier to understand names for users, and decouples us from changes that MDC might make to token names in the future * Demo using the new API to implement dark theme & primary/accent/warn * fixup! Demo using the new API to implement dark theme & primary/accent/warn * Demo completely custom token values * Add some brief bullet point docs of the API --- src/dev-app/checkbox/checkbox-demo.html | 6 + src/dev-app/theme-token-api.scss | 96 +++++++++++---- src/material-experimental/_index.scss | 4 +- src/material-experimental/theming/README.md | 61 ++++++++++ src/material-experimental/theming/_card.scss | 93 +++++++++++++++ .../theming/_checkbox.scss | 109 ++++++++++++++++++ .../theming/_theming.scss | 22 +--- .../theming/_token-resolution.scss | 88 ++++++++++++++ src/material/card/_card-theme.scss | 11 +- src/material/checkbox/_checkbox-theme.scss | 5 +- src/material/core/style/_sass-utils.scss | 12 ++ 11 files changed, 449 insertions(+), 58 deletions(-) create mode 100644 src/material-experimental/theming/README.md create mode 100644 src/material-experimental/theming/_card.scss create mode 100644 src/material-experimental/theming/_checkbox.scss create mode 100644 src/material-experimental/theming/_token-resolution.scss diff --git a/src/dev-app/checkbox/checkbox-demo.html b/src/dev-app/checkbox/checkbox-demo.html index d185850f53a0..ce19e105b890 100644 --- a/src/dev-app/checkbox/checkbox-demo.html +++ b/src/dev-app/checkbox/checkbox-demo.html @@ -279,3 +279,9 @@
No animations
+ +

+ + This checkbox has special styling when using the experimental token based theme + +

diff --git a/src/dev-app/theme-token-api.scss b/src/dev-app/theme-token-api.scss index 284b1312f249..d26eb2d68df4 100644 --- a/src/dev-app/theme-token-api.scss +++ b/src/dev-app/theme-token-api.scss @@ -1,3 +1,4 @@ +@use 'sass:map'; @use '@angular/material' as mat; @use '@angular/material-experimental' as matx; @@ -23,7 +24,7 @@ dev-app { @include mat.core(); -$light-theme: mat.define-light-theme(( +$theme: mat.define-light-theme(( color: ( primary: mat.define-palette(mat.$indigo-palette), accent: mat.define-palette(mat.$pink-palette), @@ -32,37 +33,82 @@ $light-theme: mat.define-light-theme(( density: 0, )); -$dark-theme: mat.define-dark-theme(( - color: ( - primary: mat.define-palette(mat.$blue-grey-palette), - accent: mat.define-palette(mat.$amber-palette, A200, A100, A400), - warn: mat.define-palette(mat.$deep-orange-palette), - ), - typography: mat.define-typography-config(), - density: 0, -)); - -// Set up light theme. - +// Apply all tokens (derived from `$theme`) to the `html` element. This ensures that all components +// on the page will inherit these tokens. html { @include matx.theme( - $tokens: mat.m2-tokens-from-theme($light-theme), + $tokens: mat.m2-tokens-from-theme($theme), $components: ( matx.card(), matx.checkbox(), - )); + ) + ); } -// Set up dark theme. +// Apply tokens needed for dark theme to the element with `.demo-unicorn-dark-theme`. +// This ensures that checkboxes within the element inherit the new tokens for dark theme, +// rather than the ones for light theme tokens set on `body`. Note that we're not setting *all* of +// the tokens, since many (density, typography, etc) are the same between light and dark theme. .demo-unicorn-dark-theme { - @include matx.theme( - $tokens: mat.m2-tokens-from-theme($dark-theme), - $components: ( - matx.checkbox(( - (mdc, checkbox): ( - selected-checkmark-color: red, - ) - )), - )); + @include matx.retheme(( + // TODO(mmalerba): In the future this should be configured through `matx.system-colors()` + matx.checkbox((theme-type: dark)), + matx.card((theme-type: dark)), + )); +} + +// Apply tokens related to the color palette to any element with `.mat-primary`, `.mat-accent`, or +// `.mat-warn` This ensures that checkboxes within the element inherit the new tokens for the +// appropriate palette, rather than the any color that may have been set on an element further up +// the hierarchy. Again, rather than applying *all* the tokens, we apply only the ones effected by +// the palette color. With this setup, the palette class need not go on the component itself +// (e.g. ), it can go on some ancestor element and the tokens will +// flow down. If multiple elements specify different classes, the closest one to the component will +// take precedence. +// (e.g.
I'm primary
) +.mat-primary { + @include matx.retheme(( + matx.checkbox(( + color-palette: map.get($theme, color, primary) + )), + )); +} +.mat-accent { + @include matx.retheme(( + matx.checkbox(( + color-palette: map.get($theme, color, accent) + )), + )); +} +.mat-warn { + @include matx.retheme(( + matx.checkbox(( + color-palette: map.get($theme, color, warn) + )), + )); +} + +// Apply tokens for a completely custom checkbox that appears as an unfilled red box when unchecked, +// and a filled green box when checked. +.demo-traffic-light-checkbox { + @include matx.retheme(( + matx.checkbox(( + checkmark-color: transparent, + selected-box-color: green, + selected-focus-box-color: green, + selected-hover-box-color: green, + selected-pressed-box-color: green, + selected-focus-ring-color: green, + selected-hover-ring-color: green, + selected-pressed-ring-color: green, + unselected-box-color: red, + unselected-focus-box-color: red, + unselected-hover-box-color: red, + unselected-pressed-box-color: red, + unselected-focus-ring-color: red, + unselected-hover-ring-color: red, + unselected-pressed-ring-color: red, + )) + )); } diff --git a/src/material-experimental/_index.scss b/src/material-experimental/_index.scss index fd2a45e8fc61..8ce0e1b965fc 100644 --- a/src/material-experimental/_index.scss +++ b/src/material-experimental/_index.scss @@ -5,6 +5,8 @@ popover-edit-typography, popover-edit-density, popover-edit-theme; // Token-based theming API -@forward './theming/theming' show theme, card, checkbox; +@forward './theming/theming' show theme, retheme; +@forward './theming/checkbox' show checkbox; +@forward './theming/card' show card; // Additional public APIs for individual components diff --git a/src/material-experimental/theming/README.md b/src/material-experimental/theming/README.md new file mode 100644 index 000000000000..ebe43c7a4cfd --- /dev/null +++ b/src/material-experimental/theming/README.md @@ -0,0 +1,61 @@ +This is an experimental theming API based on [design tokens](https://m3.material.io/foundations/design-tokens/how-to-use-tokens). It is currently in the prototype phase, +and still being evaluated. + +## Design tokens +- Design tokens are a set of variables that determine what components look like. They can affect things like color, typography, desnity, elevation, border radius, and more. +- Angular Material represents design tokens as CSS variables + +## M2 vs M3 tokens +- Angular Material can use tokens corresponding to either the [Material Design 2](https://m2.material.io/) or [Material Design 3](https://m3.material.io/) spec + - Token values for M2 can be obtained by: + 1. Generating them from an Angular Material theme object (e.g. one defined with `mat.define-light-theme`). To generate M2 tokens for a theme, pass it to the `mat.m2-tokens-from-theme` function. + - Token values for M3 are not yet available + +Example: +```scss +// Create an Angular Material theme. +$my-theme: mat.define-light-theme(...); + +// Create tokens for M2 from the theme. +$m2-tokens: mat.m2-tokens-from-theme($my-theme); +``` +## Component theme configuration functions +- These functions are used to specify which tokens should be applied by the theming mixins _and_ to customize the tokens used in that component to something other than the value from the token set +- So far the following component theme configuration functions have been implements: + - `matx.checkbox` configures tokens for the mat-checkbox to be applied + - `matx.card` configures tokens for the mat-card to be applied +- The returned configurations from these functions are passed to `matx.theme` or `matx.retheme` +- If no arguments are passed, the configuration instructs the mixin to just output the default value for all of the tokens needed by that component +- The functions can also accept a map of customizations as an argument. + - Each function has its own set of supported map keys that can be used to customize the value of the underlying tokens + - The map keys are a higher level API then the tokens, some of the keys may result in a single token being change, but some may change multiple tokens + - For supported map keys (TODO: have docs for these): + - See `$_customization-resolvers` [here](https://github.com/angular/components/blob/main/src/material-experimental/theming/_checkbox.scss) for `matx.checkbox` + - See `$_customization-resolvers` [here](https://github.com/angular/components/blob/main/src/material-experimental/theming/_card.scss) for `matx.card` + +## Theming mixins +- There are 2 mixins used for theming apps + - `matx.theme` is intended to apply the full theme for some components, with all tokens they need to function. + - `matx.retheme` is intended to re-apply specific tokens to change the appearance for some components by overriding the tokens applied by `matx.theme`. +- Both mixins emit *only* CSS variables representing design tokens +- Both mixins emit their tokens directly under the user specified selector. This gives the user complete control over the selector specificity. +- Using `matx.theme` + - Takes 2 arguments: + - `$tokens` The set of token defaults that will be used for any tokens not explicitly customized by the component theme config + - `$components` List of component theme configs indicating which components to emit tokens for, and optionally, customizations for some token values + - Outputs *all* tokens used by the configured components +- Using `matx.retheme` + - Takes 1 argument: + - `$components` List of component theme configs to emit customized token values for + - Outputs *only* the explicitly customized tokens, not any of the other tokens used by the component + +## Recommended theming structure +- Apply the base token values using `matx.theme` *once* +- Choose selectors with minimal specificity when applying tokens +- Prefer to rely on CSS inheritance to apply token overrides rather than specificity. + For example if checkbox tokens are set on the root element (`html`) they will be inherited down + the DOM and affect any `` within the document. If checkboxes in a specific section + need to appear differently, say within `.dark-sidebar`, set the token overrides on the + `.dark-sidebar` element and they will be inherited down to the checkboxes within, instead of the + values from the root element. +- For a small example, see this [alternate partial theme](https://github.com/angular/components/blob/main/src/dev-app/theme-token-api.scss) for the dev-app diff --git a/src/material-experimental/theming/_card.scss b/src/material-experimental/theming/_card.scss new file mode 100644 index 000000000000..7ec6148f403d --- /dev/null +++ b/src/material-experimental/theming/_card.scss @@ -0,0 +1,93 @@ +@use 'sass:color'; +@use 'sass:meta'; +@use '@angular/material' as mat; +@use './token-resolution'; + +// TODO(mmalerba): This should live under material/card when moving out of experimental. + +/// Gets tokens for setting the card's shape. +/// @param {String} $shape The card's shape. +/// @return {Map} A map of tokens for setting the card's shape. +// Note: we use a function rather than simple rename, because we want to map a single shape value to +// multiple tokens, rather than offer separate shape customizations for elevated and outlined cards. +@function _get-tokens-for-card-shape($shape) { + @return ( + (mdc, elevated-card): (container-shape: $shape), + (mdc, outline-card): (container-shape: $shape), + ); +} + +/// Gets tokens for setting the card's color. +/// @param {String} $shape The card's shape. +/// @return {Map} A map of tokens for setting the card's shape. +@function _get-tokens-for-card-color($color) { + @return ( + (mdc, elevated-card): (container-color: $color), + (mdc, outline-card): (container-color: $color), + ); +} + +/// Gets a map of card token values that are derived from the theme type. +/// @param {'light' | 'dark'} $theme-type The type of theme. +/// @return {Map} A map of card token values derived from the given theme type. +@function _get-tokens-for-theme-type($theme-type) { + $is-dark: $theme-type == 'dark'; + $foreground: if($is-dark, white, black); + $card-color: if($is-dark, mat.get-color-from-palette(mat.$gray-palette, 800), white); + $outline-color: color.change($foreground, $alpha: 0.12); + $subtitle-color: if($is-dark, rgba(white, 0.7), rgba(black, 0.54)); + + @return ( + (mdc, elevated-card): ( + container-color: $card-color, + ), + (mdc, outlined-card): ( + container-color: $card-color, + outline-color: $outline-color, + ), + (mat, card): ( + subtitle-text-color: $subtitle-color, + ), + ); +} + +/// Resolvers for mat-card customizations. +$_customization-resolvers: mat.private-merge-all( + token-resolution.alias(( + elevation: container-elevation, + shadow-color: container-shadow-color, + ), (mdc, elevated-card)), + token-resolution.forward(( + outline-width, + outline-color + ), (mdc, outlined-card)), + token-resolution.alias(( + title-font: title-text-font, + title-line-height: title-text-line-height, + title-font-size: title-text-size, + title-letter-spacing: title-text-tracking, + title-font-weight: title-text-weight, + subtitle-font: subtitle-text-font, + subtitle-line-height: subtitle-text-line-height, + subtitle-font-size: subtitle-text-size, + subtitle-letter-spacing: subtitle-text-tracking, + subtitle-font-weight: subtitle-text-weight, + subtitle-color: subtitle-text-color + ), (mat, card)), + ( + background-color: meta.get-function(_get-tokens-for-card-color), + border-radius: meta.get-function(_get-tokens-for-card-shape), + theme-type: meta.get-function(_get-tokens-for-theme-type), + ) +); + +/// Configure the mat-card's theme. +/// @param {Map} $customizations [()] A map of custom values to use when theming mat-card. +@function card($customizations: ()) { + @return ( + id: 'mat.card', + customizations: token-resolution.resolve-customized-tokens( + 'mat.card', $_customization-resolvers, $customizations), + deps: (), + ); +} diff --git a/src/material-experimental/theming/_checkbox.scss b/src/material-experimental/theming/_checkbox.scss new file mode 100644 index 000000000000..b89b2c8e9907 --- /dev/null +++ b/src/material-experimental/theming/_checkbox.scss @@ -0,0 +1,109 @@ +@use 'sass:color'; +@use 'sass:map'; +@use 'sass:meta'; +@use '@angular/material' as mat; +@use '@material/theme/theme-color' as mdc-theme-color; +@use './token-resolution'; + +// TODO(mmalerba): This should live under material/checkbox when moving out of experimental. + +// Duplicated from core/tokens/m2/mdc/checkbox +// TODO(mmalerba): Delete duplicated code when this is moved out of experimental. +@function _contrast-tone($value, $light-color: '#fff', $dark-color: '#000') { + @if ($value == 'dark' or $value == 'light' or type-of($value) == 'color') { + @return if(mdc-theme-color.contrast-tone($value) == 'dark', $dark-color, $light-color); + } + @return false; +} + +/// Gets a map of checkbox token values that are derived from the given palette. +/// @param {Map} $palette An Angular Material palette object. +/// @return {Map} A map of checkbox token values derived from the given palette. +@function _get-tokens-for-color-palette($palette) { + $palette-default-color: mat.get-color-from-palette($palette); + $checkmark-color: _contrast-tone($palette-default-color); + + @return ( + (mdc, checkbox): ( + selected-checkmark-color: $checkmark-color, + selected-focus-icon-color: $palette-default-color, + selected-hover-icon-color: $palette-default-color, + selected-icon-color: $palette-default-color, + selected-pressed-icon-color: $palette-default-color, + selected-focus-state-layer-color: $palette-default-color, + selected-hover-state-layer-color: $palette-default-color, + selected-pressed-state-layer-color: $palette-default-color, + ) + ); +} + +/// Gets a map of checkbox token values that are derived from the theme type. +/// @param {'light' | 'dark'} $theme-type The type of theme. +/// @return {Map} A map of checkbox token values derived from the given theme type. +@function _get-tokens-for-theme-type($theme-type) { + $is-dark: $theme-type == dark; + $foreground: if($is-dark, white, black); + $disabled-color: color.change($foreground, $alpha: 0.38); + $border-color: color.change($foreground, $alpha: 0.54); + $active-border-color: mat.get-color-from-palette(mat.$gray-palette, if($is-dark, 200, 900)); + + @return ( + (mdc, checkbox): ( + disabled-selected-icon-color: $disabled-color, + disabled-unselected-icon-color: $disabled-color, + unselected-focus-icon-color: $active-border-color, + unselected-hover-icon-color: $active-border-color, + unselected-icon-color: $border-color, + unselected-pressed-icon-color: $border-color, + unselected-focus-state-layer-color: $foreground, + unselected-hover-state-layer-color: $foreground, + unselected-pressed-state-layer-color: $foreground, + ) + ); +} + +/// Resolvers for mat-checkbox customizations. +$_customization-resolvers: map.merge( + token-resolution.alias(( + checkmark-color: selected-checkmark-color, + disabled-checkmark-color: disabled-selected-checkmark-color, + selected-focus-ring-opacity: selected-focus-state-layer-opacity, + selected-hover-ring-opacity: selected-hover-state-layer-opacity, + selected-pressed-ring-opacity: selected-pressed-state-layer-opacity, + unselected-focus-ring-opacity: unselected-focus-state-layer-opacity, + unselected-hover-ring-opacity: unselected-hover-state-layer-opacity, + unselected-pressed-ring-opacity: unselected-pressed-state-layer-opacity, + disabled-selected-box-color: disabled-selected-icon-color, + disabled-unselected-box-color: disabled-unselected-icon-color, + selected-focus-box-color: selected-focus-icon-color, + selected-hover-box-color: selected-hover-icon-color, + selected-box-color: selected-icon-color, + selected-pressed-box-color: selected-pressed-icon-color, + unselected-focus-box-color: unselected-focus-icon-color, + unselected-hover-box-color: unselected-hover-icon-color, + unselected-box-color: unselected-icon-color, + unselected-pressed-box-color: unselected-pressed-icon-color, + selected-focus-ring-color: selected-focus-state-layer-color, + selected-hover-ring-color: selected-hover-state-layer-color, + selected-pressed-ring-color: selected-pressed-state-layer-color, + unselected-focus-ring-color: unselected-focus-state-layer-color, + unselected-hover-ring-color: unselected-hover-state-layer-color, + unselected-pressed-ring-color: unselected-pressed-state-layer-color, + ripple-size: state-layer-size, + ), (mdc, checkbox)), + ( + color-palette: meta.get-function(_get-tokens-for-color-palette), + theme-type: meta.get-function(_get-tokens-for-theme-type), + ) +); + +/// Configure the mat-checkbox's theme. +/// @param {Map} $customizations [()] A map of custom values to use when theming mat-checkbox. +@function checkbox($customizations: ()) { + @return ( + id: 'mat.checkbox', + customizations: token-resolution.resolve-customized-tokens( + 'mat.checkbox', $_customization-resolvers, $customizations), + deps: (), + ); +} diff --git a/src/material-experimental/theming/_theming.scss b/src/material-experimental/theming/_theming.scss index 9a6b181d8a8a..1c4007da0bb1 100644 --- a/src/material-experimental/theming/_theming.scss +++ b/src/material-experimental/theming/_theming.scss @@ -110,25 +110,5 @@ $_error-on-missing-dep: false; // - override-theme // - retheme @mixin retheme($components) { - @include _theme((), $components); -} - -/// Configure the mat-card's theme. -/// @param {Map} $customizations [()] A map of custom token values to use when theming mat-card. -@function card($customizations: ()) { - @return ( - id: 'mat.card', - customizations: $customizations, - deps: (), - ); -} - -/// Configure the mat-checkbox's theme. -/// @param {Map} $customizations [()] A map of custom token values to use when theming mat-checkbox. -@function checkbox($customizations: ()) { - @return ( - id: 'mat.checkbox', - customizations: $customizations, - deps: (), - ); + @include _theme((), mat.private-coerce-to-list($components)); } diff --git a/src/material-experimental/theming/_token-resolution.scss b/src/material-experimental/theming/_token-resolution.scss new file mode 100644 index 000000000000..d3edcce7d729 --- /dev/null +++ b/src/material-experimental/theming/_token-resolution.scss @@ -0,0 +1,88 @@ +@use 'sass:list'; +@use 'sass:map'; +@use 'sass:meta'; + +/// Creates a map of short token names to fully qualified token name under the given namespace. +/// @param {List} $tokens A list of tokens to forward under the given namespace. +/// @param {List} $namespace The namespace to use for the forwarded tokens. +/// @return {Map} A map of the short token name to pairs of (namespace, token-name) representing the +/// fully-qualified name +/// @example +/// forward((token1, token2), (mat, my-comp)) +/// => ( +/// token1: ((mat, my-comp), token1), +/// token2: ((mat, my-comp), token1) +/// ) +@function forward($tokens, $namespace) { + $result: (); + @each $token in $tokens { + $result: map.set($result, $token, ($namespace, $token)); + } + @return $result; +} + +// Creates a map of token alias names to fully qualified canonical names under the given namespace. +/// @param {Map} $tokens A map of aliases to canonical short names for tokens under the given +/// namespace. +/// @param {List} $namespace The namespace to use for the canonical tokens. +/// @return A map of the token alias name to pairs of (namespace, token-name) representing the +/// fully-qualified canonical name of the token. +/// @example +/// alias((alias1: canonical1, alias2: canonical2), (mat, my-comp)) +/// => ( +/// alias1: ((mat, my-comp), canonical1), +/// alias2: ((mat, my-comp), canonical2) +/// ) +@function alias($tokens, $namespace) { + $result: (); + @each $from, $to in $tokens { + $result: map.set($result, $from, ($namespace, $to)); + } + @return $result; +} + +/// Gets the full set of customized tokens from a component configuration's customization map. +/// @param {String} $component-id The id of the component whose customizations are being resolved. +/// Used for error logging purposes. +/// @param {Map} $customization-resolvers A map of resolvers that map customization keys to +/// fully-qualified token names or functions to generate fully-qualified token names. +/// @param {Map} $customizations A map of values for customization keys +/// @return {Map} A map of fully-qualified token values +/// @example +/// resolve-customized-tokens('mat.checkbox', +/// forward(my-color, my-size, (mat, my-comp)), +/// (my-color: red, my-size: 100px) +/// ) +/// => ( +/// (mat, my-comp): ( +/// my-color: red, +/// my-size: 100px +/// ) +/// ) +@function resolve-customized-tokens($component-id, $customization-resolvers, $customizations) { + $result: (); + + @each $customization, $value in $customizations { + $resolver: map.get($customization-resolvers, $customization); + @if not $resolver { + @error 'Unrecognized customization for #{$component-id}: #{$customization}'; + } + + $resolver-type: meta.type-of($resolver); + @if $resolver-type == 'list' { + // If the resolver is a list, it represents the token namespace and name. + $key-and-value: list.append($resolver, $value); + $result: map.deep-merge($result, map.set((), $key-and-value...)); + } + @else if $resolver-type == 'function' { + // If the resolver is a function, it should take a value and return a token map. + $result: map.deep-merge($result, meta.call($resolver, $value)); + } + @else { + // Anything else is unexpected. + @error 'Invalid customization resolver for `#{$customization}` on #{$component-id}'; + } + } + + @return $result; +} diff --git a/src/material/card/_card-theme.scss b/src/material/card/_card-theme.scss index 2f3eb3cbb71a..bef100328208 100644 --- a/src/material/card/_card-theme.scss +++ b/src/material/card/_card-theme.scss @@ -79,11 +79,8 @@ } @mixin theme-from-tokens($tokens) { - // Add values for card tokens. - .mat-mdc-card { - @include mdc-elevated-card-theme.theme(map.get($tokens, tokens-mdc-elevated-card.$prefix)); - @include mdc-outlined-card-theme.theme(map.get($tokens, tokens-mdc-outlined-card.$prefix)); - @include token-utils.create-token-values( - tokens-mat-card.$prefix, map.get($tokens, tokens-mat-card.$prefix)); - } + @include mdc-elevated-card-theme.theme(map.get($tokens, tokens-mdc-elevated-card.$prefix)); + @include mdc-outlined-card-theme.theme(map.get($tokens, tokens-mdc-outlined-card.$prefix)); + @include token-utils.create-token-values( + tokens-mat-card.$prefix, map.get($tokens, tokens-mat-card.$prefix)); } diff --git a/src/material/checkbox/_checkbox-theme.scss b/src/material/checkbox/_checkbox-theme.scss index b46761feeac5..b18b0b323dcc 100644 --- a/src/material/checkbox/_checkbox-theme.scss +++ b/src/material/checkbox/_checkbox-theme.scss @@ -92,8 +92,5 @@ @mixin theme-from-tokens($tokens) { // TODO(mmalerba): Some of the theme styles above are not represented in terms of tokens, // so this mixin is currently incomplete. - - .mat-mdc-checkbox { - @include mdc-checkbox-theme.theme(map.get($tokens, tokens-mdc-checkbox.$prefix)); - } + @include mdc-checkbox-theme.theme(map.get($tokens, tokens-mdc-checkbox.$prefix)); } diff --git a/src/material/core/style/_sass-utils.scss b/src/material/core/style/_sass-utils.scss index af9fb2218cef..772cfbec7b35 100644 --- a/src/material/core/style/_sass-utils.scss +++ b/src/material/core/style/_sass-utils.scss @@ -14,6 +14,18 @@ } } +/// A version of the standard `map.merge` function that takes a variable number of arguments. +/// Each argument is merged into the final result from left to right. +/// @param {List} $maps The maps to combine with map.merge +/// @return {Map} The combined result of successively calling map.merge with each parameter. +@function merge-all($maps...) { + $result: (); + @each $map in $maps { + $result: map.merge($result, $map); + } + @return $result; +} + /// A version of the standard `map.deep-merge` function that takes a variable number of arguments. /// Each argument is deep-merged into the final result from left to right. /// @param {List} $maps The maps to combine with map.deep-merge