Skip to content

Commit

Permalink
feat: recursively expand nested composite tokens (#1244)
Browse files Browse the repository at this point in the history
* feat: recursively expand nested composite tokens

* fix: expand object type check, maintain ref if possible

* fix: handle multi-value object tokens like shadows

---------

Co-authored-by: Abel van Beek <abel.van.beek@tromsfylke.no>
Co-authored-by: jorenbroekema <joren.broekema@gmail.com>
  • Loading branch information
3 people committed Jun 28, 2024
1 parent c856761 commit 904d6b2
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 11 deletions.
10 changes: 10 additions & 0 deletions .changeset/lucky-buckets-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'style-dictionary': minor
---

Some fixes for Expand utility:

- Array values such as `dashArray` property of `strokeStyle` tokens no longer get expanded unintentionally, `typeof 'object'` check changed to `isPlainObject` check.
- Nested object-value tokens (such as `style` property inside `border` tokens) will now also be expanded.
- When references are involved during expansion, the resolved value is used when the property is an object, if not, then we keep the reference as is.
This is because if the reference is to an object value, the expansion might break the reference.
141 changes: 141 additions & 0 deletions __tests__/utils/expandObjectTokens.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,147 @@ describe('utils', () => {
});
});

it('should expand nested composite tokens', () => {
const refInput = {
black: {
value: '#000',
type: 'color',
},
stroke: {
value: {
dashArray: ['0.5rem', '0.25rem'],
lineCap: 'round',
},
type: 'strokeStyle',
},
border: {
value: {
color: '{black}',
width: '3px',
style: '{stroke}',
},
type: 'border',
},
};

const expanded = expandTokens(refInput, {
expand: true,
usesDtcg: false,
});

expect(expanded).to.eql({
black: {
value: '#000',
type: 'color',
},
stroke: {
dashArray: {
value: ['0.5rem', '0.25rem'],
type: 'dimension',
},
lineCap: {
value: 'round',
type: 'lineCap',
},
},
border: {
// color can remain unresolved ref because its resolved value is not an object
color: { value: '{black}', type: 'color' },
width: { value: '3px', type: 'dimension' },
// style must be its resolved value because it is an object and potentially gets expanded,
// breaking the original reference
style: {
dashArray: {
value: ['0.5rem', '0.25rem'],
type: 'dimension',
},
lineCap: {
value: 'round',
type: 'lineCap',
},
},
},
});
});

it('should expand shadow tokens', () => {
const refInput = {
shade: {
type: 'shadow',
value: [
{
offsetX: '2px',
offsetY: '4px',
blur: '2px',
spread: '0',
color: '#000',
},
{
offsetX: '10px',
offsetY: '12px',
blur: '4px',
spread: '3px',
color: '#ccc',
},
],
},
};

const expanded = expandTokens(refInput, {
expand: true,
usesDtcg: false,
});

expect(expanded).to.eql({
shade: {
1: {
offsetX: {
type: 'dimension',
value: '2px',
},
offsetY: {
type: 'dimension',
value: '4px',
},
blur: {
type: 'dimension',
value: '2px',
},
spread: {
type: 'dimension',
value: '0',
},
color: {
type: 'color',
value: '#000',
},
},
2: {
offsetX: {
type: 'dimension',
value: '10px',
},
offsetY: {
type: 'dimension',
value: '12px',
},
blur: {
type: 'dimension',
value: '4px',
},
spread: {
type: 'dimension',
value: '3px',
},
color: {
type: 'color',
value: '#ccc',
},
},
},
});
});

it('should support DTCG format', () => {
const input = {
border: {
Expand Down
34 changes: 25 additions & 9 deletions lib/utils/expandObjectTokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { resolveReferences } from './references/resolveReferences.js';
import usesReferences from './references/usesReferences.js';
import { deepmerge } from './deepmerge.js';
import isPlainObject from 'is-plain-obj';

/**
* @typedef {import('../../types/DesignToken.d.ts').DesignToken} DesignToken
Expand Down Expand Up @@ -53,6 +54,10 @@ export const DTCGTypesMap = {
letterSpacing: 'dimension',
lineHeight: 'number',
},
// https://design-tokens.github.io/community-group/format/#object-value
strokeStyle: {
dashArray: 'dimension',
},
};

/**
Expand Down Expand Up @@ -198,7 +203,7 @@ export function expandToken(token, opts, platform) {
function expandTokensRecurse(slice, original, opts, platform) {
for (const key in slice) {
const token = slice[key];
if (typeof token !== 'object' || token === null) {
if (!isPlainObject(token) || token === null) {
continue;
}
const uses$ = opts.usesDtcg;
Expand All @@ -207,17 +212,28 @@ function expandTokensRecurse(slice, original, opts, platform) {
// if our token is a ref, we have to resolve it first in order to expand its value
if (typeof value === 'string' && usesReferences(value)) {
value = resolveReferences(value, original, { usesDtcg: uses$ });
token[uses$ ? '$value' : 'value'] = value;
}
if (typeof value === 'object' && shouldExpand(token, opts, platform)) {
// TODO: Support nested objects, e.g. a border can have a style prop (strokeStyle) which itself
// can also be an object value with dashArray and lineCap props.
// More info: https://design-tokens.github.io/community-group/format/#example-border-composite-token-examples
slice[key] = expandToken(token, opts, platform);

if (
isPlainObject(value) ||
// support multi-value arrays where each item is an object, e.g. shadow tokens
(Array.isArray(value) && value.every((sub) => isPlainObject(sub)))
) {
// if the resolved value is an object, then we must assume it could get expanded and
// we must set the value to the resolved value, since the reference might be broken after expansion
slice[key][uses$ ? '$value' : 'value'] = value;

if (shouldExpand(token, opts, platform)) {
slice[key] = expandToken(token, opts, platform);
}
}
} else {
expandTokensRecurse(token, original, opts, platform);
}
// We might expect an else statement here on the line above, but we also want
// to recurse if a value is present so that we support expanding nested object values,
// e.g. a border can have a style prop (strokeStyle) which itself
// can also be an object value with dashArray and lineCap props.
// More info: https://design-tokens.github.io/community-group/format/#example-border-composite-token-examples
expandTokensRecurse(slice[key], original, opts, platform);
}
}

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 904d6b2

Please sign in to comment.