| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- //
- // Copyright 2021 Google Inc.
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- //
- @use 'sass:list';
- @use 'sass:map';
- @use 'sass:meta';
- @use 'sass:string';
- @use './custom-properties';
- /// A flat Map of keys and their values. Keys may be set to any static CSS
- /// value, or another key's name to resolve to that key's value.
- ///
- /// @example - scss
- /// $_store: (
- /// primary: purple, // keys may be set to CSS values...
- /// button-color: primary, // ...or resolve to another key's value
- /// );
- ///
- /// @type {Map}
- $_store: ();
- /// A flat Map of relationship links between keys. While key values may
- /// resolve to another key's value in the key store, the store does not
- /// preserve or infer relationships between keys.
- ///
- /// Instead, this link Map records the original relationship between keys as
- /// their values are updated and potentially overridden with customizations.
- ///
- /// @example - scss
- /// // Given these keys...
- /// $primary: purple;
- /// $button-color: $primary; // ...button-color is linked to the primary key
- ///
- /// // A key store with value customizations may look like this:
- /// $_store: (
- /// primary: amber,
- /// button-color: teal, // the relationship is lost with a customization
- /// );
- ///
- /// // The links Map preserves the relationship for custom property
- /// // generation, while the store Map is only focused on values.
- /// $_links: (
- /// button-color: primary,
- /// );
- ///
- /// @type {Map}
- $_links: ();
- /// A map of key options. If a key has options, it will have an entry in this
- /// variable with a Map value with options for the key.
- ///
- /// @example - scss
- /// // Option structure
- /// $_options: (
- /// key-name: (
- /// // An additional prefix to add when generating the varname of a
- /// // key's custom property
- /// // --mdc-<prefix>-key-name
- /// custom-property-prefix: prefix
- /// )
- /// );
- ///
- /// @type {Map}
- $_options: ();
- /// Indicates whether or not the provided value is a registered key.
- ///
- /// @param {String} $key - One or more key parts to check
- /// @return {Bool} True if the key is registered, or false if it is not.
- @function is-key($key...) {
- $key: combine($key...);
- @return map.has-key($_store, $key);
- }
- /// Retrieves a List of all keys matching the provided key group prefix.
- ///
- /// @example - scss
- /// $keys: get-keys(typography);
- /// // (typography-headline,
- /// // typography-body,
- /// // typography-body-font,
- /// // typography-body-size)
- ///
- /// @param {String} $group - Optional group prefix to search by. If ommitted,
- /// all registered keys will be returned.
- /// @return {List} A List of all keys matching the group prefix.
- @function get-keys($group: '') {
- $keys: ();
- @each $key in map.keys($_store) {
- @if string.index($key, $group) == 1 {
- $keys: list.append($keys, $key);
- }
- }
- @return $keys;
- }
- /// Registers a Map of keys and their values with the key store. Key values may
- /// either be CSS values or other key strings.
- ///
- /// @example - scss
- /// @include set-values((
- /// primary: teal,
- /// label-color: primary
- /// ));
- ///
- /// Options may also be added for each key by providing an `$options` parameter.
- ///
- /// @example - scss
- /// @include set-values(
- /// $key-map,
- /// $options: (
- /// // An additional prefix to add when generating the varname of a
- /// // key's custom property
- /// // --mdc-<prefix>-key-name
- /// custom-property-prefix: prefix
- /// )
- /// );
- ///
- /// Note that this mixin only sets key values. If a key points to another key,
- /// it does not link those keys when custom properties are emitted. Use
- /// `add-link()` or `register-theme()` to create links between keys.
- ///
- /// @see {mixin} set-value
- /// @see {mixin} add-link
- /// @see {mixin} register-theme
- ///
- /// @param {Map} $key-map - A Map of keys to register.
- /// @param {Map} $options [null] - Optional Map of options to add for each key.
- @mixin set-values($key-map, $options: null) {
- $unused: set-values($key-map, $options: $options);
- }
- /// Function version of `set-values()`.
- ///
- /// Mixins cannot be invoked within functions in Sass. Use this when
- /// `set-values()` must be used within a function. The return value may be
- /// discarded or re-assigned to the `$key-map` provided.
- ///
- /// @example - scss
- /// @function foo() {
- /// $unused: set-values((primary: teal));
- /// }
- ///
- /// @function bar() {
- /// $key-map: (primary: teal);
- /// $key-map: set-values($key-map);
- /// }
- ///
- /// @see {mixin} set-values
- ///
- /// @return {Map} `$key-map`, unmodified, for convenience.
- @function set-values($key-map, $options: null) {
- @each $key, $value in $key-map {
- $key: set-value($key, $value, $options: $options);
- }
- @return $key-map;
- }
- /// Sets the value of a key. Key values may either be CSS values or other key
- /// strings.
- ///
- /// @example - scss
- /// @include set-value(primary, teal);
- /// @include set-value(label-color, primary);
- ///
- /// Options may also be added for each key by providing an `$options` parameter.
- ///
- /// @example - scss
- /// @include set-value(key-name, teal, $options: (
- /// // An additional prefix to add when generating the varname of a
- /// // key's custom property
- /// // --mdc-<prefix>-key-name
- /// custom-property-prefix: prefix
- /// ));
- ///
- /// Note that this mixin only sets the key's value. If the key points to another
- /// key, it does not link those keys when custom properties are emitted. Use
- /// `add-link()` or `register-theme()` to create links between keys.
- ///
- /// @see {mixin} add-link
- /// @see {mixin} register-theme
- ///
- /// @param {String} $key - The key to set a value for.
- /// @param {*} $value - The value of the key.
- /// @param {Map} $options [null] - Optional Map of options to add for each key.
- @mixin set-value($key, $value, $options: null) {
- $unused: set-value($key, $value, $options: $options);
- }
- /// Function version of `set-value()`.
- ///
- /// Mixins cannot be invoked within functions in Sass. Use this when
- /// `set-value()` must be used within a function. The return value may be
- /// discarded or re-assigned to the `$key` provided.
- ///
- /// @example - scss
- /// @function foo() {
- /// $unused: set-value(primary, teal);
- /// }
- ///
- /// @function bar() {
- /// $key: primary;
- /// $key: set-value($key, teal);
- /// }
- ///
- /// @see {mixin} set-value
- ///
- /// @return {String} `$key`, unmodified, for convenience.
- @function set-value($key, $value, $options: null) {
- // Use !global to avoid shadowing
- // https://sass-lang.com/documentation/variables#shadowing
- $_store: map.set($_store, $key, $value) !global;
- @if $options {
- $_options: map.set($_options, $key, $options) !global;
- }
- @return $key;
- }
- /// Add a link between two keys.
- ///
- /// When keys are linked and chained custom properties are emitted, the value
- /// of `$key` will always include the `var()` function of its linked key, even
- /// if it overrides its linked key's value.
- ///
- /// @example - scss
- /// @include add-link(label-color, primary);
- /// @include set-values((
- /// primary: teal,
- /// label-color: amber
- /// ));
- ///
- /// .primary {
- /// @include theme.property(color, primary);
- /// }
- ///
- /// .label-color {
- /// @include theme.property(color, label-color);
- /// }
- ///
- /// @example - css
- /// .primary {
- /// color: var(--primary, teal);
- /// }
- ///
- /// .label-color {
- /// color: var(--label-color, var(--primary, amber));
- /// }
- ///
- ///
- /// If a key does not already have a value set, its value will be set to the
- /// linked key provided.
- ///
- /// @param {String} $key - The key to add a link to.
- /// @param {String} $link - The name to link to `$key`.
- /// @throw When attempting to change the link of a key that has already been
- /// linked.
- @mixin add-link($key, $link) {
- $unused: add-link($key, $link);
- }
- /// Function version of `add-link()`.
- ///
- /// Mixins cannot be invoked within functions in Sass. Use this when
- /// `add-link()` must be used within a function. The return value may be
- /// discarded or re-assigned to the `$key` provided.
- ///
- /// @example - scss
- /// @function foo() {
- /// $unused: add-link(label-color, primary);
- /// }
- ///
- /// @function bar() {
- /// $key: label-color;
- /// $key: set-value($key, primary);
- /// }
- ///
- /// @see {mixin} `add-link()`
- ///
- /// @return {String} `$key` for convenience.
- @function add-link($key, $link) {
- @if map.has-key($_links, $key) {
- @error '#{$key} already has a link';
- }
- // Use !global to avoid shadowing
- // https://sass-lang.com/documentation/variables#shadowing
- $_links: map.set($_links, $key, $link) !global;
- @if not map.has-key($_store, $key) {
- $key: set-value($key, $link);
- }
- @return $key;
- }
- /// Resolve a key to its CSS value. This may be a static CSS value or a dynamic
- /// `var()` value.
- ///
- /// The value that this function returns may change depending on configuration
- /// options if a key's value points to another key.
- ///
- /// To always retrieve the static CSS value a key resolves to, even if it points
- /// to another key, provide `$deep: true` as a parameter to the function.
- ///
- /// @param {String...} $key - One or more key parts to resolve to a CSS value.
- /// @param {Bool} $deep [false] - Set to true as a named parameter to always
- /// resolve the key to its static CSS value and not a dynamic `var()` value.
- /// @return {*} The value the key resolves to. This may be `null` if the key
- /// (or the key it points to) has not been registered.
- @function resolve($key...) {
- $deep: map.get(meta.keywords($key), deep);
- $key: combine($key...);
- $value: map.get($_store, $key);
- @if is-key($value) {
- $value: resolve($value);
- }
- @return $value;
- }
- /// Register a `$theme` Map variable's keys. This should only be done once in
- /// the `theme-styles()` mixin with the canonical `$theme` Map to initialize
- /// default values and linked keys.
- ///
- /// @example - scss
- /// @mixin theme-styles($theme: button-filled-theme.$light-theme) {
- /// @include keys.register-theme($theme, button-filled);
- /// @include button-filled-theme.theme($theme);
- /// }
- ///
- /// A component's `$theme` Map may have shared keys (such as color, shape, and
- /// typography) that need linked before user customization with the `theme()`
- /// mixin.
- ///
- /// The `register-theme()` mixin handles adding these links with `add-link()`
- /// dynamically from a canonical `$theme` configuration provided by a trusted
- /// source in `theme-styles()`. Subsequent calls to `theme()` will not invoke
- /// `register-theme()` or change the linked keys' registration.
- ///
- /// @param {Map} $theme - The theme Map to register keys for.
- /// @param {String} $prefix [null] - Optional prefix to prepend before each key.
- /// @param {Map} $options [null] - Optional Map of options to add for each key.
- @mixin register-theme($theme, $prefix: null, $options: null) {
- // The first $theme Map received in theme-styles() should be used to
- // register keys.
- // Subsequent calls to theme() to customize key values will not be
- // wrapped within theme-styles() and will not change the registered
- // key values (or more importantly, their links), since
- // customizations may be simple one-offs.
- @each $key, $value in $theme {
- @if $value != null {
- $key: combine($prefix, $key);
- @include set-value($key, $value, $options: $options);
- @if is-key($value) {
- @include add-link($key, $link: $value);
- }
- }
- }
- }
- /// Create and resolve custom properties from a user-provided `$theme` Map
- /// variable. The created custom properties are returned in a Map that matches
- /// the key structure of `$theme`.
- ///
- /// This function should be used within a `theme()` mixin after validation and
- /// before providing any values to subsequent mixins. This will ensure that all
- /// values are custom properties to support runtime theming.
- ///
- /// @example - scss
- /// $light-theme: (
- /// label-color: on-primary
- /// );
- ///
- /// @mixin theme($theme) {
- /// $theme: keys.create-theme-properties($theme, button-filled);
- /// /*(
- /// label-color: (
- /// varname: --mdc-button-filled-label-color,
- /// fallback: (
- /// varname: --mdc-theme-on-primary,
- /// fallback: white,
- /// )
- /// )
- /// )*/
- /// }
- ///
- /// @param {Map} $theme - The theme Map to create custom properties for.
- /// @param {String} $prefix [null] - Optional prefix to prepend for each key's
- /// custom property.
- /// @return {Map} A similar `$theme` Map whose values are replaced with the
- /// newly created and resolved custom properties.
- @function create-theme-properties($theme, $prefix: null) {
- $theme-with-props: ();
- @each $name, $value in $theme {
- @if $value != null {
- @if is-key($value) {
- $value: create-custom-property($value);
- }
- $key: combine($prefix, $name);
- @if _is-map($value) {
- @each $k, $v in $value {
- $theme-with-props: map.set(
- $theme-with-props,
- $name,
- $k,
- custom-properties.create(_create-varname(combine($key, $k)), $v)
- );
- }
- } @else {
- $theme-with-props: map.set(
- $theme-with-props,
- $name,
- custom-properties.create(_create-varname($key), $value)
- );
- }
- }
- }
- @return $theme-with-props;
- }
- /// Create a custom property for a key that represents the key's linked
- /// relationships and final resolved static value.
- ///
- /// This function ignores customization options and is intended to return the
- /// most accurate data structure representation of a key. Customization options
- /// (such as custom property configuration) will change how the returned value
- /// is emitted.
- ///
- /// @param {$tring...} $key - One or more key parts to create a custom property
- /// for.
- /// @return {Map} A custom property Map for the key.
- @function create-custom-property($key...) {
- $key: combine($key...);
- $prop: custom-properties.create(_create-varname($key));
- $link: map.get($_links, $key);
- @if $link {
- $prop: custom-properties.set-fallback($prop, create-custom-property($link));
- }
- @return custom-properties.set-fallback($prop, resolve($key, $deep: true));
- }
- @mixin declare-custom-properties($theme, $prefix: null) {
- $theme: create-theme-properties($theme, $prefix);
- @each $key, $value in $theme {
- @if _is-map($value) {
- @each $k, $v in $value {
- @include custom-properties.declaration($v);
- }
- } @else {
- @include custom-properties.declaration($value);
- }
- }
- }
- /// Creates a custom property varname for a key. This function will add a key's
- /// option's `custom-property-prefix` if it exists.
- ///
- /// @param {String...} $key - One or more key parts to create a varname for.
- /// @return {String} The key's custom property varname.
- @function _create-varname($key...) {
- $key: combine($key...);
- $prefix: map.get($_options, $key, custom-property-prefix);
- @if $prefix {
- $key: combine($prefix, $key);
- }
- @return custom-properties.create-varname($key);
- }
- /// Combines one or more key parts into a key.
- ///
- /// @example - scss
- /// $key: combine(body, font-size);
- /// // body-font-size
- ///
- /// @param {String...} $parts - Arbitrary number of string key parts to combine.
- /// @return {String} A combined key string.
- @function combine($parts...) {
- // Allow extra keywords to be passed to other functions without impacting this
- // function, which does not expect any keywords.
- $unused: meta.keywords($parts);
- $key: '';
- @each $part in $parts {
- @if $part {
- @if $key == '' {
- $key: $part;
- } @else {
- $key: #{$key}-#{$part};
- }
- }
- }
- @return $key;
- }
- @function _is-map($map) {
- @return meta.type-of($map) == 'map' and not
- custom-properties.is-custom-prop($map);
- }
- /// Transform a user-provided `$theme` map's values into `var()` custom property
- /// values.
- ///
- /// Note: this function does NOT create fallback values so it should not be used
- /// in contexts where IE11 support is needed. For those cases use
- /// `keys.create-theme-properties` instead.
- ///
- /// Use this function in `theme-styles()` mixins to transform values into
- /// custom property `var()` "slots" that can subsequently be styled via
- /// `keys.declare-custom-properties` in the `theme()` mixin by the user.
- ///
- /// @example - scss
- /// $light-theme: (
- /// label-color: purple
- /// );
- ///
- /// @mixin theme-styles($theme) {
- /// $theme: keys.create-theme-vars($theme, button);
- ///
- /// .foo {
- /// color: map.get($theme, label-color);
- /// }
- /// }
- ///
- /// @example - css
- /// .foo {
- /// color: var(--mdc-button-label-color, purple);
- /// }
- ///
- /// @param {Map} $theme - The theme Map to transform values into custom property
- /// `var()`s.
- /// @param {String} $prefix - Component and variant prefix to prepend for each
- /// token's custom property name.
- /// @return {Map} The provided `$theme` Map whose values are replaced with the
- /// `var()` custom properties.
- @function create-theme-vars($theme, $prefix) {
- @each $key, $value in $theme {
- @if $value != null {
- $token: combine($prefix, $key);
- @if meta.type-of($value) == 'map' {
- $value: create-theme-vars($value, $token);
- } @else {
- $value: custom-properties.create-var(
- custom-properties.create($token, $value)
- );
- }
- $theme: map.set($theme, $key, $value);
- }
- }
- @return $theme;
- }
|