Skip to content
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

Initial implementation of M3 theme objects #27532

Merged
merged 9 commits into from
Aug 7, 2023
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
/src/material-experimental/popover-edit/** @andrewseguin
/src/material-experimental/selection/** @andrewseguin
/src/material-experimental/theming/** @mmalerba
/src/material-experimental/theming-next/** @mmalerba

# CDK experimental package
/src/cdk-experimental/* @andrewseguin
Expand Down
1 change: 1 addition & 0 deletions .ng-dev/commit-message.mts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const commitMessage: CommitMessageConfig = {
'cdk/tree',
'google-maps',
'material-experimental/column-resize',
'material-experimental/theming-next',
'material-experimental/theming',
'material-experimental/menubar',
'material-experimental/popover-edit',
Expand Down
60 changes: 38 additions & 22 deletions src/dev-app/theme-token-api.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
@use 'sass:map';
@use '@angular/material' as mat;
@use '@angular/material-experimental' as matx;

// Add an indicator to make it clear that the app is styled with the experimental theming API.
dev-app {
&::before {
content: 'Using experimental theming API';
Expand All @@ -16,33 +18,47 @@ dev-app {
}
}

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

// Emit Angular Material core styles.
@include mat.core();

$theme: mat.define-light-theme((
// Base theme configuration for our M3 theme.
$m3-base-config: (
color: (
primary: mat.define-palette(mat.$indigo-palette),
accent: mat.define-palette(mat.$pink-palette),
primary: matx.$m3-green-palette
),
typography: mat.define-typography-config(),
density: 0,
));
typography: (
brand-family: 'monospace'
mmalerba marked this conversation as resolved.
Show resolved Hide resolved
)
);

// Our M3 light theme.
$light-theme: matx.define-theme($m3-base-config);

// Apply all tokens (derived from `$theme`) to the `html` element. This ensures that all components
// on the page will inherit these tokens.
// Our M3 dark theme.
$dark-theme: matx.define-theme(map.set($m3-base-config, color, theme-type, dark));
mmalerba marked this conversation as resolved.
Show resolved Hide resolved

// Emit default theme styles.
html {
@include matx.theme(
matx.token-defaults(matx.get-m3-tokens()),
matx.card(),
matx.checkbox(),
);
@include mat.checkbox-theme($light-theme);
@include mat.card-theme($light-theme);
}

// Emit dark theme styles.
.demo-unicorn-dark-theme {
// TODO(mmalerba): choose colors from the theming API.
background: black;
color: white;

@include mat.checkbox-color($dark-theme);
@include mat.card-color($dark-theme);
}

// TODO(mmalerba): Figure out a consistent solution for handling dark themes & color palette
// variants across M2 & M3 (likely by implementing `matx.system-colors`). As a reference, see the
// prior version of this file that showed a possible way to accomplish this in M2:
// https://github.com/angular/components/blob/5f5c5160dc20331619fc6729aa2ad78ac84af1c3/src/dev-app/theme-token-api.scss
// Emit density styles for each scale.
@each $scale in (maximum, 0, -1, -2, -3, minimum) {
$scale-theme: matx.define-theme(map.set($m3-base-config, density, scale, $scale));

.demo-density-#{$scale} {
@include mat.checkbox-density($scale-theme);
@include mat.card-density($scale-theme);
}
}
6 changes: 4 additions & 2 deletions src/material-experimental/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ sass_library(
name = "theming_scss_lib",
srcs = MATERIAL_EXPERIMENTAL_SCSS_LIBS + [
"//src/material-experimental/theming:theming_scss_lib",
"//src/material-experimental/theming-next:theming_next_scss_lib",
],
)

sass_library(
name = "sass_lib",
srcs = ["_index.scss"],
deps = [":theming_scss_lib"],
deps = [
":theming_scss_lib",
],
)

# Generate the material-experimental `package.json` by adding all MDC dependencies
Expand All @@ -48,7 +51,6 @@ ng_package(
srcs = [
":package_json",
":sass_lib",
":theming_scss_lib",
],
tags = ["release-package"],
deps = MATERIAL_EXPERIMENTAL_TARGETS + MATERIAL_EXPERIMENTAL_TESTING_TARGETS,
Expand Down
7 changes: 3 additions & 4 deletions src/material-experimental/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
popover-edit-typography, popover-edit-density, popover-edit-theme;

// Token-based theming API
@forward './theming/theming' show theme, token-defaults;
@forward './theming/checkbox' show checkbox;
@forward './theming/card' show card;
@forward './theming/m3-tokens' show get-m3-tokens;
@forward './theming/definition' show define-theme, define-colors, define-typography, define-density;
@forward './theming/m3-palettes' as m3-* show $m3-red-palette, $m3-orange-palette,
$m3-yellow-palette, $m3-green-palette, $m3-blue-palette, $m3-indigo-palette, $m3-violet-palette;

// Additional public APIs for individual components
9 changes: 9 additions & 0 deletions src/material-experimental/theming-next/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_next_scss_lib",
srcs = glob(["**/_*.scss"]),
deps = ["//src/material:sass_lib"],
)
146 changes: 146 additions & 0 deletions src/material-experimental/theming-next/_m3-tokens.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
@use 'sass:map';
@use '@angular/material' as mat;
@use '@material/tokens/v0_161' as mdc-tokens;

