From db5b8dc29b900470523bb20eea1ba255c2dc1168 Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Wed, 31 Jul 2024 11:15:51 -0600 Subject: [PATCH] feat(multiple): fallback to system level variables (#29480) * feat(multiple): fallback to system level variables * feat(multiple): create sass util for checking css var name * feat(multiple): add docs for generate-tokens density param * feat(multiple): make some properties/functions private * feat(multiple): lint fixes * feat(multiple): add todo for weird param * feat(multiple): add check for using color mix --- src/material/_index.scss | 4 +- src/material/core/style/_elevation.scss | 5 + src/material/core/style/_sass-utils.scss | 22 +++- src/material/core/tokens/_m3-tokens.scss | 118 ++++++++++++++++-- .../core/tokens/_token-definition.scss | 6 + src/material/core/tokens/_token-utils.scss | 106 ++++++++++------ src/material/core/tokens/m3/mat/_app.scss | 5 +- 7 files changed, 209 insertions(+), 57 deletions(-) diff --git a/src/material/_index.scss b/src/material/_index.scss index 18cd3b295656..1f140daa4d99 100644 --- a/src/material/_index.scss +++ b/src/material/_index.scss @@ -18,7 +18,9 @@ @forward './core/typography/typography' show typography-hierarchy; @forward './core/typography/typography-utils' show font-shorthand; @forward './core/tokens/m2' show m2-tokens-from-theme; -@forward './core/tokens/m3-tokens' show system-level-colors, system-level-typography; +@forward './core/tokens/m3-tokens' show system-level-colors, + system-level-typography, system-level-elevation, system-level-shape, + system-level-motion, system-level-state; // Private/Internal @forward './core/density/private/all-density' show all-component-densities; diff --git a/src/material/core/style/_elevation.scss b/src/material/core/style/_elevation.scss index 3dff26f4ab80..6a9d9cacbba2 100644 --- a/src/material/core/style/_elevation.scss +++ b/src/material/core/style/_elevation.scss @@ -3,6 +3,7 @@ @use 'sass:meta'; @use 'sass:string'; @use './variables'; +@use './sass-utils'; $_umbra-opacity: 0.2; $_penumbra-opacity: 0.14; @@ -143,6 +144,10 @@ $prefix: 'mat-elevation-z'; @return null; } + @if (sass-utils.is-css-var-name($zValue)) { + @return $zValue; + } + @if meta.type-of($zValue) != number or not math.is-unitless($zValue) { @error '$zValue must be a unitless number, but received `#{$zValue}`'; } diff --git a/src/material/core/style/_sass-utils.scss b/src/material/core/style/_sass-utils.scss index a57483b9b99b..92735c5cf77f 100644 --- a/src/material/core/style/_sass-utils.scss +++ b/src/material/core/style/_sass-utils.scss @@ -1,4 +1,5 @@ @use 'sass:color'; +@use 'sass:string'; @use 'sass:list'; @use 'sass:map'; @use 'sass:meta'; @@ -64,12 +65,23 @@ $use-system-typography-variables: false; /// A version of the Sass `color.change` function that is safe ot use with CSS variables. @function safe-color-change($color, $args...) { $args: meta.keywords($args); + $use-color-mix: $use-system-color-variables or + (is-css-var-name($color) and string.index($color, '--mat') == 1); @if (meta.type-of($color) == 'color') { @return color.change($color, $args...); } - @else if ($color != null and map.get($args, alpha) != null and $use-system-color-variables) { + @else if ($color != null and + map.get($args, alpha) != null and $use-color-mix) { $opacity: map.get($args, alpha); - @return #{color-mix(in srgb, #{$color} #{($opacity * 100) + '%'}, transparent)}; + @if meta.type-of($opacity) == number { + $opacity: ($opacity * 100) + '%'; + } + + @if (is-css-var-name($color)) { + $color: var($color); + } + + @return #{color-mix(in srgb, #{$color} #{$opacity}, transparent)}; } @return $color; } @@ -91,3 +103,9 @@ $use-system-typography-variables: false; } @return $kwargs; } + +// Returns whether the $value is a CSS variable name based on whether it's a string prefixed +// by "--". +@function is-css-var-name($value) { + @return meta.type-of($value) == string and string.index($value, '--') == 1; +} diff --git a/src/material/core/tokens/_m3-tokens.scss b/src/material/core/tokens/_m3-tokens.scss index 605d6322cae0..a86b2d6333c0 100644 --- a/src/material/core/tokens/_m3-tokens.scss +++ b/src/material/core/tokens/_m3-tokens.scss @@ -4,9 +4,17 @@ @use './m3'; @use './m3/definitions' as m3-token-definitions; @use '../tokens/m2' as m2-tokens; +@use '../style/elevation'; @use './density'; @use './format-tokens'; +// Default system level prefix to use when directly calling the `system-level-*` mixins +$_system-level-prefix: sys; + +// Prefix used for component token fallback variables, e.g. +// `color: var(--mdc-text-button-label-text-color, var(--mat-app-primary));` +$_system-fallback-prefix: mat-app; + /// Generates tokens for the given palette with the given prefix. /// @param {Map} $palette The palette to generate tokens for /// @param {String} $prefix The key prefix used to name the tokens @@ -86,8 +94,9 @@ $_cached-token-slots: null; /// Generates a set of namespaced tokens for all components. /// @param {Map} $systems The MDC system tokens /// @param {Boolean} $include-non-systemized Whether to include non-systemized tokens +/// @param {Boolean} $include-density Whether to include density tokens /// @return {Map} A map of namespaced tokens -@function _generate-tokens($systems, $include-non-systemized: false) { +@function _generate-tokens($systems, $include-non-systemized: false, $include-density: false) { $systems: map.merge(( md-sys-color: (), md-sys-elevation: (), @@ -116,16 +125,63 @@ $_cached-token-slots: null; // Strip out tokens that are systemized by our made up density system. @each $namespace, $tokens in $result { @each $token, $value in $tokens { - @if density.is-systemized($namespace, $token) { + @if density.is-systemized($namespace, $token) and not $include-density { $tokens: map.remove($tokens, $token); } } $result: map.set($result, $namespace, $tokens); } + @return $result; } -@mixin system-level-colors($theme, $overrides: ()) { +// Return a new map where the values are the same as the provided map's +// keys, prefixed with "--mat-app-". For example: +// (key1: '', key2: '') --> (key1: --mat-app-key1, key2: --mat-app-key2) +@function _create-system-app-vars-map($map) { + $new-map: (); + @each $key, $value in $map { + $new-map: map.set($new-map, $key, --#{$_system-fallback-prefix}-#{$key}); + } + @return $new-map; +} + +// Create a components tokens map where values are based on +// system fallback variables referencing Material's system keys. +// Includes density token fallbacks where density is 0. +@function create-system-fallbacks() { + $app-vars: ( + 'md-sys-color': + _create-system-app-vars-map(m3-token-definitions.md-sys-color-values-light()), + 'md-sys-typescale': + _create-system-app-vars-map(m3-token-definitions.md-sys-typescale-values()), + 'md-sys-elevation': + _create-system-app-vars-map(m3-token-definitions.md-sys-elevation-values()), + 'md-sys-state': + _create-system-app-vars-map(m3-token-definitions.md-sys-state-values()), + 'md-sys-shape': + _create-system-app-vars-map(m3-token-definitions.md-sys-shape-values()), + ); + + @return sass-utils.deep-merge-all( + _generate-tokens($app-vars, true, true), + generate-density-tokens(0) + ); +} + +// Emits CSS variables for Material's system level values. Uses the +// namespace prefix in $_system-fallback-prefix. +// e.g. --mat-app-surface: #E5E5E5 +@mixin theme($theme, $overrides: ()) { + @include system-level-colors($theme, $overrides, $_system-fallback-prefix); + @include system-level-typography($theme, $overrides, $_system-fallback-prefix); + @include system-level-elevation($theme, $overrides, $_system-fallback-prefix); + @include system-level-shape($theme, $overrides, $_system-fallback-prefix); + @include system-level-motion($theme, $overrides, $_system-fallback-prefix); + @include system-level-state($theme, $overrides, $_system-fallback-prefix); +} + +@mixin system-level-colors($theme, $overrides: (), $prefix: null) { $palettes: map.get($theme, _mat-theming-internals-do-not-access, palettes); $base-palettes: ( neutral: map.get($palettes, neutral), @@ -135,12 +191,15 @@ $_cached-token-slots: null; ); $type: map.get($theme, _mat-theming-internals-do-not-access, theme-type); - $system-variables-prefix: map.get($theme, _mat-theming-internals-do-not-access, - color-system-variables-prefix) or sys; $primary: map.merge(map.get($palettes, primary), $base-palettes); $tertiary: map.merge(map.get($palettes, tertiary), $base-palettes); $error: map.get($palettes, error); + @if (not $prefix) { + $prefix: map.get($theme, _mat-theming-internals-do-not-access, + color-system-variables-prefix) or $_system-level-prefix; + } + $ref: ( md-ref-palette: _generate-ref-palette-tokens($primary, $tertiary, $error) ); @@ -150,27 +209,28 @@ $_cached-token-slots: null; m3-token-definitions.md-sys-color-values-light($ref)); @each $name, $value in $sys-colors { - --#{$system-variables-prefix}-#{$name}: #{map.get($overrides, $name) or $value}; + --#{$prefix}-#{$name}: #{map.get($overrides, $name) or $value}; } } -@mixin system-level-typography($theme, $overrides: ()) { +@mixin system-level-typography($theme, $overrides: (), $prefix: null) { $font-definition: map.get($theme, _mat-theming-internals-do-not-access, font-definition); $brand: map.get($font-definition, brand); $plain: map.get($font-definition, plain); $bold: map.get($font-definition, bold); $medium: map.get($font-definition, medium); $regular: map.get($font-definition, regular); - $system-variables-prefix: map.get($theme, _mat-theming-internals-do-not-access, - typography-system-variables-prefix) or sys; $ref: ( md-ref-typeface: _generate-ref-typeface-tokens($brand, $plain, $bold, $medium, $regular) ); - $sys-typescale: m3-token-definitions.md-sys-typescale-values($ref); + @if (not $prefix) { + $prefix: map.get($theme, _mat-theming-internals-do-not-access, + typography-system-variables-prefix) or $_system-level-prefix; + } - @each $name, $value in $sys-typescale { - --#{$system-variables-prefix}-#{$name}: #{map.get($overrides, $name) or $value}; + @each $name, $value in m3-token-definitions.md-sys-typescale-values($ref) { + --#{$prefix}-#{$name}: #{map.get($overrides, $name) or $value}; } } @@ -182,6 +242,40 @@ $_cached-token-slots: null; @return $result; } +@mixin system-level-elevation($theme, $overrides: (), $prefix: $_system-level-prefix) { + $shadow-color: map.get( + $theme, _mat-theming-internals-do-not-access, color-tokens, (mdc, theme), shadow); + + @for $level from 0 through 24 { + $value: elevation.get-box-shadow($level, $shadow-color); + --#{$prefix}-elevation-shadow-level-#{$level}: #{$value}; + } + + @each $name, $value in m3-token-definitions.md-sys-elevation-values() { + $level: map.get($overrides, $name) or $value; + $value: elevation.get-box-shadow($level, $shadow-color); + --#{$prefix}-#{$name}: #{$value}; + } +} + +@mixin system-level-shape($theme, $overrides: (), $prefix: $_system-level-prefix) { + @each $name, $value in m3-token-definitions.md-sys-shape-values() { + --#{$prefix}-#{$name}: #{map.get($overrides, $name) or $value}; + } +} + +@mixin system-level-state($theme, $overrides: (), $prefix: $_system-level-prefix) { + @each $name, $value in m3-token-definitions.md-sys-state-values() { + --#{$prefix}-#{$name}: #{map.get($overrides, $name) or $value}; + } +} + +@mixin system-level-motion($theme, $overrides: (), $prefix: $_system-level-prefix) { + @each $name, $value in m3-token-definitions.md-sys-motion-values() { + --#{$prefix}-#{$name}: #{map.get($overrides, $name) or $value}; + } +} + @function _get-sys-color($type, $ref, $prefix) { $mdc-sys-color: if($type == dark, m3-token-definitions.md-sys-color-values-dark($ref), diff --git a/src/material/core/tokens/_token-definition.scss b/src/material/core/tokens/_token-definition.scss index 4e582dc2fec8..1208b89d9af9 100644 --- a/src/material/core/tokens/_token-definition.scss +++ b/src/material/core/tokens/_token-definition.scss @@ -6,6 +6,7 @@ @use '../m2/palette' as m2-palette; @use '../m2/theming' as m2-theming; @use '../m2/typography' as m2-typography; +@use '../style/sass-utils'; @use './m3/definitions' as m3-token-definitions; // Indicates whether we're building internally. Used for backwards compatibility. @@ -161,6 +162,11 @@ $_system-fallbacks: null; $color-key: map.get($pair, color); $opacity-key: map.get($pair, opacity); $color: map.get($tokens, $color-key); + + @if (sass-utils.is-css-var-name($color)) { + $color: var(#{$color}); + } + $opacity: map.get($opacity-lookup, $opacity-key); @if(meta.type-of($color) == 'color') { diff --git a/src/material/core/tokens/_token-utils.scss b/src/material/core/tokens/_token-utils.scss index 86690913b6f8..6ac1c4e1acdb 100644 --- a/src/material/core/tokens/_token-utils.scss +++ b/src/material/core/tokens/_token-utils.scss @@ -3,9 +3,11 @@ @use 'sass:string'; @use '../style/elevation'; @use '../style/sass-utils'; +@use './m3-tokens'; $_tokens: null; $_component-prefix: null; +$_system-fallbacks: m3-tokens.create-system-fallbacks(); // Sets the token prefix and map to use when creating token slots. @mixin use-tokens($prefix, $tokens) { @@ -19,8 +21,8 @@ $_component-prefix: null; } // Combines a prefix and a string to generate a CSS variable name for a token. -@function _get-css-variable($prefix, $name) { - @if $prefix == null or $name == null { +@function _create-var-name($prefix, $token) { + @if $prefix == null or $token == null { @error 'Must specify both prefix and name when generating token'; } @@ -31,69 +33,91 @@ $_component-prefix: null; $string-prefix: if($string-prefix == '', $part, '#{$string-prefix}-#{$part}'); } - @return string.unquote('--#{$string-prefix}-#{$name}'); + @return string.unquote('--#{$string-prefix}-#{$token}'); } -// Emits a slot for the given token, provided that it has a non-null value in the token map passed -// to `use-tokens`. -@mixin create-token-slot($property, $token, $emit-fallback: false) { +// Creates a CSS variable, including the fallback if provided. +@function _create-var($name, $fallback: null) { + @if ($fallback) { + @return var($name, $fallback); + } @else { + @return var($name); + } +} + +// Gets the value of the token given the current global context state. +@function _get-token-value($token, $fallback) { + $var-name: _create-var-name($_component-prefix, $token); + $fallback: _get-token-fallback($token, $fallback); + @return _create-var($var-name, $fallback); +} + +// Assertion mixin that throws an error if the global state has not been set up by wrapping +// calls with `use-tokens`. +@function _assert-use-tokens($token) { @if $_component-prefix == null or $_tokens == null { - @error '`create-token-slot` must be used within `use-tokens`'; + @error 'Function was not called within a wrapping call of `use-tokens`'; } @if not map.has-key($_tokens, $token) { @error 'Token #{$token} does not exist. Configured tokens are: #{map.keys($_tokens)}'; } - @if map.get($_tokens, $token) != null { - $fallback: null; - @if ($emit-fallback == true) { - $fallback: map.get($_tokens, $token); - } - @else if ($emit-fallback) { - $fallback: $emit-fallback; - } + @return true; +} - $var-name: _get-css-variable($_component-prefix, $token); - $var-reference: if($fallback == null, var(#{$var-name}), var(#{$var-name}, #{$fallback})); - #{$property}: #{$var-reference}; +// Emits a slot for the given token, provided that it has a non-null value in the token map passed +// to `use-tokens`. +// Accepts an optional fallback parameter to include in the CSS variable. +// If $fallback is `true`, then use the tokens map to get the fallback. +// TODO: Remove the use case where we accept "true" and handle any failing client screenshots +@mixin create-token-slot($property, $token, $fallback: null) { + $_assert: _assert-use-tokens($token); + @if map.get($_tokens, $token) != null { + #{$property}: #{_get-token-value($token, $fallback)}; } } // Returns the name of a token including the current prefix. Intended to be used in calculations // involving tokens. `create-token-slot` should be used when outputting tokens. @function get-token-variable-name($token) { - @if $_component-prefix == null or $_tokens == null { - @error '`get-token-variable` must be used within `use-tokens`'; - } - @if not map.has-key($_tokens, $token) { - @error 'Token #{$token} does not exist. Configured tokens are: #{map.keys($_tokens)}'; - } - - @return _get-css-variable($_component-prefix, $token); + $_assert: _assert-use-tokens($token); + @return _create-var-name($_component-prefix, $token); } // Returns a `var()` reference to a specific token. Intended for declarations // where the token has to be referenced as a part of a larger expression. -@function get-token-variable($token, $use-tokens-fallback: false, $fallback: null) { - @if $_component-prefix == null or $_tokens == null { - @error '`get-token-variable-reference` must be used within `use-tokens`'; - } - @if not map.has-key($_tokens, $token) { - @error 'Token #{$token} does not exist. Configured tokens are: #{map.keys($_tokens)}'; - } - - $var: get-token-variable-name($token); +// Accepts an optional fallback parameter to include in the CSS variable. +// If $fallback is `true`, then use the tokens map to get the fallback. +// TODO: Remove the use case where we accept "true" and handle any failing client screenshots +@function get-token-variable($token, $fallback: null) { + $_assert: _assert-use-tokens($token); + @return _get-token-value($token, $fallback); +} - @if ($use-tokens-fallback) { +// Gets the token's fallback value. Prefers adding a system-level fallback if one exists, otherwise +// use the provided fallback. +@function _get-token-fallback($token, $fallback: null) { + // If the $fallback is `true`, this is the component's signal to use the current token map value + @if ($fallback == true) { $fallback: map.get($_tokens, $token); } - @if ($fallback != null) { - @return var($var, $fallback); + // Check whether there's a system-level fallback. If not, return the optional + // provided fallback (otherwise null). + $sys-fallback: map.get($_system-fallbacks, $_component-prefix, $token); + @if (not $sys-fallback) { + @return $fallback; } - @else { - @return var($var); + + @if (sass-utils.is-css-var-name($sys-fallback)) { + @return _create-var($sys-fallback, $fallback); } + + // TODO(mat-app-theme): Return the system-level fallback. + // Changing this will affect clients that do not properly call theme mixins since the tokens + // will be undefined and now default to M3 system values, causing a number of screenshot failures. + // @return $sys-fallback; + @return $fallback; } // Outputs a map of tokens under a specific prefix. @@ -101,7 +125,7 @@ $_component-prefix: null; @if $tokens != null { @each $key, $value in $tokens { @if $value != null { - #{_get-css-variable($prefix, $key)}: #{$value}; + #{_create-var-name($prefix, $key)}: #{$value}; } } } diff --git a/src/material/core/tokens/m3/mat/_app.scss b/src/material/core/tokens/m3/mat/_app.scss index 9614d34575a4..07e945a46c92 100644 --- a/src/material/core/tokens/m3/mat/_app.scss +++ b/src/material/core/tokens/m3/mat/_app.scss @@ -1,4 +1,5 @@ @use 'sass:map'; +@use 'sass:meta'; @use '../../../style/elevation'; @use '../../token-definition'; @@ -17,7 +18,9 @@ $prefix: (mat, app); text-color: map.get($systems, md-sys-color, on-background), ); - @if ($shadow-color) { + // If the shadow-color is an actual color, convert it to a box-shadow value. Otherwise + // use the token value as itself (e,g. as a CSS var name). + @if (meta.type-of($shadow-color) == color) { @for $zValue from 0 through 24 { $shadow: elevation.get-box-shadow($zValue, $shadow-color); $tokens: map.set($tokens, 'elevation-shadow-level-#{$zValue}', $shadow);