From 1afd9259adacf4cdf429dd0648b82bd23b3cdad6 Mon Sep 17 00:00:00 2001 From: Daniel Freedman Date: Mon, 13 Feb 2023 11:01:14 -0800 Subject: [PATCH] feat(shape, string-ext): Allow shape corners to fall back to a single custom property - shape: new `resolve-tokens()` function will use a fallback variable for the generated corners - string-ext: add `split()` function to split a string into a list of ordered substrings PiperOrigin-RevId: 509277443 --- sass/_shape.scss | 91 ++++++++++++++++++++++++++ sass/_string-ext.scss | 42 ++++++++++++ sass/test/_shape.test.scss | 109 ++++++++++++++++++++++++++++++++ sass/test/_string-ext.test.scss | 36 +++++++++++ 4 files changed, 278 insertions(+) diff --git a/sass/_shape.scss b/sass/_shape.scss index 16221b69d3..109a8a3aea 100644 --- a/sass/_shape.scss +++ b/sass/_shape.scss @@ -6,11 +6,15 @@ @use 'sass:list'; @use 'sass:map'; @use 'sass:meta'; +@use 'sass:string'; +@use './string-ext'; @use './var'; /// Resolves one or more shape tokens and expands them into 4 separate logical /// tokens for each corner. /// +/// @deprecated - use resolve-tokens instead +/// /// @example - scss /// $theme: (container-shape: (4px 4px 0 0)); /// $theme: resolve-theme( @@ -54,6 +58,93 @@ @return $theme; } +/// Resolves one or more shape tokens and expands them into 4 separate logical +/// tokens for each corner. +/// +/// Must be called after `theme.create-theme-vars()` +/// +/// @example - scss +/// $theme: (container-shape: (4px 4px 0 0)); +/// $tokens: theme.create-theme-vars($theme, component); +/// $tokens: shape.resolve-tokens( +/// $tokens, +/// container-shape, +/// ); +/// +/// // ( +/// // container-shape-start-start: var(--md-component-container-shape-start-start, var(--md-component-container-shape, 4px)), +/// // container-shape-start-end: var(--md-component-container-shape-start-end, var(--md-component-container-shape, 4px)), +/// // container-shape-end-end: var(--md-component-container-shape-end-start, var(--md-component-container-shape, 0)), +/// // container-shape-end-start: var(--md-component-container-shape-end-end, var(--md-component-container-shape, 0)), +/// // ) +/// +/// @param {Map} $tokens - The theme to resolve tokens for. +/// @param {String...} $shape-tokens - The shape tokens to resolve. +/// @return {Map} The theme with resolved shape tokens. +@function resolve-tokens($tokens, $shape-tokens...) { + @each $token in $shape-tokens { + $shape: map.get($tokens, $token); + @if $shape != null { + @if not var.is-var($shape) { + @error 'resolve-tokens() must be called after theme.create-theme-vars()'; + } + $shape-name: var.name($shape); + // fallback may be a stringified list, split into sass list again + $shape: string-ext.split(var.fallback($shape)); + $shape-theme: resolver( + $shape: $shape, + ); + + @each $key, $value in $shape-theme { + $corner-name: '#{$shape-name}-#{$key}'; + $shape-theme: map.set( + $shape-theme, + $key, + var.create($corner-name, var.create($shape-name, $value)) + ); + } + + // Add resolved values, but allow $theme to override the results if needed. + $tokens: map.merge( + ( + '#{$token}-start-start': map.get($shape-theme, start-start), + '#{$token}-start-end': map.get($shape-theme, start-end), + '#{$token}-end-end': map.get($shape-theme, end-end), + '#{$token}-end-start': map.get($shape-theme, end-start), + ), + $tokens + ); + + $tokens: map.remove($tokens, $token); + } + } + + @return $tokens; +} + +/// Generate a shape token list from the expanded corners. +/// +/// @example - scss +/// $shape: shape.corners-to-shape-token(--md-component-container-shape); +/// // ( +/// // var(--md-component-container-shape-start-start), +/// // var(--md-component-container-shape-start-end), +/// // var(--md-component-container-shape-end-end), +/// // var(--md-component-container-shape-end-start), +/// // ) +/// foo.theme((shape: $shape)) +/// +/// @param {String} $shape-token - The shape variable the corners are generated from +/// @return {List} A list that can be processed by `expand-corners` +@function corners-to-shape-token($shape-token) { + @return ( + var.create('#{$shape-token}-start-start'), + var.create('#{$shape-token}-start-end'), + var.create('#{$shape-token}-end-end'), + var.create('#{$shape-token}-end-start') + ); +} + /// Resolves a shape value by expanding it into logical values for each corner. /// /// @param {Number|List} $shape - The shape token's value. diff --git a/sass/_string-ext.scss b/sass/_string-ext.scss index 274670fa79..0dc4b62c12 100644 --- a/sass/_string-ext.scss +++ b/sass/_string-ext.scss @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // +@use 'sass:list'; @use 'sass:string'; /// Checks if a string starts with a given prefix. @@ -149,3 +150,44 @@ @return $before + $replacement + $after; } + +/// Divides a string into an ordered list of substrings. +/// +/// @example - scss +/// @debug split("1px 2px 3px 4px"); // (1px 2px 3px 4px) +/// @debug split("1px, 2px, 3px, 4px"); // (1px 2px 3px 4px) +/// @debug split("1px/2px/3px/4px"); // (1px 2px 3px 4px) +/// +/// @param {String} $str - The string to split +/// @return {List} The list of substrings +@function split($str) { + $list: (); + $item: ''; + // list separator precedence is comma, slash, space + $separator: ' '; + @if string.index($str, ',') { + $separator: ','; + } @else if string.index($str, '/') { + $separator: '/'; + } + @for $i from 1 through string.length($str) { + $chr: string.slice($str, $i, $i); + @if $chr == $separator { + @if $item != '' { + // remove surrounding whitespace + $item: string.unquote(trim($item, ' ')); + $list: list.append($list, $item); + } + $item: ''; + } @else { + $item: $item + $chr; + } + } + // append final item + @if $item != '' { + // remove surrounding whitespace + $item: string.unquote(trim($item, ' ')); + $list: list.append($list, $item); + } + @return $list; +} diff --git a/sass/test/_shape.test.scss b/sass/test/_shape.test.scss index f82a438a13..9862b6609b 100644 --- a/sass/test/_shape.test.scss +++ b/sass/test/_shape.test.scss @@ -3,11 +3,14 @@ // SPDX-License-Identifier: Apache-2.0 // +@use 'sass:list'; @use 'sass:map'; @use 'sass:meta'; @use 'true' as test; @use '../resolvers'; @use '../shape'; +@use '../var'; +@use '../theme'; @include test.describe('shape') { @include test.describe('resolve-theme()') { @@ -286,4 +289,110 @@ ); } } + + @include test.describe('corners-to-shape-token') { + @include test.it('should return a list with expanded radius list') { + $result: shape.corners-to-shape-token(foo); + + @include test.assert-equal(meta.type-of($result), 'list'); + @include test.assert-equal(list.length($result), 4); + + @include test.assert-equal( + list.nth($result, 1), + var.create(foo-start-start) + ); + @include test.assert-equal( + list.nth($result, 2), + var.create(foo-start-end) + ); + @include test.assert-equal(list.nth($result, 3), var.create(foo-end-end)); + @include test.assert-equal( + list.nth($result, 4), + var.create(foo-end-start) + ); + } + } + + @include test.describe('resolve-tokens') { + // Setup. + $theme: ( + not-a-shape-token: 24px, + container-shape: 8px, + root-shape: ( + 1px, + 2px, + 3px, + 4px, + ), + ); + + @include test.it('should expand shape tokens into 4 corner tokens') { + $tokens: theme.create-theme-vars($theme, foo); + // Test Case. + $result: shape.resolve-tokens($tokens, root-shape, container-shape); + + // Assertion. + $expected: ( + container-shape-start-start: + var( + --md-foo-container-shape-start-start, + var(--md-foo-container-shape, 8px) + ), + container-shape-start-end: + var( + --md-foo-container-shape-start-end, + var(--md-foo-container-shape, 8px) + ), + container-shape-end-end: + var( + --md-foo-container-shape-end-end, + var(--md-foo-container-shape, 8px) + ), + container-shape-end-start: + var( + --md-foo-container-shape-end-start, + var(--md-foo-container-shape, 8px) + ), + root-shape-start-start: + var(--md-foo-root-shape-start-start, var(--md-foo-root-shape, 1px)), + root-shape-start-end: + var(--md-foo-root-shape-start-end, var(--md-foo-root-shape, 2px)), + root-shape-end-end: + var(--md-foo-root-shape-end-end, var(--md-foo-root-shape, 3px)), + root-shape-end-start: + var(--md-foo-root-shape-end-start, var(--md-foo-root-shape, 4px)), + not-a-shape-token: var(--md-foo-not-a-shape-token, 24px), + ); + @include test.assert-equal( + $result, + $expected, + $description: + 'Should expand shape tokens, remove original tokens, and not touch other tokens' + ); + } + + @include test.it('gracefully handles missing shape tokens') { + $tokens: theme.create-theme-vars( + ( + shape: 10px, + not-affected: 10px, + ), + foo + ); + $result: shape.resolve-tokens($tokens, shape, missing); + + $expected: ( + shape-start-start: + var(--md-foo-shape-start-start, var(--md-foo-shape, 10px)), + shape-start-end: + var(--md-foo-shape-start-end, var(--md-foo-shape, 10px)), + shape-end-end: var(--md-foo-shape-end-end, var(--md-foo-shape, 10px)), + shape-end-start: + var(--md-foo-shape-end-start, var(--md-foo-shape, 10px)), + not-affected: var(--md-foo-not-affected, 10px), + ); + + @include test.assert-equal($result, $expected); + } + } } diff --git a/sass/test/_string-ext.test.scss b/sass/test/_string-ext.test.scss index 8b2936d7c2..1c4e97ec94 100644 --- a/sass/test/_string-ext.test.scss +++ b/sass/test/_string-ext.test.scss @@ -5,6 +5,8 @@ @use 'true' as test; @use '../string-ext'; +@use 'sass:meta'; +@use 'sass:list'; @include test.describe('string-ext') { @include test.describe('has-prefix()') { @@ -157,4 +159,38 @@ ); } } + + @include test.describe('split()') { + @include test.it('should return a list') { + $result: string-ext.split('foo bar baz'); + @include test.assert-equal(meta.type-of($result), 'list'); + } + + @include test.it('should return ordered substrings') { + $result: string-ext.split('foo bar baz'); + $expected: (foo bar baz); + @include test.assert-equal(list.length($result), 3); + @include test.assert-equal($result, $expected); + } + + @include test.it('should handle comma separated lists') { + $result: string-ext.split('foo, bar, baz'); + $expected: (foo bar baz); + @include test.assert-equal(list.length($result), 3); + @include test.assert-equal($result, $expected); + } + + @include test.it('should handle slash separated lists') { + $result: string-ext.split('foo/ bar/ baz'); + $expected: (foo bar baz); + @include test.assert-equal(list.length($result), 3); + @include test.assert-equal($result, $expected); + } + + @include test.it('should one-item list for simple strings') { + $result: string-ext.split('foo'); + $expected: list.append((), foo); + @include test.assert-equal($result, $expected); + } + } }