/// Picks a submap containing only the given keys out the given map.
/// @param {Map} $map The map to pick from.
/// @param {List} $keys The map keys to pick.
/// @return {Map} A submap containing only the given keys.
@function _pick($map, $keys) {
$result: ();
@each $key in $keys {
@if map.has-key($map, $key) {
$result: map.set($result, $key, map.get($map, $key));
}
}
@return $result;
}

/// Filters keys with a null value out of the map.
/// @param {Map} $map The map to filter.
/// @return {Map} The given map with all of the null keys filtered out.
@function _filter-nulls($map) {
$result: ();
@each $key, $val in $map {
@if $val != null {
$result: map.set($result, $key, $val);
}
}
@return $result;
}

/// Gets the MDC tokens for the given prefix, M3 token values, and supported token slots.
/// @param {List} $prefix The token prefix for the given tokens.
/// @param {Map} $m3-values A map of M3 token values for the given prefix.
/// @param {Map} $slots A map of token slots, with null value indicating the token is not supported.
/// @return {Map} A map of fully qualified token names to values, for only the supported tokens.
@function _get-mdc-tokens($prefix, $m3-values, $slots) {
$used-token-names: map.keys(_filter-nulls(map.get($slots, $prefix)));
$used-m3-tokens: _pick($m3-values, $used-token-names);
@return (
$prefix: $used-m3-tokens,
);
}

/// Sets all of the standard typography tokens for the given token base name to the given typography
/// level.
/// @param {Map} $typography-tokens The MDC system-typescale tokens.
/// @param {String} $base-name The token base name to get the typography tokens for
/// @param {String} $typography-level The typography level to base the token values on.
/// @return {Map} A map containing the typography tokens for the given base token name.
@function _get-typography-tokens($typography-tokens, $base-name, $typography-level) {
$result: ();
@each $prop in (font, line-height, size, tracking, weight) {
$result: map.set(
$result,
#{$base-name}-#{$prop},
map.get($typography-tokens, #{$typography-level}-#{$prop}
));
}
@return $result;
}

/// Renames the keys in a map
/// @param {Map} $map The map whose keys should be renamed
/// @param {Map} $rename-keys A map of original key to renamed key to apply to $map
/// @return {Map} The result of applying the given key renames to the given map.
@function _rename-map-keys($map, $rename-keys) {
$result: $map;
@each $old-key-name, $new-key-name in $rename-keys {
@if map.has-key($map, $old-key-name) {
$result: map.set($result, $new-key-name, map.get($map, $old-key-name));
}
}
@return $result;
}

/// Renames the official checkbox tokens to match the names actually used in MDC's code (which are
/// different). This is a temporary workaround until MDC updates to use the correct names for the
/// tokens.
/// @param {Map} $tokens The map of checkbox tokens with the official tokens names
/// @return {Map} The given tokens, renamed to be compatible with MDC's token implementation.
@function _fix-checkbox-token-names($tokens) {
$rename-keys: (
'selected-icon-color': 'selected-checkmark-color',
'selected-disabled-icon-color': 'disabled-selected-checkmark-color',
'selected-container-color': 'selected-icon-color',
'selected-hover-container-color': 'selected-hover-icon-color',
'selected-disabled-container-color': 'disabled-selected-icon-color',
'selected-disabled-container-opacity': 'disabled-selected-icon-opacity',
'selected-focus-container-color': 'selected-focus-icon-color',
'selected-pressed-container-color': 'selected-pressed-icon-color',
'unselected-disabled-outline-color': 'disabled-unselected-icon-color',
'unselected-disabled-container-opacity': 'disabled-unselected-icon-opacity',
'unselected-focus-outline-color': 'unselected-focus-icon-color',
'unselected-hover-outline-color': 'unselected-hover-icon-color',
'unselected-outline-color': 'unselected-icon-color',
'unselected-pressed-outline-color': 'unselected-pressed-icon-color'
);
@return _rename-map-keys($tokens, $rename-keys);
}

