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 | }
|