Skip to content

feat(material-experimental/theming): add first part of token-based theming API #27000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@
# Material experimental package
/src/material-experimental/* @andrewseguin
/src/material-experimental/column-resize/** @andrewseguin
/src/material-experimental/mdc-tooltip/** @crisbeto
/src/material-experimental/menubar/** @jelbourn
/src/material-experimental/popover-edit/** @andrewseguin
/src/material-experimental/selection/** @andrewseguin
/src/material-experimental/theming/** @mmalerba

# CDK experimental package
/src/cdk-experimental/* @andrewseguin
Expand Down
13 changes: 7 additions & 6 deletions .ng-dev/commit-message.mts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export const commitMessage: CommitMessageConfig = {
'cdk/tree',
'google-maps',
'material-experimental/column-resize',
'material-experimental/theming',
'material-experimental/menubar',
'material-experimental/popover-edit',
'material-experimental/selection',
'material/button',
'material/card',
'material/checkbox',
Expand All @@ -53,12 +57,6 @@ export const commitMessage: CommitMessageConfig = {
'material/snack-bar',
'material/table',
'material/tabs',
'material-experimental/menubar',
'material-experimental/popover-edit',
'material-experimental/selection',
'material-moment-adapter',
'material-date-fns-adapter',
'material-luxon-adapter',
'material/autocomplete',
'material/legacy-autocomplete',
'material/badge',
Expand Down Expand Up @@ -110,6 +108,9 @@ export const commitMessage: CommitMessageConfig = {
'material/legacy-tooltip',
'material/tooltip',
'material/tree',
'material-moment-adapter',
'material-date-fns-adapter',
'material-luxon-adapter',
'youtube-player',
],
};
11 changes: 11 additions & 0 deletions src/dev-app/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ sass_binary(
],
)

sass_binary(
name = "theme_token_api",
src = "theme-token-api.scss",
deps = [
"//src/material:sass_lib",
"//src/material-experimental:sass_lib",
"//src/material/core:theming_scss_lib",
],
)

# Variables that are going to be inlined into the dev app index.html.
filegroup(
name = "variables",
Expand All @@ -154,6 +164,7 @@ filegroup(
"favicon.ico",
"index.html",
":theme",
":theme_token_api",
":variables",
"//src/dev-app/icon:icon_demo_assets",
"@npm//:node_modules/moment/min/moment-with-locales.min.js",
Expand Down
16 changes: 13 additions & 3 deletions src/dev-app/dev-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

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

/** Root component for the dev-app demos. */
Expand All @@ -18,4 +18,14 @@ import {DevAppLayout} from './dev-app/dev-app-layout';
standalone: true,
imports: [DevAppLayout, RouterModule],
})
export class DevApp {}
export class DevApp {
route = inject(ActivatedRoute);

constructor() {
this.route.queryParams.subscribe(q => {
(document.querySelector('#theme-styles') as any).href = q.hasOwnProperty('tokenapi')
? 'theme-token-api.css'
: 'theme.css';
});
}
}
2 changes: 1 addition & 1 deletion src/dev-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="theme.css" rel="stylesheet">
<link href="theme.css" rel="stylesheet" id="theme-styles">

<!-- FontAwesome for mat-icon demo. -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
Expand Down
68 changes: 68 additions & 0 deletions src/dev-app/theme-token-api.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@use '@angular/material' as mat;
@use '@angular/material-experimental' as matx;

dev-app {
&::before {
content: 'Using experimental theming API';
display: inline-block;
position: fixed;
z-index: 100;
bottom: 0;
left: 50%;
transform: translateX(-50%);
padding: 8px;
background: red;
color: white;
}
}

.demo-unicorn-dark-theme {
background: black;
color: white;
}

@include mat.core();