// TODO(mmalerba): We need a way to accept custom M3 token values generated from MDCs theme builder
// or other means. We can't just use them directly without processing them first because we need to
// add our made up tokens,
/// Gets the default token values for M3.
/// @return The default set of M3 tokens.
@function get-m3-tokens() {
$typography: mdc-tokens.md-sys-typescale-values();
$colors: mdc-tokens.md-sys-color-values-light();

// TODO(mmalerba): Refactor this to not depend on the legacy theme. This is a hack for now because
// there is no good way to get the token slots in material-experimental without exposing them all
// from material.
$fake-theme: mat.define-light-theme((
color: (
primary: mat.define-palette(mat.$red-palette),
accent: mat.define-palette(mat.$red-palette),
warn: mat.define-palette(mat.$red-palette),
),
typography: mat.define-typography-config(),
density: 0
));
$token-slots: mat.m2-tokens-from-theme($fake-theme);

// TODO(mmalerba): Fill in remaining tokens.
@return mat.private-merge-all(
// Fill in official MDC tokens.
_get-mdc-tokens((mdc, checkbox),
_fix-checkbox-token-names(mdc-tokens.md-comp-checkbox-values()), $token-slots),
_get-mdc-tokens((mdc, elevated-card), mdc-tokens.md-comp-elevated-card-values(),
$token-slots),
_get-mdc-tokens((mdc, outlined-card), mdc-tokens.md-comp-outlined-card-values(),
$token-slots),
// Choose values for our made up tokens based on MDC system tokens or sensible hardcoded
// values.
(
(mat, card): mat.private-merge-all(
_get-typography-tokens($typography, title-text, title-large),
_get-typography-tokens($typography, subtitle-text, title-medium),
(
subtitle-text-color: map.get($colors, on-surface)
)
)
),
);
}
78 changes: 78 additions & 0 deletions src/material-experimental/theming/_config-validation.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
@use 'sass:list';
@use 'sass:map';
@use '@angular/material' as mat;

/// Validates a theme config.
/// @param {Map} $config The config to test.
/// @return {List} null if no error, else the error message
@function validate-theme-config($config) {
$err: mat.private-validate-type($config, 'map', 'null');
@if $err {
@return (#{'$config'} #{'should be a color configuration object. Got:'} $config);
}
$err: mat.private-validate-allowed-values(map.keys($config or ()), color, typography, density);
@if $err {
@return (#{'$config'} #{'has unexpected properties:'} $err);
}
$err: validate-color-config(map.get($config, color));
@if $err {
@return list.set-nth($err, 1, #{'#{list.nth($err, 1)}.color'});
}
$err: validate-typography-config(map.get($config, typography));
@if $err {
@return list.set-nth($err, 1, #{'#{list.nth($err, 1)}.typography'});
}
$err: validate-density-config(map.get($config, density));
@if $err {
@return list.set-nth($err, 1, #{'#{list.nth($err, 1)}.density'});
}
@return null;
}

/// Validates a theme color config.
/// @param {Map} $config The config to test.
/// @return {List} null if no error, else the error message
@function validate-color-config($config) {
$err: mat.private-validate-type($config, 'map', 'null');
@if $err {
@return (#{'$config'} #{'should be a color configuration object. Got:'} $config);
}
$err: mat.private-validate-allowed-values(
map.keys($config or ()), theme-type, primary, secondary, tertiary);
@if $err {
@return (#{'$config'} #{'has unexpected properties:'} $err);
}
@return null;
}

/// Validates a theme typography config.
/// @param {Map} $config The config to test.
/// @return {List} null if no error, else the error message
@function validate-typography-config($config) {
$err: mat.private-validate-type($config, 'map', 'null');
@if $err {
@return (#{'$config'} #{'should be a typography configuration object. Got:'} $config);
}
$err: mat.private-validate-allowed-values(
map.keys($config or ()), brand-family, plain-family, bold-weight, medium-weight,
regular-weight);
@if $err {
@return (#{'$config'} #{'has unexpected properties:'} $err);
}
@return null;
}

/// Validates a theme density config.
/// @param {Map} $config The config to test.
/// @return {List} null if no error, else the error message
@function validate-density-config($config) {
$err: mat.private-validate-type($config, 'map', 'null');
@if $err {
@return (#{'$config'} #{'should be a density configuration object. Got:'} $config);
}
$err: mat.private-validate-allowed-values(map.keys($config or ()), scale);
@if $err {
@return (#{'$config'} #{'has unexpected properties:'} $err);
}
@return null;
}
Loading
Loading