_keys.scss 18 KB

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