$light-theme: mat.define-light-theme((
color: (
primary: mat.define-palette(mat.$indigo-palette),
accent: mat.define-palette(mat.$pink-palette),
),
typography: mat.define-typography-config(),
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.

html {
@include matx.theme(
$tokens: mat.m2-tokens-from-theme($light-theme),
$components: (
matx.card(),
matx.checkbox(),
));
}

// Set up 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,
)
)),
));
}
4 changes: 3 additions & 1 deletion src/material-experimental/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ ts_library(

sass_library(
name = "theming_scss_lib",
srcs = MATERIAL_EXPERIMENTAL_SCSS_LIBS,
srcs = MATERIAL_EXPERIMENTAL_SCSS_LIBS + [
"//src/material-experimental/theming:theming_scss_lib",
],
)

sass_library(
Expand Down
3 changes: 3 additions & 0 deletions src/material-experimental/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@
@forward './popover-edit/popover-edit-theme' as popover-edit-* show popover-edit-color,
popover-edit-typography, popover-edit-density, popover-edit-theme;

// Token-based theming API
@forward './theming/theming' show theme, card, checkbox;

// Additional public APIs for individual components
9 changes: 9 additions & 0 deletions src/material-experimental/theming/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
load("//tools:defaults.bzl", "sass_library")

package(default_visibility = ["//visibility:public"])

sass_library(
name = "theming_scss_lib",
srcs = glob(["**/_*.scss"]),
deps = ["//src/material:sass_lib"],
)
134 changes: 134 additions & 0 deletions src/material-experimental/theming/_theming.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
@use 'sass:list';
@use 'sass:map';
@use 'sass:meta';
@use '@angular/material' as mat;

/// Whether to throw an error when a required dep is not configured. If false, the dep will be
/// automatically configured instead.
$_error-on-missing-dep: false;

/// Applies the theme for the given component configuration.
/// @param {Map} $tokens A map containing the default values to use for tokens not explicitly
/// customized in the component config object.
/// @param {List} $component The component config object to emit theme tokens for.
/// @output CSS variables representing the theme tokens for this component.
@mixin _apply-theme($tokens, $component) {
$id: map.get($component, id);
$tokens: map.deep-merge($tokens, map.get($component, customizations));

// NOTE: for now we use a hardcoded if-chain, but in the future when first-class mixins are
// supported, the configuration data will contain a reference to its own theme mixin.
@if $id == 'mat.card' {
@include mat.private-apply-card-theme-from-tokens($tokens);
}
@else if $id == 'mat.checkbox' {
@include mat.private-apply-checkbox-theme-from-tokens($tokens);
}
@else {
@error 'Unrecognized component theme: #{id}';
}
}

/// Gets the transitive closure of the given list of component configuration dependencies.
/// @param {List} $components The list of component config objects to get the transitive deps for.
/// @param {Map} $configured [()] A map of already configured component IDs. Used for recursion,
/// should not be passed when calling.
/// @return {List} The transitive closure of configs for the given $components.
// TODO(mmalerba): Currently we use the deps to determine if additional tokens, other than the
// explicitly requested ones need to be emitted, but the deps do not affect the ordering in which
// the various configs are processed. Before moving out of experimental we should think more about
// the ordering behavior we want. For the most part the order shouldn't matter, unless we have 2
// configs trying to set the same token.
@function _get-transitive-deps($components, $configured: ()) {
// Mark the given components as configured.
@each $component in $components {
$configured: map.set($configured, map.get($component, id), true);
}
$new-deps: ();

// Check each of the given components for new deps.
@each $component in $components {
// Note: Deps are specified as getter functions that return a config object rather than a direct
// config object. This allows us to only call the getter if the dep has not yet been configured.
// This can be useful if we have 2 components that want to require each other to be configured.
// Example: form-field and input. If we used direct config objects in this case, it would cause
// infinite co-recursion.
@each $dep-getter in mat.private-coerce-to-list(map.get($component, deps)) {
$dep: meta.call($dep-getter);
$dep-id: map.get($dep, id);
@if not (map.has-key($configured, $dep-id)) {
@if $_error-on-missing-dep {
@error 'Missing theme: `#{map.get($component, id)}` depends on `#{$dep-id}`.' +
' Please configure the theme for `#{$dep-id}` in your call to `mat.theme`';
}
@else {
$configured: map.set($configured, $dep-id, true);
$new-deps: list.append($new-deps, $dep);
}
}
}
}

// Append on the new deps to this list of component configurations and return.
@if list.length($new-deps) > 0 {
$components: list.join($components, _get-transitive-deps($new-deps, $configured));
}
@return $components;
}

/// Apply the themes for the given component configs with the given ste of fallback token values.
/// @param {Map} $tokens A map of fallback values to use for tokens that are not explicitly
/// customized by one of the component configs.
/// @param {List} $components The list of component configurations to emit tokens for.
/// @output CSS variables representing the theme tokens for the given component configs.
@mixin _theme($tokens, $components) {
// Call the theme mixin for each configured component.
@each $component in $components {
@include _apply-theme($tokens, $component);
}
}

/// Takes the full list of tokens and a list of components to configure, and outputs all theme
/// tokens for the configured components.
/// @param {Map} $tokens A map of all tokens for the current design system.
/// @param {List} $components The list of component configurations to emit tokens for.
/// @output CSS variables representing the theme tokens for the given component configs.
// TODO(mmalerba): Consider an alternate API where `$tokens` is not a separate argument,
// but one of the configs in the `$components` list
@mixin theme($tokens, $components) {
@include _theme($tokens, _get-transitive-deps(mat.private-coerce-to-list($components)));
}

/// Takes a list of components to configure, and outputs only the theme tokens that are explicitly
/// customized by the configurations.
/// @param {List} $components The list of component configurations to emit tokens for.
/// @output CSS variables representing the theme tokens for the given component configs.
// TODO(mmalerba): What should we call this?
// - update-theme
// - adjust-theme
// - edit-theme
// - 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: (),
);
}
1 change: 1 addition & 0 deletions src/material/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ sass_library(
srcs = [
"_index.scss",
"_theming.scss",
"_token-theming.scss",
],
deps = [
"//src/material/core:core_scss_lib",
Expand Down
3 changes: 3 additions & 0 deletions src/material/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
legacy-typography-hierarchy;
@forward './core/typography/typography-utils' show typography-level,
font-size, line-height, font-weight, letter-spacing, font-family, font-shorthand;
@forward './core/tokens/m2' show m2-tokens-from-theme;

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

// Structural
@forward './core/core' show core;
Expand Down
2 changes: 2 additions & 0 deletions src/material/_token-theming.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@forward './card/card-theme' as card-* show card-theme-from-tokens;
@forward './checkbox/checkbox-theme' as checkbox-* show checkbox-theme-from-tokens;
1 change: 0 additions & 1 deletion src/material/button/_icon-button-theme.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
@use 'sass:map';
@use 'sass:math';
@use '@material/density/functions' as mdc-density-functions;
@use '@material/icon-button/mixins' as mdc-icon-button;
@use '@material/icon-button/icon-button-theme' as mdc-icon-button-theme;
@use '@material/theme/theme-color' as mdc-theme-color;
@use '../core/tokens/m2/mdc/icon-button' as tokens-mdc-icon-button;
Expand Down
2 changes: 1 addition & 1 deletion src/material/card/_card-theme.import.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
@forward 'card-theme' hide color, density, theme, typography;
@forward 'card-theme' hide color, density, theme, typography, theme-from-tokens;
@forward 'card-theme' as mat-mdc-card-* hide $mat-mdc-card-mdc-card-action-icon-color,
$mat-mdc-card-mdc-card-outline-color;
11 changes: 11 additions & 0 deletions src/material/card/_card-theme.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@use 'sass:map';
@use '../core/theming/theming';
@use '../core/typography/typography';
@use '../core/tokens/token-utils';
Expand Down Expand Up @@ -76,3 +77,13 @@
}
}
}

@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));
}
}
Loading