Skip to content

Commit aec23ac

Browse files
authored
feat(material-experimental/theming): add first part of token-based theming API (#27000)
* feat(material-experimental/theming): add first part of token-based theming API * Set up a query parameter to use the token-based Sass API in the dev-app * Generate all M2 tokens from a theme object * Add mixins to emit the theme based on tokens * Add the new mat.theme API * fixup! Add the new mat.theme API * tweaks based on recent feedback and discussions * addressed more feedback * fixup! addressed more feedback
1 parent cdf2935 commit aec23ac

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+647
-286
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@
131131
# Material experimental package
132132
/src/material-experimental/* @andrewseguin
133133
/src/material-experimental/column-resize/** @andrewseguin
134-
/src/material-experimental/mdc-tooltip/** @crisbeto
135134
/src/material-experimental/menubar/** @jelbourn
136135
/src/material-experimental/popover-edit/** @andrewseguin
137136
/src/material-experimental/selection/** @andrewseguin
137+
/src/material-experimental/theming/** @mmalerba
138138

139139
# CDK experimental package
140140
/src/cdk-experimental/* @andrewseguin

.ng-dev/commit-message.mts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export const commitMessage: CommitMessageConfig = {
4040
'cdk/tree',
4141
'google-maps',
4242
'material-experimental/column-resize',
43+
'material-experimental/theming',
44+
'material-experimental/menubar',
45+
'material-experimental/popover-edit',
46+
'material-experimental/selection',
4347
'material/button',
4448
'material/card',
4549
'material/checkbox',
@@ -53,12 +57,6 @@ export const commitMessage: CommitMessageConfig = {
5357
'material/snack-bar',
5458
'material/table',
5559
'material/tabs',
56-
'material-experimental/menubar',
57-
'material-experimental/popover-edit',
58-
'material-experimental/selection',
59-
'material-moment-adapter',
60-
'material-date-fns-adapter',
61-
'material-luxon-adapter',
6260
'material/autocomplete',
6361
'material/legacy-autocomplete',
6462
'material/badge',
@@ -110,6 +108,9 @@ export const commitMessage: CommitMessageConfig = {
110108
'material/legacy-tooltip',
111109
'material/tooltip',
112110
'material/tree',
111+
'material-moment-adapter',
112+
'material-date-fns-adapter',
113+
'material-luxon-adapter',
113114
'youtube-player',
114115
],
115116
};

src/dev-app/BUILD.bazel

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ sass_binary(
137137
],
138138
)
139139

140+
sass_binary(
141+
name = "theme_token_api",
142+
src = "theme-token-api.scss",
143+
deps = [
144+
"//src/material:sass_lib",
145+
"//src/material-experimental:sass_lib",
146+
"//src/material/core:theming_scss_lib",
147+
],
148+
)
149+
140150
# Variables that are going to be inlined into the dev app index.html.
141151
filegroup(
142152
name = "variables",
@@ -154,6 +164,7 @@ filegroup(
154164
"favicon.ico",
155165
"index.html",
156166
":theme",
167+
":theme_token_api",
157168
":variables",
158169
"//src/dev-app/icon:icon_demo_assets",
159170
"@npm//:node_modules/moment/min/moment-with-locales.min.js",

src/dev-app/dev-app.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Component, ViewEncapsulation} from '@angular/core';
10-
import {RouterModule} from '@angular/router';
9+
import {Component, inject, ViewEncapsulation} from '@angular/core';
10+
import {ActivatedRoute, RouterModule} from '@angular/router';
1111
import {DevAppLayout} from './dev-app/dev-app-layout';
1212

1313
/** Root component for the dev-app demos. */
@@ -18,4 +18,14 @@ import {DevAppLayout} from './dev-app/dev-app-layout';
1818
standalone: true,
1919
imports: [DevAppLayout, RouterModule],
2020
})
21-
export class DevApp {}
21+
export class DevApp {
22+
route = inject(ActivatedRoute);
23+
24+
constructor() {
25+
this.route.queryParams.subscribe(q => {
26+
(document.querySelector('#theme-styles') as any).href = q.hasOwnProperty('tokenapi')
27+
? 'theme-token-api.css'
28+
: 'theme.css';
29+
});
30+
}
31+
}

src/dev-app/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<link rel="preconnect" href="https://fonts.gstatic.com">
1111
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
1212
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
13-
<link href="theme.css" rel="stylesheet">
13+
<link href="theme.css" rel="stylesheet" id="theme-styles">
1414

