UNPKG

16.9 kBSCSSView Raw
1//
2// Copyright 2021 Google Inc.
3//
4// Permission is hereby granted, free of charge, to any person obtaining a copy
5// of this software and associated documentation files (the "Software"), to deal
6// in the Software without restriction, including without limitation the rights
7// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8// copies of the Software, and to permit persons to whom the Software is
9// furnished to do so, subject to the following conditions:
10//
11// The above copyright notice and this permission notice shall be included in
12// all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20// THE SOFTWARE.
21//
22
23@use 'sass:list';
24@use 'sass:map';
25@use 'sass:meta';
26@use 'sass:string';
27@use './custom-properties';
28
29/// A flat Map of keys and their values. Keys may be set to any static CSS
30/// value, or another key's name to resolve to that key's value.
31///
32/// @example - scss
33/// $_store: (
34/// primary: purple, // keys may be set to CSS values...
35/// button-color: primary, // ...or resolve to another key's value
36/// );
37///
38/// @type {Map}
39$_store: ();
40/// A flat Map of relationship links between keys. While key values may
41/// resolve to another key's value in the key store, the store does not
42/// preserve or infer relationships between keys.
43///
44/// Instead, this link Map records the original relationship between keys as
45/// their values are updated and potentially overridden with customizations.
46///
47/// @example - scss
48/// // Given these keys...
49/// $primary: purple;
50/// $button-color: $primary; // ...button-color is linked to the primary key
51///
52/// // A key store with value customizations may look like this:
53/// $_store: (
54/// primary: amber,
55/// button-color: teal, // the relationship is lost with a customization
56/// );
57///
58/// // The links Map preserves the relationship for custom property
59/// // generation, while the store Map is only focused on values.
60/// $_links: (
61/// button-color: primary,
62/// );
63///
64/// @type {Map}
65$_links: ();
66/// A map of key options. If a key has options, it will have an entry in this
67/// variable with a Map value with options for the key.
68///
69/// @example - scss
70/// // Option structure
71/// $_options: (
72/// key-name: (
73/// // An additional prefix to add when generating the varname of a
74/// // key's custom property
75/// // --mdc-<prefix>-key-name
76/// custom-property-prefix: prefix
77/// )
78/// );
79///
80/// @type {Map}
81$_options: ();
82
83/// Indicates whether or not the provided value is a registered key.
84///
85/// @param {String} $key - One or more key parts to check
86/// @return {Bool} True if the key is registered, or false if it is not.
87@function is-key($key...) {
88 $key: combine($key...);
89 @return map.has-key($_store, $key);
90}
91
92/// Retrieves a List of all keys matching the provided key group prefix.
93///
94/// @example - scss
95/// $keys: get-keys(typography);
96/// // (typography-headline,
97/// // typography-body,
98/// // typography-body-font,
99/// // typography-body-size)
100///
101/// @param {String} $group - Optional group prefix to search by. If ommitted,
102/// all registered keys will be returned.
103/// @return {List} A List of all keys matching the group prefix.
104@function get-keys($group: '') {
105 $keys: ();
106 @each $key in map.keys($_store) {
107 @if string.index($key, $group) == 1 {
108 $keys: list.append($keys, $key);
109 }
110 }
111
112 @return $keys;
113}
114
115/// Registers a Map of keys and their values with the key store. Key values may
116/// either be CSS values or other key strings.
117///
118/// @example - scss
119/// @include set-values((
120/// primary: teal,
121/// label-color: primary
122/// ));
123///
124/// Options may also be added for each key by providing an `$options` parameter.
125///
126/// @example - scss
127/// @include set-values(
128/// $key-map,
129/// $options: (
130/// // An additional prefix to add when generating the varname of a
131/// // key's custom property
132/// // --mdc-<prefix>-key-name
133/// custom-property-prefix: prefix
134/// )
135/// );
136///
137/// Note that this mixin only sets key values. If a key points to another key,
138/// it does not link those keys when custom properties are emitted. Use
139/// `add-link()` or `register-theme()` to create links between keys.
140///
141/// @see {mixin} set-value
142/// @see {mixin} add-link
143/// @see {mixin} register-theme
144///
145/// @param {Map} $key-map - A Map of keys to register.
146/// @param {Map} $options [null] - Optional Map of options to add for each key.
147@mixin set-values($key-map, $options: null) {
148 $unused: set-values($key-map, $options: $options);
149}
150
151/// Function version of `set-values()`.
152///
153/// Mixins cannot be invoked within functions in Sass. Use this when
154/// `set-values()` must be used within a function. The return value may be
155/// discarded or re-assigned to the `$key-map` provided.
156///
157/// @example - scss
158/// @function foo() {
159/// $unused: set-values((primary: teal));
160/// }
161///
162/// @function bar() {
163/// $key-map: (primary: teal);
164/// $key-map: set-values($key-map);
165/// }
166///
167/// @see {mixin} set-values
168///
169/// @return {Map} `$key-map`, unmodified, for convenience.
170@function set-values($key-map, $options: null) {
171 @each $key, $value in $key-map {
172 $key: set-value($key, $value, $options: $options);
173 }
174
175 @return $key-map;
176}
177
178/// Sets the value of a key. Key values may either be CSS values or other key
179/// strings.
180///
181/// @example - scss
182/// @include set-value(primary, teal);
183/// @include set-value(label-color, primary);
184///
185/// Options may also be added for each key by providing an `$options` parameter.
186///
187/// @example - scss
188/// @include set-value(key-name, teal, $options: (
189/// // An additional prefix to add when generating the varname of a
190/// // key's custom property
191/// // --mdc-<prefix>-key-name
192/// custom-property-prefix: prefix
193/// ));
194///
195/// Note that this mixin only sets the key's value. If the key points to another
196/// key, it does not link those keys when custom properties are emitted. Use
197/// `add-link()` or `register-theme()` to create links between keys.
198///
199/// @see {mixin} add-link
200/// @see {mixin} register-theme
201///
202/// @param {String} $key - The key to set a value for.
203/// @param {*} $value - The value of the key.
204/// @param {Map} $options [null] - Optional Map of options to add for each key.
205@mixin set-value($key, $value, $options: null) {
206 $unused: set-value($key, $value, $options: $options);
207}
208
209/// Function version of `set-value()`.
210///
211/// Mixins cannot be invoked within functions in Sass. Use this when
212/// `set-value()` must be used within a function. The return value may be
213/// discarded or re-assigned to the `$key` provided.
214///
215/// @example - scss
216/// @function foo() {
217/// $unused: set-value(primary, teal);
218/// }
219///
220/// @function bar() {
221/// $key: primary;
222/// $key: set-value($key, teal);
223/// }
224///
225/// @see {mixin} set-value
226///
227/// @return {String} `$key`, unmodified, for convenience.
228@function set-value($key, $value, $options: null) {
229 // Use !global to avoid shadowing
230 // https://sass-lang.com/documentation/variables#shadowing
231 $_store: map.set($_store, $key, $value) !global;
232 @if $options {
233 $_options: map.set($_options, $key, $options) !global;
234 }
235
236 @return $key;
237}
238
239/// Add a link between two keys.
240///
241/// When keys are linked and chained custom properties are emitted, the value
242/// of `$key` will always include the `var()` function of its linked key, even
243/// if it overrides its linked key's value.
244///
245/// @example - scss
246/// @include add-link(label-color, primary);
247/// @include set-values((
248/// primary: teal,
249/// label-color: amber
250/// ));
251///
252/// .primary {
253/// @include theme.property(color, primary);
254/// }
255///
256/// .label-color {
257/// @include theme.property(color, label-color);
258/// }
259///
260/// @example - css
261/// .primary {
262/// color: var(--primary, teal);
263/// }
264///
265/// .label-color {
266/// color: var(--label-color, var(--primary, amber));
267/// }
268///
269///
270/// If a key does not already have a value set, its value will be set to the
271/// linked key provided.
272///
273/// @param {String} $key - The key to add a link to.
274/// @param {String} $link - The name to link to `$key`.
275/// @throw When attempting to change the link of a key that has already been
276/// linked.
277@mixin add-link($key, $link) {
278 $unused: add-link($key, $link);
279}
280
281/// Function version of `add-link()`.
282///
283/// Mixins cannot be invoked within functions in Sass. Use this when
284/// `add-link()` must be used within a function. The return value may be
285/// discarded or re-assigned to the `$key` provided.
286///
287/// @example - scss
288/// @function foo() {
289/// $unused: add-link(label-color, primary);
290/// }
291///
292/// @function bar() {
293/// $key: label-color;
294/// $key: set-value($key, primary);
295/// }
296///
297/// @see {mixin} `add-link()`
298///
299/// @return {String} `$key` for convenience.
300@function add-link($key, $link) {
301 @if map.has-key($_links, $key) {
302 @error '#{$key} already has a link';
303 }
304
305 // Use !global to avoid shadowing
306 // https://sass-lang.com/documentation/variables#shadowing
307 $_links: map.set($_links, $key, $link) !global;
308 @if not map.has-key($_store, $key) {
309 $key: set-value($key, $link);
310 }
311
312 @return $key;
313}
314
315/// Resolve a key to its CSS value. This may be a static CSS value or a dynamic
316/// `var()` value.
317///
318/// The value that this function returns may change depending on configuration
319/// options if a key's value points to another key.
320///
321/// To always retrieve the static CSS value a key resolves to, even if it points
322/// to another key, provide `$deep: true` as a parameter to the function.
323///
324/// @param {String...} $key - One or more key parts to resolve to a CSS value.
325/// @param {Bool} $deep [false] - Set to true as a named parameter to always
326/// resolve the key to its static CSS value and not a dynamic `var()` value.
327/// @return {*} The value the key resolves to. This may be `null` if the key
328/// (or the key it points to) has not been registered.
329@function resolve($key...) {
330 $deep: map.get(meta.keywords($key), deep);
331 $key: combine($key...);
332
333 $value: map.get($_store, $key);
334 @if is-key($value) {
335 $value: resolve($value);
336 }
337
338 @return $value;
339}
340
341/// Register a `$theme` Map variable's keys. This should only be done once in
342/// the `theme-styles()` mixin with the canonical `$theme` Map to initialize
343/// default values and linked keys.
344///
345/// @example - scss
346/// @mixin theme-styles($theme: button-filled-theme.$light-theme) {
347/// @include keys.register-theme($theme, button-filled);
348/// @include button-filled-theme.theme($theme);
349/// }
350///
351/// A component's `$theme` Map may have shared keys (such as color, shape, and
352/// typography) that need linked before user customization with the `theme()`
353/// mixin.
354///
355/// The `register-theme()` mixin handles adding these links with `add-link()`
356/// dynamically from a canonical `$theme` configuration provided by a trusted
357/// source in `theme-styles()`. Subsequent calls to `theme()` will not invoke
358/// `register-theme()` or change the linked keys' registration.
359///
360/// @param {Map} $theme - The theme Map to register keys for.
361/// @param {String} $prefix [null] - Optional prefix to prepend before each key.
362/// @param {Map} $options [null] - Optional Map of options to add for each key.
363@mixin register-theme($theme, $prefix: null, $options: null) {
364 // The first $theme Map received in theme-styles() should be used to
365 // register keys.
366 // Subsequent calls to theme() to customize key values will not be
367 // wrapped within theme-styles() and will not change the registered
368 // key values (or more importantly, their links), since
369 // customizations may be simple one-offs.
370 @each $key, $value in $theme {
371 @if $value != null {
372 $key: combine($prefix, $key);
373 @include set-value($key, $value, $options: $options);
374 @if is-key($value) {
375 @include add-link($key, $link: $value);
376 }
377 }
378 }
379}
380
381/// Create and resolve custom properties from a user-provided `$theme` Map
382/// variable. The created custom properties are returned in a Map that matches
383/// the key structure of `$theme`.
384///
385/// This function should be used within a `theme()` mixin after validation and
386/// before providing any values to subsequent mixins. This will ensure that all
387/// values are custom properties to support runtime theming.
388///
389/// @example - scss
390/// $light-theme: (
391/// label-color: on-primary
392/// );
393///
394/// @mixin theme($theme) {
395/// $theme: keys.create-theme-properties($theme, button-filled);
396/// /*(
397/// label-color: (
398/// varname: --mdc-button-filled-label-color,
399/// fallback: (
400/// varname: --mdc-theme-on-primary,
401/// fallback: white,
402/// )
403/// )
404/// )*/
405/// }
406///
407/// @param {Map} $theme - The theme Map to create custom properties for.
408/// @param {String} $prefix [null] - Optional prefix to prepend for each key's
409/// custom property.
410/// @return {Map} A similar `$theme` Map whose values are replaced with the
411/// newly created and resolved custom properties.
412@function create-theme-properties($theme, $prefix: null) {
413 $theme-with-props: ();
414 @each $name, $value in $theme {
415 @if $value != null {
416 @if is-key($value) {
417 $value: create-custom-property($value);
418 }
419
420 $key: combine($prefix, $name);
421
422 @if _is-map($value) {
423 @each $k, $v in $value {
424 $theme-with-props: map.set(
425 $theme-with-props,
426 $name,
427 $k,
428 custom-properties.create(_create-varname(combine($key, $k)), $v)
429 );
430 }
431 } @else {
432 $theme-with-props: map.set(
433 $theme-with-props,
434 $name,
435 custom-properties.create(_create-varname($key), $value)
436 );
437 }
438 }
439 }
440
441 @return $theme-with-props;
442}
443
444/// Create a custom property for a key that represents the key's linked
445/// relationships and final resolved static value.
446///
447/// This function ignores customization options and is intended to return the
448/// most accurate data structure representation of a key. Customization options
449/// (such as custom property configuration) will change how the returned value
450/// is emitted.
451///
452/// @param {$tring...} $key - One or more key parts to create a custom property
453/// for.
454/// @return {Map} A custom property Map for the key.
455@function create-custom-property($key...) {
456 $key: combine($key...);
457 $prop: custom-properties.create(_create-varname($key));
458 $link: map.get($_links, $key);
459 @if $link {
460 $prop: custom-properties.set-fallback($prop, create-custom-property($link));
461 }
462
463 @return custom-properties.set-fallback($prop, resolve($key, $deep: true));
464}
465
466@mixin declare-custom-properties($theme, $prefix: null) {
467 $theme: create-theme-properties($theme, $prefix);
468
469 @each $key, $value in $theme {
470 @if _is-map($value) {
471 @each $k, $v in $value {
472 @include custom-properties.declaration($v);
473 }
474 } @else {
475 @include custom-properties.declaration($value);
476 }
477 }
478}
479
480/// Creates a custom property varname for a key. This function will add a key's
481/// option's `custom-property-prefix` if it exists.
482///
483/// @param {String...} $key - One or more key parts to create a varname for.
484/// @return {String} The key's custom property varname.
485@function _create-varname($key...) {
486 $key: combine($key...);
487 $prefix: map.get($_options, $key, custom-property-prefix);
488 @if $prefix {
489 $key: combine($prefix, $key);
490 }
491
492 @return custom-properties.create-varname($key);
493}
494
495/// Combines one or more key parts into a key.
496///
497/// @example - scss
498/// $key: combine(body, font-size);
499/// // body-font-size
500///
501/// @param {String...} $parts - Arbitrary number of string key parts to combine.
502/// @return {String} A combined key string.
503@function combine($parts...) {
504 // Allow extra keywords to be passed to other functions without impacting this
505 // function, which does not expect any keywords.
506 $unused: meta.keywords($parts);
507 $key: '';
508 @each $part in $parts {
509 @if $part {
510 @if $key == '' {
511 $key: $part;
512 } @else {
513 $key: #{$key}-#{$part};
514 }
515 }
516 }
517
518 @return $key;
519}
520
521@function _is-map($map) {
522 @return meta.type-of($map) == 'map' and not
523 custom-properties.is-custom-prop($map);
524}