Skip to content

Commit

Permalink
feat(material-experimental/theming): Introduce a facade layer between…
Browse files Browse the repository at this point in the history
… user-facing customizable keys and actual MDC token names (angular#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
  • Loading branch information
mmalerba authored Jun 9, 2023
1 parent cddb04f commit 227a741
Show file tree
Hide file tree
Showing 11 changed files with 449 additions and 58 deletions.
6 changes: 6 additions & 0 deletions src/dev-app/checkbox/checkbox-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,9 @@ <h5>No animations</h5>
</mat-checkbox>
</div>
</div>

<p>
<mat-checkbox class="demo-traffic-light-checkbox">
This checkbox has special styling when using the experimental token based theme
</mat-checkbox>
</p>
96 changes: 71 additions & 25 deletions src/dev-app/theme-token-api.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@use 'sass:map';
@use '@angular/material' as mat;
@use '@angular/material-experimental' as matx;

Expand All @@ -23,7 +24,7 @@ dev-app {

@include mat.core();

$light-theme: mat.define-light-theme((
$theme: mat.define-light-theme((
color: (
primary: mat.define-palette(mat.$indigo-palette),
accent: mat.define-palette(mat.$pink-palette),
Expand All @@ -32,37 +33,82 @@ $light-theme: mat.define-light-theme((
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.

// Apply all tokens (derived from `$theme`) to the `html` element. This ensures that all components
// on the page will inherit these tokens.
html {
@include matx.theme(
$tokens: mat.m2-tokens-from-theme($light-theme),
$tokens: mat.m2-tokens-from-theme($theme),
$components: (
matx.card(),
matx.checkbox(),
));
)
);
}

// Set up dark theme.

// Apply tokens needed for dark theme to the element with `.demo-unicorn-dark-theme`.
// This ensures that checkboxes within the element inherit the new tokens for dark theme,
// rather than the ones for light theme tokens set on `body`. Note that we're not setting *all* of
// the tokens, since many (density, typography, etc) are the same between light and 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,
)
)),
));
@include matx.retheme((
// TODO(mmalerba): In the future this should be configured through `matx.system-colors()`
matx.checkbox((theme-type: dark)),
matx.card((theme-type: dark)),
));
}

// Apply tokens related to the color palette to any element with `.mat-primary`, `.mat-accent`, or
// `.mat-warn` This ensures that checkboxes within the element inherit the new tokens for the
// appropriate palette, rather than the any color that may have been set on an element further up
// the hierarchy. Again, rather than applying *all* the tokens, we apply only the ones effected by
// the palette color. With this setup, the palette class need not go on the component itself
// (e.g. <mat-checkbox class="mat-primary">), it can go on some ancestor element and the tokens will
// flow down. If multiple elements specify different classes, the closest one to the component will
// take precedence.
// (e.g. <div class="mat-warn><mat-checkbox class="mat-primary">I'm primary</mat-checkbox></div>)
.mat-primary {
@include matx.retheme((
matx.checkbox((
color-palette: map.get($theme, color, primary)
)),
));
}
.mat-accent {
@include matx.retheme((
matx.checkbox((
color-palette: map.get($theme, color, accent)
)),
));
}
.mat-warn {
@include matx.retheme((
matx.checkbox((
color-palette: map.get($theme, color, warn)
)),
));
}

// Apply tokens for a completely custom checkbox that appears as an unfilled red box when unchecked,
// and a filled green box when checked.
.demo-traffic-light-checkbox {
@include matx.retheme((
matx.checkbox((
checkmark-color: transparent,
selected-box-color: green,
selected-focus-box-color: green,
selected-hover-box-color: green,
selected-pressed-box-color: green,
selected-focus-ring-color: green,
selected-hover-ring-color: green,
selected-pressed-ring-color: green,
unselected-box-color: red,
unselected-focus-box-color: red,
unselected-hover-box-color: red,
unselected-pressed-box-color: red,
unselected-focus-ring-color: red,
unselected-hover-ring-color: red,
unselected-pressed-ring-color: red,
))
));
}
4 changes: 3 additions & 1 deletion src/material-experimental/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
popover-edit-typography, popover-edit-density, popover-edit-theme;

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

// Additional public APIs for individual components
61 changes: 61 additions & 0 deletions src/material-experimental/theming/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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,
and still being evaluated.

## Design tokens
- 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.
- Angular Material represents design tokens as CSS variables

## M2 vs M3 tokens
- 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
- Token values for M2 can be obtained by:
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.
- Token values for M3 are not yet available

Example:
```scss
// Create an Angular Material theme.
$my-theme: mat.define-light-theme(...);

// Create tokens for M2 from the theme.
$m2-tokens: mat.m2-tokens-from-theme($my-theme);
```
## Component theme configuration functions
- 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
- So far the following component theme configuration functions have been implements:
- `matx.checkbox` configures tokens for the mat-checkbox to be applied
- `matx.card` configures tokens for the mat-card to be applied
- The returned configurations from these functions are passed to `matx.theme` or `matx.retheme`
- 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
- The functions can also accept a map of customizations as an argument.
- Each function has its own set of supported map keys that can be used to customize the value of the underlying tokens
- 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
- For supported map keys (TODO: have docs for these):
- See `$_customization-resolvers` [here](https://github.com/angular/components/blob/main/src/material-experimental/theming/_checkbox.scss) for `matx.checkbox`
- See `$_customization-resolvers` [here](https://github.com/angular/components/blob/main/src/material-experimental/theming/_card.scss) for `matx.card`

## Theming mixins
- There are 2 mixins used for theming apps
- `matx.theme` is intended to apply the full theme for some components, with all tokens they need to function.
- `matx.retheme` is intended to re-apply specific tokens to change the appearance for some components by overriding the tokens applied by `matx.theme`.
- Both mixins emit *only* CSS variables representing design tokens
- Both mixins emit their tokens directly under the user specified selector. This gives the user complete control over the selector specificity.
- Using `matx.theme`
- Takes 2 arguments:
- `$tokens` The set of token defaults that will be used for any tokens not explicitly customized by the component theme config
- `$components` List of component theme configs indicating which components to emit tokens for, and optionally, customizations for some token values
- Outputs *all* tokens used by the configured components
- Using `matx.retheme`
- Takes 1 argument:
- `$components` List of component theme configs to emit customized token values for
- Outputs *only* the explicitly customized tokens, not any of the other tokens used by the component

## Recommended theming structure
- Apply the base token values using `matx.theme` *once*
- Choose selectors with minimal specificity when applying tokens
- Prefer to rely on CSS inheritance to apply token overrides rather than specificity.
For example if checkbox tokens are set on the root element (`html`) they will be inherited down
the DOM and affect any `<mat-checkbox>` within the document. If checkboxes in a specific section
need to appear differently, say within `.dark-sidebar`, set the token overrides on the
`.dark-sidebar` element and they will be inherited down to the checkboxes within, instead of the
values from the root element.
- 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
93 changes: 93 additions & 0 deletions src/material-experimental/theming/_card.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
@use 'sass:color';
@use 'sass:meta';
@use '@angular/material' as mat;
@use './token-resolution';

// TODO(mmalerba): This should live under material/card when moving out of experimental.

/// Gets tokens for setting the card's shape.
/// @param {String} $shape The card's shape.
/// @return {Map} A map of tokens for setting the card's shape.
// Note: we use a function rather than simple rename, because we want to map a single shape value to
// multiple tokens, rather than offer separate shape customizations for elevated and outlined cards.
@function _get-tokens-for-card-shape($shape) {
@return (
(mdc, elevated-card): (container-shape: $shape),
(mdc, outline-card): (container-shape: $shape),
);
}

/// Gets tokens for setting the card's color.
/// @param {String} $shape The card's shape.
/// @return {Map} A map of tokens for setting the card's shape.
@function _get-tokens-for-card-color($color) {
@return (
(mdc, elevated-card): (container-color: $color),
(mdc, outline-card): (container-color: $color),
);
}

/// Gets a map of card token values that are derived from the theme type.
/// @param {'light' | 'dark'} $theme-type The type of theme.
/// @return {Map} A map of card token values derived from the given theme type.
@function _get-tokens-for-theme-type($theme-type) {
$is-dark: $theme-type == 'dark';
$foreground: if($is-dark, white, black);
$card-color: if($is-dark, mat.get-color-from-palette(mat.$gray-palette, 800), white);
$outline-color: color.change($foreground, $alpha: 0.12);
$subtitle-color: if($is-dark, rgba(white, 0.7), rgba(black, 0.54));

@return (
(mdc, elevated-card): (
container-color: $card-color,
),
(mdc, outlined-card): (
container-color: $card-color,
outline-color: $outline-color,
),
(mat, card): (
subtitle-text-color: $subtitle-color,
),
);
}

/// Resolvers for mat-card customizations.
$_customization-resolvers: mat.private-merge-all(
token-resolution.alias((
elevation: container-elevation,
shadow-color: container-shadow-color,
), (mdc, elevated-card)),
token-resolution.forward((
outline-width,
outline-color
), (mdc, outlined-card)),
token-resolution.alias((
title-font: title-text-font,
title-line-height: title-text-line-height,
title-font-size: title-text-size,
title-letter-spacing: title-text-tracking,
title-font-weight: title-text-weight,
subtitle-font: subtitle-text-font,
subtitle-line-height: subtitle-text-line-height,
subtitle-font-size: subtitle-text-size,
subtitle-letter-spacing: subtitle-text-tracking,
subtitle-font-weight: subtitle-text-weight,
subtitle-color: subtitle-text-color
), (mat, card)),
(
background-color: meta.get-function(_get-tokens-for-card-color),
border-radius: meta.get-function(_get-tokens-for-card-shape),
theme-type: meta.get-function(_get-tokens-for-theme-type),
)
);

/// Configure the mat-card's theme.
/// @param {Map} $customizations [()] A map of custom values to use when theming mat-card.
@function card($customizations: ()) {
@return (
id: 'mat.card',
customizations: token-resolution.resolve-customized-tokens(
'mat.card', $_customization-resolvers, $customizations),
deps: (),
);
}
Loading

0 comments on commit 227a741

Please sign in to comment.