1515
<!-- FontAwesome for mat-icon demo. -->
1616
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
@use '@angular/material' as mat;
2+
@use '@angular/material-experimental' as matx;
3+
4+
dev-app {
5+
&::before {
6+
content: 'Using experimental theming API';
7+
display: inline-block;
8+
position: fixed;
9+
z-index: 100;
10+
bottom: 0;
11+
left: 50%;
12+
transform: translateX(-50%);
13+
padding: 8px;
14+
background: red;
15+
color: white;
16+
}
17+
}
18+
19+
.demo-unicorn-dark-theme {
20+
background: black;
21+
color: white;
22+
}
23+
24+
@include mat.core();
25+
26+
$light-theme: mat.define-light-theme((
27+
color: (
28+
primary: mat.define-palette(mat.$indigo-palette),
29+
accent: mat.define-palette(mat.$pink-palette),
30+
),
31+
typography: mat.define-typography-config(),
32+
density: 0,
33+
));
34+
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+
47+
html {
48+
@include matx.theme(
49+
$tokens: mat.m2-tokens-from-theme($light-theme),
50+
$components: (
51+
matx.card(),
52+
matx.checkbox(),
53+
));
54+
}
55+
56+
// Set up dark theme.
57+
58+
.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+
));
68+
}

src/material-experimental/BUILD.bazel

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ ts_library(
1919

2020
sass_library(
2121
name = "theming_scss_lib",
22-
srcs = MATERIAL_EXPERIMENTAL_SCSS_LIBS,
22+
srcs = MATERIAL_EXPERIMENTAL_SCSS_LIBS + [
23+
"//src/material-experimental/theming:theming_scss_lib",
24+
],
2325
)
2426

2527
sass_library(

src/material-experimental/_index.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@
44
@forward './popover-edit/popover-edit-theme' as popover-edit-* show popover-edit-color,
55
popover-edit-typography, popover-edit-density, popover-edit-theme;
66

7+
// Token-based theming API
8+
@forward './theming/theming' show theme, card, checkbox;
9+
710
// Additional public APIs for individual components
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
load("//tools:defaults.bzl", "sass_library")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
sass_library(
6+
name = "theming_scss_lib",
7+
srcs = glob(["**/_*.scss"]),
8+
deps = ["//src/material:sass_lib"],
9+
)
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
@use 'sass:list';
2+
@use 'sass:map';
3+
@use 'sass:meta';
4+
@use '@angular/material' as mat;
5+
6+
/// Whether to throw an error when a required dep is not configured. If false, the dep will be
7+
/// automatically configured instead.
8+
$_error-on-missing-dep: false;
9+
10+
/// Applies the theme for the given component configuration.
11+
/// @param {Map} $tokens A map containing the default values to use for tokens not explicitly
12+
/// customized in the component config object.
13+
/// @param {List} $component The component config object to emit theme tokens for.
14+
/// @output CSS variables representing the theme tokens for this component.
15+
@mixin _apply-theme($tokens, $component) {
16+
$id: map.get($component, id);
17+
$tokens: map.deep-merge($tokens, map.get($component, customizations));
18+
19+
// NOTE: for now we use a hardcoded if-chain, but in the future when first-class mixins are
20+
// supported, the configuration data will contain a reference to its own theme mixin.
21+
@if $id == 'mat.card' {
22+
@include mat.private-apply-card-theme-from-tokens($tokens);
23+
}
24+
@else if $id == 'mat.checkbox' {
25+
@include mat.private-apply-checkbox-theme-from-tokens($tokens);
26+
}
27+
@else {
28+
@error 'Unrecognized component theme: #{id}';
29+
}
30+
}
31+
32+
/// Gets the transitive closure of the given list of component configuration dependencies.
33+
/// @param {List} $components The list of component config objects to get the transitive deps for.
34+
/// @param {Map} $configured [()] A map of already configured component IDs. Used for recursion,
35+
/// should not be passed when calling.
36+
/// @return {List} The transitive closure of configs for the given $components.
37+
// TODO(mmalerba): Currently we use the deps to determine if additional tokens, other than the
38+
// explicitly requested ones need to be emitted, but the deps do not affect the ordering in which
39+
// the various configs are processed. Before moving out of experimental we should think more about
40+
// the ordering behavior we want. For the most part the order shouldn't matter, unless we have 2
41+
// configs trying to set the same token.
42+
@function _get-transitive-deps($components, $configured: ()) {
43+
// Mark the given components as configured.
44+
@each $component in $components {
45+
$configured: map.set($configured, map.get($component, id), true);
46+
}
47+
$new-deps: ();
48+
49+
// Check each of the given components for new deps.
50+
@each $component in $components {
51+
// Note: Deps are specified as getter functions that return a config object rather than a direct
52+
// config object. This allows us to only call the getter if the dep has not yet been configured.
53+
// This can be useful if we have 2 components that want to require each other to be configured.
54+
// Example: form-field and input. If we used direct config objects in this case, it would cause
55+
// infinite co-recursion.
56+
@each $dep-getter in mat.private-coerce-to-list(map.get($component, deps)) {
57+
$dep: meta.call($dep-getter);
58+
$dep-id: map.get($dep, id);
59+
@if not (map.has-key($configured, $dep-id)) {
60+
@if $_error-on-missing-dep {
61+
@error 'Missing theme: `#{map.get($component, id)}` depends on `#{$dep-id}`.' +
62+
' Please configure the theme for `#{$dep-id}` in your call to `mat.theme`';
63+
}
64+
@else {
65+
$configured: map.set($configured, $dep-id, true);
66+
$new-deps: list.append($new-deps, $dep);
67+
}
68+
}
69+
}
70+
}
71+
72+
// Append on the new deps to this list of component configurations and return.
73+
@if list.length($new-deps) > 0 {
74+
$components: list.join($components, _get-transitive-deps($new-deps, $configured));
75+
}
76+
@return $components;
77+
}
78+
79+
/// Apply the themes for the given component configs with the given ste of fallback token values.
80+
/// @param {Map} $tokens A map of fallback values to use for tokens that are not explicitly
81+
/// customized by one of the component configs.
82+
/// @param {List} $components The list of component configurations to emit tokens for.
83+
/// @output CSS variables representing the theme tokens for the given component configs.
84+
@mixin _theme($tokens, $components) {
85+
// Call the theme mixin for each configured component.
86+
@each $component in $components {
87+
@include _apply-theme($tokens, $component);
88+
}
89+
}
90+
91+
/// Takes the full list of tokens and a list of components to configure, and outputs all theme
92+
/// tokens for the configured components.
93+
/// @param {Map} $tokens A map of all tokens for the current design system.
94+
/// @param {List} $components The list of component configurations to emit tokens for.
95+
/// @output CSS variables representing the theme tokens for the given component configs.
96+
// TODO(mmalerba): Consider an alternate API where `$tokens` is not a separate argument,
97+
// but one of the configs in the `$components` list
98+
@mixin theme($tokens, $components) {
99+
@include _theme($tokens, _get-transitive-deps(mat.private-coerce-to-list($components)));
100+
}
101+
102+
/// Takes a list of components to configure, and outputs only the theme tokens that are explicitly
103+
/// customized by the configurations.
104+
/// @param {List} $components The list of component configurations to emit tokens for.
105+
/// @output CSS variables representing the theme tokens for the given component configs.
106+
// TODO(mmalerba): What should we call this?
107+
// - update-theme
108+
// - adjust-theme
109+
// - edit-theme
110+
// - override-theme
111+
// - retheme
112+
@mixin retheme($components) {
113+
@include _theme((), $components);
114+
}
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+
}

src/material/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ sass_library(
2424
srcs = [
2525
"_index.scss",
2626
"_theming.scss",
27+
"_token-theming.scss",
2728
],
2829
deps = [
2930
"//src/material/core:core_scss_lib",

src/material/_index.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
legacy-typography-hierarchy;
1515
@forward './core/typography/typography-utils' show typography-level,
1616
font-size, line-height, font-weight, letter-spacing, font-family, font-shorthand;
17+
@forward './core/tokens/m2' show m2-tokens-from-theme;
1718

1819
// Private/Internal
1920
@forward './core/density/private/all-density' show all-component-densities;
@@ -37,6 +38,8 @@
3738
@forward './core/style/button-common' as private-button-common-*;
3839
// The form field density mixin needs to be exposed, because the paginator depends on it.
3940
@forward './form-field/form-field-theme' as private-form-field-* show private-form-field-density;
41+
@forward './token-theming' as private-apply-*;
42+
@forward './core/style/sass-utils' as private-*;
4043

4144
// Structural
4245
@forward './core/core' show core;

src/material/_token-theming.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@forward './card/card-theme' as card-* show card-theme-from-tokens;
2+
@forward './checkbox/checkbox-theme' as checkbox-* show checkbox-theme-from-tokens;

src/material/button/_icon-button-theme.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
@use 'sass:map';
22
@use 'sass:math';
33
@use '@material/density/functions' as mdc-density-functions;
4-
@use '@material/icon-button/mixins' as mdc-icon-button;
54
@use '@material/icon-button/icon-button-theme' as mdc-icon-button-theme;
65
@use '@material/theme/theme-color' as mdc-theme-color;
76
@use '../core/tokens/m2/mdc/icon-button' as tokens-mdc-icon-button;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
@forward 'card-theme' hide color, density, theme, typography;
1+
@forward 'card-theme' hide color, density, theme, typography, theme-from-tokens;
22
@forward 'card-theme' as mat-mdc-card-* hide $mat-mdc-card-mdc-card-action-icon-color,
33
$mat-mdc-card-mdc-card-outline-color;

src/material/card/_card-theme.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@use 'sass:map';
12
@use '../core/theming/theming';
23
@use '../core/typography/typography';
34
@use '../core/tokens/token-utils';
@@ -76,3 +77,13 @@
7677
}
7778
}
7879
}
80+
81+
@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+
}
89+
}

0 commit comments

Comments
 (0)