Skip to content

Commit

Permalink
feat(shape, string-ext): Allow shape corners to fall back to a single…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
dfreedm authored and copybara-github committed Feb 13, 2023
1 parent 56dc57b commit 1afd925
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 0 deletions.
91 changes: 91 additions & 0 deletions sass/_shape.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down
42 changes: 42 additions & 0 deletions sass/_string-ext.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
109 changes: 109 additions & 0 deletions sass/test/_shape.test.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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()') {
Expand Down Expand Up @@ -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);
}
}
}
36 changes: 36 additions & 0 deletions sass/test/_string-ext.test.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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()') {
Expand Down Expand Up @@ -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);
}
}
}

0 comments on commit 1afd925

Please sign in to comment.