Skip to content

Commit 0b7e656

Browse files
authored
feat(material-experimental/theming): Add support for color variants (#28279)
* feat(material-experimental/theming): Add support for color variants Adds token infrastructure to be able to support color variants for M3 components. This is accomplished by adding an additional namesapce to the token map per color variant. For example, if the mat-icon tokens live in the token map under `(mat, icon): (token-values...)`, and we want to support a primary color variant, we will add a new namespace `(mat, icon, primary): (token-value-overrides...)`. When applying a color or theme mixin for an M3 theme, users can specify the desired variant as an extra parameter to the mixin: `@include mat.icon-theme($theme, $color-variant: primary)`. The mixin will then merge any overrides from the variant namespace into the default values from the standard namespace to determine the final value for each CSS variable. Note that the user must provide this a keyword argument, not a positional argument. This makes the API easier to maintain should we need to add other options later. In addition to adding the necessary infrastructure for color variants, this PR also builds out the color variant supprot for mat-icon to prove out the infrstructure and serve as an example. * docs: Update sass doc comments based on feedback
1 parent 58d4958 commit 0b7e656

File tree

6 files changed

+162
-19
lines changed

6 files changed

+162
-19
lines changed

src/dev-app/theme-m3.scss

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22
@use '@angular/material' as mat;
33
@use '@angular/material-experimental' as matx;
44

5+
// TODO(mmalerba): Consider adding this as a back-compat API for users who want it, rather than just
6+
// a demo thing.
7+
@mixin color-variants-back-compat($theme) {
8+
.mat-primary {
9+
&.mat-icon { @include mat.icon-color($theme, $color-variant: primary); }
10+
}
11+
12+
.mat-accent {
13+
&.mat-icon { @include mat.icon-color($theme, $color-variant: secondary); }
14+
}
15+
16+
.mat-warn {
17+
&.mat-icon { @include mat.icon-color($theme, $color-variant: error); }
18+
}
19+
}
20+
521
// Add an indicator to make it clear that the app is styled with the experimental M3 theme.
622
dev-app {
723
&::before {
@@ -79,6 +95,8 @@ html {
7995
@include mat.tree-theme($light-theme);
8096
}
8197

98+
@include color-variants-back-compat($light-theme);
99+
82100
// Emit dark theme styles.
83101
.demo-unicorn-dark-theme {
84102
// TODO(mmalerba): choose colors from the theming API.
@@ -124,6 +142,8 @@ html {
124142
@include mat.toolbar-color($dark-theme);
125143
@include mat.tooltip-color($dark-theme);
126144
@include mat.tree-color($dark-theme);
145+
146+
@include color-variants-back-compat($dark-theme);
127147
}
128148

129149
// Emit density styles for each scale.

src/material-experimental/theming/_custom-tokens.scss

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,23 @@
334334
/// @param {Boolean} $exclude-hardcoded Whether to exclude hardcoded token values
335335
/// @return {Map} A set of custom tokens for the mat-icon
336336
@function icon($systems, $exclude-hardcoded) {
337-
@return (
337+
@return ((
338338
color: _hardcode(inherit, $exclude-hardcoded),
339-
);
339+
), (
340+
// Color variants:
341+
primary: (
342+
color: map.get($systems, md-sys-color, primary),
343+
),
344+
secondary: (
345+
color: map.get($systems, md-sys-color, secondary),
346+
),
347+
tertiary: (
348+
color: map.get($systems, md-sys-color, tertiary),
349+
),
350+
error: (
351+
color: map.get($systems, md-sys-color, error),
352+
)
353+
));
340354
}
341355

342356
/// Generates custom tokens for the mat-button.

src/material-experimental/theming/_m3-tokens.scss

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
@use 'sass:map';
21
@use 'sass:list';
2+
@use 'sass:map';
33
@use 'sass:meta';
44
@use '@angular/material' as mat;
55
@use '@material/tokens/v0_161' as mdc-tokens;
@@ -51,15 +51,35 @@
5151

5252
/// Gets the MDC tokens for the given prefix, M3 token values, and supported token slots.
5353
/// @param {List} $prefix The token prefix for the given tokens.
54-
/// @param {Map} $m3-values A map of M3 token values for the given prefix.
54+
/// @param {Map|(Map, Map)} $m3-values A map of M3 token values for the given prefix.
55+
/// This param may also be a tuple of maps, the first one representing the default M3 token values,
56+
// and the second containing overrides for different color variants.
57+
// Single map example:
58+
// (token1: green, token2: 2px)
59+
// Tuple example:
60+
// (
61+
// (token1: green, token2: 2px),
62+
// (
63+
// secondary: (token1: blue),
64+
// error: (token1: red),
65+
// )
66+
// )
5567
/// @param {Map} $slots A map of token slots, with null value indicating the token is not supported.
68+
/// @param {String|null} $variant The name of the variant the token values are for.
5669
/// @return {Map} A map of fully qualified token names to values, for only the supported tokens.
57-
@function _namespace-tokens($prefix, $m3-values, $slots) {
70+
@function _namespace-tokens($prefix, $m3-values, $slots, $variant: null) {
71+
$result: ();
72+
@if $variant == null and meta.type-of($m3-values) == 'list' and list.length($m3-values == 2) {
73+
$variants: list.nth($m3-values, 2);
74+
$m3-values: list.nth($m3-values, 1);
75+
@each $variant, $overrides in $variants {
76+
$result: map.merge($result, _namespace-tokens($prefix, $overrides, $slots, $variant));
77+
}
78+
}
5879
$used-token-names: map.keys(_filter-nulls(map.get($slots, $prefix)));
5980
$used-m3-tokens: _pick(_filter-nulls($m3-values), $used-token-names);
60-
@return (
61-
$prefix: $used-m3-tokens,
62-
);
81+
$prefix: if($variant == null, $prefix, list.append($prefix, $variant));
82+
@return map.merge($result, ($prefix: $used-m3-tokens));
6383
}
6484

6585
/// Generates tokens for the given palette with the given prefix.
@@ -394,7 +414,7 @@
394414
$token-slots: mat.m2-tokens-from-theme($fake-theme);
395415

396416
// TODO(mmalerba): Fill in remaining tokens.
397-
$result: mat.private-merge-all(
417+
$result: mat.private-deep-merge-all(
398418
// Add the system color & typography tokens (so we can give users access via an API).
399419
(
400420
(mdc, theme): map.get($systems, md-sys-color),

src/material/core/style/_sass-utils.scss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@use 'sass:color';
2+
@use 'sass:list';
23
@use 'sass:map';
34
@use 'sass:meta';
45

@@ -59,3 +60,21 @@
5960
$args: meta.keywords($args);
6061
@return if(meta.type-of($color) == 'color', color.change($color, $args...), $color);
6162
}
63+
64+
/// Gets the given arguments as a map of keywords and validates that only supported arguments were
65+
/// passed.
66+
/// @param {ArgList} $args The arguments to convert to a keywords map.
67+
/// @param {List} $supported-args The supported argument names.
68+
/// @return {Map} The $args as a map of argument name to argument value.
69+
@function validate-keyword-args($args, $supported-args) {
70+
@if list.length($args) > 0 {
71+
@error #{'Expected keyword args, but got positional args: '}#{$args};
72+
}
73+
$kwargs: meta.keywords($args);
74+
@each $arg, $v in $kwargs {
75+
@if list.index($supported-args, $arg) == null {
76+
@error #{'Unsupported argument '}#{$arg}#{'. Valid arguments are: '}#{$supported-args};
77+
}
78+
}
79+
@return $kwargs;
80+
}

src/material/core/tokens/_token-utils.scss

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
@use 'sass:list';
12
@use 'sass:map';
23
@use '@material/elevation/elevation-theme' as mdc-elevation-theme;
34
@use '@material/theme/custom-properties' as mdc-custom-properties;
45
@use '@material/theme/theme' as mdc-theme;
56
@use '@material/theme/keys' as mdc-keys;
7+
@use '../style/sass-utils';
68
@use '../theming/palette';
79
@use '../theming/theming';
810
@use '../typography/typography';
@@ -125,3 +127,57 @@ $_component-prefix: null;
125127
$shadow-color-token: null,
126128
));
127129
}
130+
131+
/// Checks whether a list starts wih a given prefix
132+
/// @param {List} $list The list value to check the prefix of.
133+
/// @param {List} $prefix The prefix to check.
134+
/// @return {Boolean} Whether the list starts with the prefix.
135+
@function _is-prefix($list, $prefix) {
136+
@for $i from 1 through list.length($prefix) {
137+
@if list.nth($list, $i) != list.nth($prefix, $i) {
138+
@return false;
139+
}
140+
}
141+
@return true;
142+
}
143+
144+
/// Gets the supported color variants in the given token set for the given prefix.
145+
/// @param {Map} $tokens The full token map.
146+
/// @param {List} $prefix The component prefix to get color variants for.
147+
/// @return {List} The supported color variants.
148+
@function _supported-color-variants($tokens, $prefix) {
149+
$result: ();
150+
@each $namespace in map.keys($tokens) {
151+
@if list.length($prefix) == list.length($namespace) - 1 and _is-prefix($namespace, $prefix) {
152+
$result: list.append($result, list.nth($namespace, list.length($namespace)), comma);
153+
}
154+
}
155+
@return $result;
156+
}
157+
158+
/// Gets the token values for the given components prefix with the given options.
159+
/// @param {Map} $tokens The full token map.
160+
/// @param {List} $prefix The component prefix to get the token values for.
161+
/// @param {ArgList} Any additional options
162+
/// Currently the additional supported options are:
163+
// - $color-variant (The color variant to use for the component)
164+
/// @throws If given options are invalid
165+
/// @return {Map} The token values for the requested component.
166+
@function get-tokens-for($tokens, $prefix, $options...) {
167+
$options: sass-utils.validate-keyword-args($options, (color-variant));
168+
@if $tokens == () {
169+
@return ();
170+
}
171+
$values: map.get($tokens, $prefix);
172+
$color-variant: map.get($options, color-variant);
173+
@if $color-variant == null {
174+
@return $values;
175+
}
176+
$overrides: map.get($tokens, list.append($prefix, $color-variant));
177+
@if $overrides == null {
178+
@error #{'Invalid color variant: '}#{$color-variant}#{'. Supported color variants are: '}#{
179+
_supported-color-variants($tokens, $prefix)
180+
}#{'.'};
181+
}
182+
@return map.merge($values, $overrides);
183+
}

src/material/icon/_icon-theme.scss

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
@use 'sass:map';
21
@use '../core/theming/theming';
32
@use '../core/theming/inspection';
43
@use '../core/tokens/m2/mat/icon' as tokens-mat-icon;
@@ -11,16 +10,24 @@
1110
@include token-utils.create-token-values(tokens-mat-icon.$prefix, $tokens);
1211
}
1312

13+
/// Outputs base theme styles (styles not dependent on the color, typography, or density settings)
14+
/// for the mat-icon.
15+
/// @param {Map} $theme The theme to generate base styles for.
1416
@mixin base($theme) {
1517
@if inspection.get-theme-version($theme) == 1 {
1618
@include _theme-from-tokens(inspection.get-theme-tokens($theme, base));
1719
}
1820
@else {}
1921
}
2022

21-
@mixin color($theme) {
23+
/// Outputs color theme styles for the mat-icon.
24+
/// @param {Map} $theme The theme to generate color styles for.
25+
/// @param {ArgList} Additional optional arguments (only supported for M3 themes):
26+
/// $color-variant: The color variant to use for the icon: primary, secondary, tertiary, or error
27+
/// (If not specified, default neutral color will be used).
28+
@mixin color($theme, $options...) {
2229
@if inspection.get-theme-version($theme) == 1 {
23-
@include _theme-from-tokens(inspection.get-theme-tokens($theme, color));
30+
@include _theme-from-tokens(inspection.get-theme-tokens($theme, color), $options...);
2431
}
2532
@else {
2633
@include sass-utils.current-selector-or-root() {
@@ -44,24 +51,33 @@
4451
}
4552
}
4653

54+
/// Outputs typography theme styles for the mat-icon.
55+
/// @param {Map} $theme The theme to generate typography styles for.
4756
@mixin typography($theme) {
4857
@if inspection.get-theme-version($theme) == 1 {
4958
@include _theme-from-tokens(inspection.get-theme-tokens($theme, typography));
5059
}
5160
@else {}
5261
}
5362

63+
/// Outputs density theme styles for the mat-icon.
64+
/// @param {Map} $theme The theme to generate density styles for.
5465
@mixin density($theme) {
5566
@if inspection.get-theme-version($theme) == 1 {
5667
@include _theme-from-tokens(inspection.get-theme-tokens($theme, density));
5768
}
5869
@else {}
5970
}
6071

61-
@mixin theme($theme) {
72+
/// Outputs all (base, color, typography, and density) theme styles for the mat-icon.
73+
/// @param {Map} $theme The theme to generate styles for.
74+
/// @param {ArgList} Additional optional arguments (only supported for M3 themes):
75+
/// $color-variant: The color variant to use for the icon: primary, secondary, tertiary, or error
76+
/// (If not specified, default neutral color will be used).
77+
@mixin theme($theme, $options...) {
6278
@include theming.private-check-duplicate-theme-styles($theme, 'mat-icon') {
6379
@if inspection.get-theme-version($theme) == 1 {
64-
@include _theme-from-tokens(inspection.get-theme-tokens($theme));
80+
@include _theme-from-tokens(inspection.get-theme-tokens($theme), $options...);
6581
}
6682
@else {
6783
@include base($theme);
@@ -78,9 +94,7 @@
7894
}
7995
}
8096

81-
@mixin _theme-from-tokens($tokens) {
82-
@if ($tokens != ()) {
83-
@include token-utils.create-token-values(
84-
tokens-mat-icon.$prefix, map.get($tokens, tokens-mat-icon.$prefix));
85-
}
97+
@mixin _theme-from-tokens($tokens, $options...) {
98+
$mat-icon-tokens: token-utils.get-tokens-for($tokens, tokens-mat-icon.$prefix, $options...);
99+
@include token-utils.create-token-values(tokens-mat-icon.$prefix, $mat-icon-tokens);
86100
}

0 commit comments

Comments
 (0)