Skip to content

Commit ce8ea53

Browse files
committed
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
1 parent aec23ac commit ce8ea53

File tree

9 files changed

+309
-35
lines changed

9 files changed

+309
-35
lines changed

src/dev-app/theme-token-api.scss

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,7 @@ html {
6060
$tokens: mat.m2-tokens-from-theme($dark-theme),
6161
$components: (
6262
matx.checkbox((
63-
(mdc, checkbox): (
64-
selected-checkmark-color: red,
65-
)
63+
checkmark-color: red,
6664
)),
6765
));
6866
}

src/material-experimental/_index.scss

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
popover-edit-typography, popover-edit-density, popover-edit-theme;
66

77
// Token-based theming API
8-
@forward './theming/theming' show theme, card, checkbox;
8+
@forward './theming/theming' show theme, retheme;
9+
@forward './theming/checkbox' show checkbox;
10+
@forward './theming/card' show card;
911

1012
// Additional public APIs for individual components
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
@use 'sass:color';
2+
@use 'sass:meta';
3+
@use '@angular/material' as mat;
4+
@use './token-resolution';
5+
6+
// TODO(mmalerba): This should live under material/card when moving out of experimental.
7+
8+
/// Gets tokens for setting the card's shape.
9+
/// @param {String} $shape The card's shape.
10+
/// @return {Map} A map of tokens for setting the card's shape.
11+
// Note: we use a function rather than simple rename, because we want to map a single shape value to
12+
// multiple tokens, rather than offer separate shape customizations for elevated and outlined cards.
13+
@function _get-tokens-for-card-shape($shape) {
14+
@return (
15+
(mdc, elevated-card): (container-shape: $shape),
16+
(mdc, outline-card): (container-shape: $shape),
17+
);
18+
}
19+
20+
/// Gets tokens for setting the card's color.
21+
/// @param {String} $shape The card's shape.
22+
/// @return {Map} A map of tokens for setting the card's shape.
23+
@function _get-tokens-for-card-color($color) {
24+
@return (
25+
(mdc, elevated-card): (container-color: $color),
26+
(mdc, outline-card): (container-color: $color),
27+
);
28+
}
29+
30+
/// Gets a map of card token values that are derived from the theme type.
31+
/// @param {'light' | 'dark'} $theme-type The type of theme.
32+
/// @return {Map} A map of card token values derived from the given theme type.
33+
@function _get-tokens-for-theme-type($theme-type) {
34+
$is-dark: $theme-type == 'dark';
35+
$foreground: if($is-dark, white, black);
36+
$card-color: if($is-dark, mat.get-color-from-palette(mat.$gray-palette, 800), white);
37+
$outline-color: color.change($foreground, $alpha: 0.12);
38+
$subtitle-color: if($is-dark, rgba(white, 0.7), rgba(black, 0.54));
39+
40+
@return (
41+
(mdc, elevated-card): (
42+
container-color: $card-color,
43+
),
44+
(mdc, outlined-card): (
45+
container-color: $card-color,
46+
outline-color: $outline-color,
47+
),
48+
(mat, card): (
49+
subtitle-text-color: $subtitle-color,
50+
),
51+
);
52+
}
53+
54+
/// Resolvers for mat-card customizations.
55+
$_customization-resolvers: mat.private-merge-all(
56+
token-resolution.alias((
57+
elevation: container-elevation,
58+
shadow-color: container-shadow-color,
59+
), (mdc, elevated-card)),
60+
token-resolution.forward((
61+
outline-width,
62+
outline-color
63+
), (mdc, outlined-card)),
64+
token-resolution.alias((
65+
title-font: title-text-font,
66+
title-line-height: title-text-line-height,
67+
title-font-size: title-text-size,
68+
title-letter-spacing: title-text-tracking,
69+
title-font-weight: title-text-weight,
70+
subtitle-font: subtitle-text-font,
71+
subtitle-line-height: subtitle-text-line-height,
72+
subtitle-font-size: subtitle-text-size,
73+
subtitle-letter-spacing: subtitle-text-tracking,
74+
subtitle-font-weight: subtitle-text-weight,
75+
subtitle-color: subtitle-text-color
76+
), (mat, card)),
77+
(
78+
background-color: meta.get-function(_get-tokens-for-card-color),
79+
border-radius: meta.get-function(_get-tokens-for-card-shape),
80+
theme-type: meta.get-function(_get-tokens-for-theme-type),
81+
)
82+
);
83+
84+
/// Configure the mat-card's theme.
85+
/// @param {Map} $customizations [()] A map of custom values to use when theming mat-card.
86+
@function card($customizations: ()) {
87+
@return (
88+
id: 'mat.card',
89+
customizations: token-resolution.resolve-customized-tokens(
90+
'mat.card', $_customization-resolvers, $customizations),
91+
deps: (),
92+
);
93+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
@use 'sass:color';
2+
@use 'sass:map';
3+
@use 'sass:meta';
4+
@use '@angular/material' as mat;
5+
@use '@material/theme/theme-color' as mdc-theme-color;
6+
@use './token-resolution';
7+
8+
// TODO(mmalerba): This should live under material/checkbox when moving out of experimental.
9+
10+
// Duplicated from core/tokens/m2/mdc/checkbox
11+
// TODO(mmalerba): Delete duplicated code when this is moved out of experimental.
12+
@function _contrast-tone($value, $light-color: '#fff', $dark-color: '#000') {
13+
@if ($value == 'dark' or $value == 'light' or type-of($value) == 'color') {
14+
@return if(mdc-theme-color.contrast-tone($value) == 'dark', $dark-color, $light-color);
15+
}
16+
@return false
17+
}
18+
19+
/// Gets a map of checkbox token values that are derived from the given palette.
20+
/// @param {Map} $palette An Angular Material palette object.
21+
/// @return {Map} A map of checkbox token values derived from the given palette.
22+
@function _get-tokens-for-color-palette($palette) {
23+
$palette-default-color: mat.get-color-from-palette($palette);
24+
$checkmark-color: _contrast-tone($palette-default-color);
25+
26+
@return (
27+
(mdc, checkbox): (
28+
selected-checkmark-color: $checkmark-color,
29+
selected-focus-icon-color: $palette-default-color,
30+
selected-hover-icon-color: $palette-default-color,
31+
selected-icon-color: $palette-default-color,
32+
selected-pressed-icon-color: $palette-default-color,
33+
selected-focus-state-layer-color: $palette-default-color,
34+
selected-hover-state-layer-color: $palette-default-color,
35+
selected-pressed-state-layer-color: $palette-default-color,
36+
)
37+
);
38+
}
39+
40+
/// Gets a map of checkbox token values that are derived from the theme type.
41+
/// @param {'light' | 'dark'} $theme-type The type of theme.
42+
/// @return {Map} A map of checkbox token values derived from the given theme type.
43+
@function _get-tokens-for-theme-type($theme-type) {
44+
$is-dark: $theme-type == dark;
45+
$foreground: if($is-dark, white, black);
46+
$disabled-color: color.change($foreground, $alpha: 0.38);
47+
$border-color: color.change($foreground, $alpha: 0.54);
48+
$active-border-color: mat.get-color-from-palette(mat.$gray-palette, if($is-dark, 200, 900));
49+
50+
@return (
51+
(mdc, checkbox): (
52+
disabled-selected-icon-color: $disabled-color,
53+
disabled-unselected-icon-color: $disabled-color,
54+
unselected-focus-icon-color: $active-border-color,
55+
unselected-hover-icon-color: $active-border-color,
56+
unselected-icon-color: $border-color,
57+
unselected-pressed-icon-color: $border-color,
58+
unselected-focus-state-layer-color: $foreground,
59+
unselected-hover-state-layer-color: $foreground,
60+
unselected-pressed-state-layer-color: $foreground,
61+
)
62+
);
63+
}
64+
65+
/// Resolvers for mat-checkbox customizations.
66+
$_customization-resolvers: map.merge(
67+
token-resolution.alias((
68+
checkmark-color: selected-checkmark-color,
69+
disabled-checkmark-color: disabled-selected-checkmark-color,
70+
selected-focus-ring-opacity: selected-focus-state-layer-opacity,
71+
selected-hover-ring-opacity: selected-hover-state-layer-opacity,
72+
selected-pressed-ring-opacity: selected-pressed-state-layer-opacity,
73+
unselected-focus-ring-opacity: unselected-focus-state-layer-opacity,
74+
unselected-hover-ring-opacity: unselected-hover-state-layer-opacity,
75+
unselected-pressed-ring-opacity: unselected-pressed-state-layer-opacity,
76+
disabled-selected-box-color: disabled-selected-icon-color,
77+
disabled-unselected-box-color: disabled-unselected-icon-color,
78+
selected-focus-box-color: selected-focus-icon-color,
79+
selected-hover-box-color: selected-hover-icon-color,
80+
selected-icon-color: selected-box-color,
81+
selected-pressed-box-color: selected-pressed-icon-color,
82+
unselected-focus-box-color: unselected-focus-icon-color,
83+
unselected-hover-box-color: unselected-hover-icon-color,
84+
unselected-box-color: unselected-icon-color,
85+
unselected-pressed-box-color: unselected-pressed-icon-color,
86+
selected-focus-ring-color: selected-focus-state-layer-color,
87+
selected-hover-ring-color: selected-hover-state-layer-color,
88+
selected-pressed-ring-color: selected-pressed-state-layer-color,
89+
unselected-focus-ring-color: unselected-focus-state-layer-color,
90+
unselected-hover-ring-color: unselected-hover-state-layer-color,
91+
unselected-pressed-ring-color: unselected-pressed-state-layer-color,
92+
ripple-size: state-layer-size,
93+
), (mdc, checkbox)),
94+
(
95+
color-palette: meta.get-function(_get-tokens-for-color-palette),
96+
theme-type: meta.get-function(_get-tokens-for-theme-type),
97+
)
98+
);
99+
100+
/// Configure the mat-checkbox's theme.
101+
/// @param {Map} $customizations [()] A map of custom values to use when theming mat-checkbox.
102+
@function checkbox($customizations: ()) {
103+
@return (
104+
id: 'mat.checkbox',
105+
customizations: token-resolution.resolve-customized-tokens(
106+
'mat.checkbox', $_customization-resolvers, $customizations),
107+
deps: (),
108+
);
109+
}

src/material-experimental/theming/_theming.scss

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -112,23 +112,3 @@ $_error-on-missing-dep: false;
112112
@mixin retheme($components) {
113113
@include _theme((), $components);
114114
}
115-
116-
/// Configure the mat-card's theme.
117-
/// @param {Map} $customizations [()] A map of custom token values to use when theming mat-card.
118-
@function card($customizations: ()) {
119-
@return (
120-
id: 'mat.card',
121-
customizations: $customizations,
122-
deps: (),
123-
);
124-
}
125-
126-
/// Configure the mat-checkbox's theme.
127-
/// @param {Map} $customizations [()] A map of custom token values to use when theming mat-checkbox.
128-
@function checkbox($customizations: ()) {
129-
@return (
130-
id: 'mat.checkbox',
131-
customizations: $customizations,
132-
deps: (),
133-
);
134-
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
@use 'sass:list';
2+
@use 'sass:map';
3+
@use 'sass:meta';
4+
5+
/// Creates a map of short token names to fully qualified token name under the given namespace.
6+
/// @param {List} $tokens A list of tokens to forward under the given namespace.
7+
/// @param {List} $namespace The namespace to use for the forwarded tokens.
8+
/// @return {Map} A map of the short token name to pairs of (namespace, token-name) representing the
9+
/// fully-qualified name
10+
/// @example
11+
/// forward((token1, token2), (mat, my-comp))
12+
/// => (
13+
/// token1: ((mat, my-comp), token1),
14+
/// token2: ((mat, my-comp), token1)
15+
/// )
16+
@function forward($tokens, $namespace) {
17+
$result: ();
18+
@each $token in $tokens {
19+
$result: map.set($result, $token, ($namespace, $token));
20+
}
21+
@return $result;
22+
}
23+
24+
// Creates a map of token alias names to fully qualified canonical names under the given namespace.
25+
/// @param {Map} $tokens A map of aliases to canonical short names for tokens under the given
26+
/// namespace.
27+
/// @param {List} $namespace The namespace to use for the canonical tokens.
28+
/// @return A map of the token alias name to pairs of (namespace, token-name) representing the
29+
/// fully-qualified canonical name of the token.
30+
/// @example
31+
/// alias((alias1: canonical1, alias2: canonical2), (mat, my-comp))
32+
/// => (
33+
/// alias1: ((mat, my-comp), canonical1),
34+
/// alias2: ((mat, my-comp), canonical2)
35+
/// )
36+
@function alias($tokens, $namespace) {
37+
$result: ();
38+
@each $from, $to in $tokens {
39+
$result: map.set($result, $from, ($namespace, $to));
40+
}
41+
@return $result;
42+
}
43+
44+
/// Gets the full set of customized tokens from a component configuration's customization map.
45+
/// @param {String} $component-id The id of the component whose customizations are being resolved.
46+
/// Used for error logging purposes.
47+
/// @param {Map} $customization-resolvers A map of resolvers that map customization keys to
48+
/// fully-qualified token names or functions to generate fully-qualified token names.
49+
/// @param {Map} $customizations A map of values for customization keys
50+
/// @return {Map} A map of fully-qualified token values
51+
/// @example
52+
/// resolve-customized-tokens('mat.checkbox',
53+
/// forward(my-color, my-size, (mat, my-comp)),
54+
/// (my-color: red, my-size: 100px)
55+
/// )
56+
/// => (
57+
/// (mat, my-comp): (
58+
/// my-color: red,
59+
/// my-size: 100px
60+
/// )
61+
/// )
62+
@function resolve-customized-tokens($component-id, $customization-resolvers, $customizations) {
63+
$result: ();
64+
65+
@each $customization, $value in $customizations {
66+
$resolver: map.get($customization-resolvers, $customization);
67+
@if not $resolver {
68+
@error 'Unrecognized customization for #{$component-id}: #{$customization}';
69+
}
70+
71+
$resolver-type: meta.type-of($resolver);
72+
@if $resolver-type == 'list' {
73+
// If the resolver is a list, it represents the token namespace and name.
74+
$key-and-value: list.append($resolver, $value);
75+
$result: map.deep-merge($result, map.set((), $key-and-value...));
76+
} @else if $resolver-type == 'function' {
77+
// If the resolver is a function, it should take a value and return a token map.
78+
$result: map.deep-merge($result, meta.call($resolver, $value))
79+
} @else {
80+
// Anything else is unexpected.
81+
@error 'Invalid customization resolver for `#{$customization}` on #{$component-id}';
82+
}
83+
}
84+
85+
@return $result;
86+
}

src/material/card/_card-theme.scss

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,8 @@
7979
}
8080

8181
@mixin theme-from-tokens($tokens) {
82-
// Add values for card tokens.
83-
.mat-mdc-card {
84-
@include mdc-elevated-card-theme.theme(map.get($tokens, tokens-mdc-elevated-card.$prefix));
85-
@include mdc-outlined-card-theme.theme(map.get($tokens, tokens-mdc-outlined-card.$prefix));
86-
@include token-utils.create-token-values(
87-
tokens-mat-card.$prefix, map.get($tokens, tokens-mat-card.$prefix));
88-
}
82+
@include mdc-elevated-card-theme.theme(map.get($tokens, tokens-mdc-elevated-card.$prefix));
83+
@include mdc-outlined-card-theme.theme(map.get($tokens, tokens-mdc-outlined-card.$prefix));
84+
@include token-utils.create-token-values(
85+
tokens-mat-card.$prefix, map.get($tokens, tokens-mat-card.$prefix));
8986
}

src/material/checkbox/_checkbox-theme.scss

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,5 @@
9292
@mixin theme-from-tokens($tokens) {
9393
// TODO(mmalerba): Some of the theme styles above are not represented in terms of tokens,
9494
// so this mixin is currently incomplete.
95-
96-
.mat-mdc-checkbox {
97-
@include mdc-checkbox-theme.theme(map.get($tokens, tokens-mdc-checkbox.$prefix));
98-
}
95+
@include mdc-checkbox-theme.theme(map.get($tokens, tokens-mdc-checkbox.$prefix));
9996
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@
1414
}
1515
}
1616

17+
/// A version of the standard `map.merge` function that takes a variable number of arguments.
18+
/// Each argument is merged into the final result from left to right.
19+
/// @param {List} $maps The maps to combine with map.merge
20+
/// @return {Map} The combined result of successively calling map.merge with each parameter.
21+
@function merge-all($maps...) {
22+
$result: ();
23+
@each $map in $maps {
24+
$result: map.merge($result, $map);
25+
}
26+
@return $result;
27+
}
28+
1729
/// A version of the standard `map.deep-merge` function that takes a variable number of arguments.
1830
/// Each argument is deep-merged into the final result from left to right.
1931
/// @param {List} $maps The maps to combine with map.deep-merge

0 commit comments

Comments
 (0)