Skip to content

Commit 227a741

Browse files
authored
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
1 parent cddb04f commit 227a741

File tree

11 files changed

+449
-58
lines changed

11 files changed

+449
-58
lines changed

src/dev-app/checkbox/checkbox-demo.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,9 @@ <h5>No animations</h5>
279279
</mat-checkbox>
280280
</div>
281281
</div>
282+
283+
<p>
284+
<mat-checkbox class="demo-traffic-light-checkbox">
285+
This checkbox has special styling when using the experimental token based theme
286+
</mat-checkbox>
287+
</p>

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

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@use 'sass:map';
12
@use '@angular/material' as mat;
23
@use '@angular/material-experimental' as matx;
34

@@ -23,7 +24,7 @@ dev-app {
2324

2425
@include mat.core();
2526

26-
$light-theme: mat.define-light-theme((
27+
$theme: mat.define-light-theme((
2728
color: (
2829
primary: mat.define-palette(mat.$indigo-palette),
2930
accent: mat.define-palette(mat.$pink-palette),
@@ -32,37 +33,82 @@ $light-theme: mat.define-light-theme((
3233
density: 0,
3334
));
3435

35-
$dark-theme: mat.define-dark-theme((
36-
color: (
37-
primary: mat.define-palette(mat.$blue-grey-palette),
38-
accent: mat.define-palette(mat.$amber-palette, A200, A100, A400),
39-
warn: mat.define-palette(mat.$deep-orange-palette),
40-
),
41-
typography: mat.define-typography-config(),
42-
density: 0,
43-
));
44-
45-
// Set up light theme.
46-
36+
// Apply all tokens (derived from `$theme`) to the `html` element. This ensures that all components
37+
// on the page will inherit these tokens.
4738
html {
4839
@include matx.theme(
49-
$tokens: mat.m2-tokens-from-theme($light-theme),
40+
$tokens: mat.m2-tokens-from-theme($theme),
5041
$components: (
5142
matx.card(),
5243
matx.checkbox(),
53-
));
44+
)
45+
);
5446
}
5547

56-
// Set up dark theme.
5748

49+
// Apply tokens needed for dark theme to the element with `.demo-unicorn-dark-theme`.
50+
// This ensures that checkboxes within the element inherit the new tokens for dark theme,
51+
// rather than the ones for light theme tokens set on `body`. Note that we're not setting *all* of
52+
// the tokens, since many (density, typography, etc) are the same between light and dark theme.
5853
.demo-unicorn-dark-theme {
59-
@include matx.theme(
60-
$tokens: mat.m2-tokens-from-theme($dark-theme),
61-
$components: (
62-
matx.checkbox((
63-
(mdc, checkbox): (
64-
selected-checkmark-color: red,
65-
)
66-
)),
67-
));
54+
@include matx.retheme((
55+
// TODO(mmalerba): In the future this should be configured through `matx.system-colors()`
56+
matx.checkbox((theme-type: dark)),
57+
matx.card((theme-type: dark)),
58+
));
59+
}
60+
61+
// Apply tokens related to the color palette to any element with `.mat-primary`, `.mat-accent`, or
62+
// `.mat-warn` This ensures that checkboxes within the element inherit the new tokens for the
63+
// appropriate palette, rather than the any color that may have been set on an element further up
64+
// the hierarchy. Again, rather than applying *all* the tokens, we apply only the ones effected by
65+
// the palette color. With this setup, the palette class need not go on the component itself
66+
// (e.g. <mat-checkbox class="mat-primary">), it can go on some ancestor element and the tokens will
67+
// flow down. If multiple elements specify different classes, the closest one to the component will
68+
// take precedence.
69+
// (e.g. <div class="mat-warn><mat-checkbox class="mat-primary">I'm primary</mat-checkbox></div>)
70+
.mat-primary {
71+
@include matx.retheme((
72+
matx.checkbox((
73+
color-palette: map.get($theme, color, primary)
74+
)),
75+
));
76+
}
77+
.mat-accent {
78+
@include matx.retheme((
79+
matx.checkbox((
80+
color-palette: map.get($theme, color, accent)
81+
)),
82+
));
83+
}
84+
.mat-warn {
85+
@include matx.retheme((
86+
matx.checkbox((
87+
color-palette: map.get($theme, color, warn)
88+
)),
89+
));
90+
}
91+
92+
// Apply tokens for a completely custom checkbox that appears as an unfilled red box when unchecked,
93+
// and a filled green box when checked.
94+
.demo-traffic-light-checkbox {
95+
@include matx.retheme((
96+
matx.checkbox((
97+
checkmark-color: transparent,
98+
selected-box-color: green,
99+
selected-focus-box-color: green,
100+
selected-hover-box-color: green,
101+
selected-pressed-box-color: green,
102+
selected-focus-ring-color: green,
103+
selected-hover-ring-color: green,
104+
selected-pressed-ring-color: green,
105+
unselected-box-color: red,
106+
unselected-focus-box-color: red,
107+
unselected-hover-box-color: red,
108+
unselected-pressed-box-color: red,
109+
unselected-focus-ring-color: red,
110+
unselected-hover-ring-color: red,
111+
unselected-pressed-ring-color: red,
112+
))
113+
));
68114
}

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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
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,
2+
and still being evaluated.
3+
4+
## Design tokens
5+
- 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.
6+
- Angular Material represents design tokens as CSS variables
7+
8+
## M2 vs M3 tokens
9+
- 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
10+
- Token values for M2 can be obtained by:
11+
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.
12+
- Token values for M3 are not yet available
13+
14+
Example:
15+
```scss
16+
// Create an Angular Material theme.
17+
$my-theme: mat.define-light-theme(...);
18+
19+
// Create tokens for M2 from the theme.
20+
$m2-tokens: mat.m2-tokens-from-theme($my-theme);
21+
```
22+
## Component theme configuration functions
23+
- 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
24+
- So far the following component theme configuration functions have been implements:
25+
- `matx.checkbox` configures tokens for the mat-checkbox to be applied
26+
- `matx.card` configures tokens for the mat-card to be applied
27+
- The returned configurations from these functions are passed to `matx.theme` or `matx.retheme`
28+
- 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
29+
- The functions can also accept a map of customizations as an argument.
30+
- Each function has its own set of supported map keys that can be used to customize the value of the underlying tokens
31+
- 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
32+
- For supported map keys (TODO: have docs for these):
33+
- See `$_customization-resolvers` [here](https://github.com/angular/components/blob/main/src/material-experimental/theming/_checkbox.scss) for `matx.checkbox`
34+
- See `$_customization-resolvers` [here](https://github.com/angular/components/blob/main/src/material-experimental/theming/_card.scss) for `matx.card`
35+
36+
## Theming mixins
37+
- There are 2 mixins used for theming apps
38+
- `matx.theme` is intended to apply the full theme for some components, with all tokens they need to function.
39+
- `matx.retheme` is intended to re-apply specific tokens to change the appearance for some components by overriding the tokens applied by `matx.theme`.
40+
- Both mixins emit *only* CSS variables representing design tokens
41+
- Both mixins emit their tokens directly under the user specified selector. This gives the user complete control over the selector specificity.
42+
- Using `matx.theme`
43+
- Takes 2 arguments:
44+
- `$tokens` The set of token defaults that will be used for any tokens not explicitly customized by the component theme config
45+
- `$components` List of component theme configs indicating which components to emit tokens for, and optionally, customizations for some token values
46+
- Outputs *all* tokens used by the configured components
47+
- Using `matx.retheme`
48+
- Takes 1 argument:
49+
- `$components` List of component theme configs to emit customized token values for
50+
- Outputs *only* the explicitly customized tokens, not any of the other tokens used by the component
51+
52+
## Recommended theming structure
53+
- Apply the base token values using `matx.theme` *once*
54+
- Choose selectors with minimal specificity when applying tokens
55+
- Prefer to rely on CSS inheritance to apply token overrides rather than specificity.
56+
For example if checkbox tokens are set on the root element (`html`) they will be inherited down
57+
the DOM and affect any `<mat-checkbox>` within the document. If checkboxes in a specific section
58+
need to appear differently, say within `.dark-sidebar`, set the token overrides on the
59+
`.dark-sidebar` element and they will be inherited down to the checkboxes within, instead of the
60+
values from the root element.
61+
- 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
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+
}

0 commit comments

Comments
 (0)