diff --git a/src/dev-app/theme-token-api.scss b/src/dev-app/theme-token-api.scss index 5a2d16ddf317..2831159cce03 100644 --- a/src/dev-app/theme-token-api.scss +++ b/src/dev-app/theme-token-api.scss @@ -1,4 +1,5 @@ @use '@angular/material' as mat; +@use '@angular/material-experimental'; dev-app { &::before { @@ -15,4 +16,38 @@ dev-app { } } -@debug 'Generated M2 tokens:' mat.m2-tokens-from-theme(); +.demo-unicorn-dark-theme { + background: black; + color: white; +} + +@include mat.core(); + +$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. + +@include material-experimental.theme($components: ( + material-experimental.card(), + material-experimental.checkbox(), +)); + +// Set up dark theme. + +.demo-unicorn-dark-theme { + @include material-experimental.theme($tokens: mat.m2-tokens-from-theme($dark-theme), $components: ( + material-experimental.checkbox(( + (mdc, checkbox): ( + selected-checkmark-color: red, + ) + )), + )); +} diff --git a/src/material-experimental/_index.scss b/src/material-experimental/_index.scss index b862e70da073..fd2a45e8fc61 100644 --- a/src/material-experimental/_index.scss +++ b/src/material-experimental/_index.scss @@ -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 diff --git a/src/material-experimental/theming/_theming.scss b/src/material-experimental/theming/_theming.scss new file mode 100644 index 000000000000..016f2db8801e --- /dev/null +++ b/src/material-experimental/theming/_theming.scss @@ -0,0 +1,102 @@ +@use 'sass:list'; +@use 'sass:map'; +@use 'sass:meta'; +@use 'sass:string'; +@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. +@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 dependency configurations for the given list of component configurations. +@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 { + @each $dep-getter in mat.private-normalize-args-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; +} + +@mixin _theme($tokens, $components) { + // Call the theme mixin for each configured component. + @at-root #{& or body} { + @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. +@mixin theme($tokens: mat.m2-tokens-from-theme(), $components) { + @include _theme($tokens, _get-transitive-deps(mat.private-normalize-args-list($components))); +} + +// TODO(mmalerba): What should we call this? +// - update-theme +// - adjust-theme +// - edit-theme +// - override-theme +// - retheme +// Takes a list of components to configure, and outputs only the theme tokens that are explicitly +// customized by the configurations. +@mixin update-theme($components) { + @include _theme((), $components); +} + +// Configure the mat-card's theme. +@function card($customizations: ()) { + @return ( + id: 'mat.card', + customizations: $customizations, + deps: (), + ); +} + +// Configure the mat-checkbox's theme. +@function checkbox($customizations: ()) { + @return ( + id: 'mat.checkbox', + customizations: $customizations, + deps: (), + ); +} diff --git a/src/material/_index.scss b/src/material/_index.scss index d5ba704d2437..aaa7c6fca1a3 100644 --- a/src/material/_index.scss +++ b/src/material/_index.scss @@ -38,6 +38,7 @@ // 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; diff --git a/src/material/core/style/_sass-utils.scss b/src/material/core/style/_sass-utils.scss index b4922910b83e..10a8e45ddade 100644 --- a/src/material/core/style/_sass-utils.scss +++ b/src/material/core/style/_sass-utils.scss @@ -1,4 +1,6 @@ +@use 'sass:list'; @use 'sass:map'; +@use 'sass:meta'; // A version of the standard `map.deep-merge` function that takes a variable number of arguments. // Each argument is deep-merged into the final result from left to right. @@ -9,3 +11,10 @@ } @return $result; } + +// Normalizes a list of arguments to ensure it really is a list and not a single arg. +// This should be used when dealing with user-passed lists of args to avoid confusing errors, +// since Sass treats `($x)` as equivalent to `$x`. +@function normalize-args-list($list) { + @return if(meta.type-of($list) != 'list', ($list,), $list); +}