Skip to content

Commit 6d82e89

Browse files
authored
Improve Sass spacing migration to cover interpolations (#7412)
<!-- ☝️How to write a good PR title: - Prefix it with [ComponentName] (if applicable), for example: [Button] - Start with a verb, for example: Add, Delete, Improve, Fix… - Give as much context as necessary and as little as possible - Prefix it with [WIP] while it’s a work in progress --> ### WHY are these changes introduced? We were missing some `spacing()` functions on the initial migrations where it was being used within interpolations: ```scss padding: -#{spacing()}; padding calc(100% - #{spacing()}; ``` <!-- Context about the problem that’s being addressed. --> ### WHAT is this pull request doing? This enhances the `replace-sass-spacing` to catch edge-cases of Sass interpolations. ```scss // Before padding: -#{spacing()}; padding calc(100% - #{spacing()}; // After padding: -1 * var(--p-space-4); // note this will also add a manual check comment padding calc(100% - var(--p-space-4)}; ``` <!-- Summary of the changes committed. Before / after screenshots are appreciated for UI changes. Make sure to include alt text that describes the screenshot. If you include an animated gif showing your change, wrapping it in a details tag is recommended. Gifs usually autoplay, which can cause accessibility issues for people reviewing your PR: <details> <summary>Summary of your gif(s)</summary> <img src="..." alt="Description of what the gif shows"> </details> --> <!-- ℹ️ Delete the following for small / trivial changes --> ### How to 🎩 🖥 [Local development instructions](https://github.com/Shopify/polaris/blob/main/README.md#local-development) 🗒 [General tophatting guidelines](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting.md) 📄 [Changelog guidelines](https://github.com/Shopify/polaris/blob/main/.github/CONTRIBUTING.md#changelog) <!-- Give as much information as needed to experiment with the component in the playground. --> <details> <summary>Copy-paste this code in <code>playground/Playground.tsx</code>:</summary> ```jsx import React from 'react'; import {Page} from '../src'; export function Playground() { return ( <Page title="Playground"> {/* Add the code you want to test in here */} </Page> ); } ``` </details> ### 🎩 checklist - [ ] Tested on [mobile](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting.md#cross-browser-testing) - [ ] Tested on [multiple browsers](https://help.shopify.com/en/manual/shopify-admin/supported-browsers) - [ ] Tested for [accessibility](https://github.com/Shopify/polaris/blob/main/documentation/Accessibility%20testing.md) - [ ] Updated the component's `README.md` with documentation changes - [ ] [Tophatted documentation](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting%20documentation.md) changes in the style guide
1 parent 13546e3 commit 6d82e89

File tree

8 files changed

+208
-51
lines changed

8 files changed

+208
-51
lines changed

.changeset/large-pillows-wave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/polaris-migrator': patch
3+
---
4+
5+
Enhance Sass spacing migration to catch Sass interpolations

polaris-migrator/src/migrations/replace-sass-spacing/replace-sass-spacing.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import {
99
isSassFunction,
1010
hasSassFunction,
1111
hasNumericOperator,
12+
hasSassInterpolation,
13+
removeSassInterpolation,
14+
hasNegativeSassInterpolation,
15+
replaceNegativeSassInterpolation,
16+
createInlineComment,
1217
} from '../../utilities/sass';
1318
import {isKeyOf} from '../../utilities/type-guards';
1419

@@ -38,6 +43,17 @@ const plugin = (options: PluginOptions = {}): Plugin => {
3843

3944
const parsedValue = valueParser(decl.value);
4045

46+
// Convert -#{spacing()} to -1 * #{spacing()}
47+
if (hasNegativeSassInterpolation(decl.value)) {
48+
replaceNegativeSassInterpolation(parsedValue);
49+
}
50+
51+
// Remove #{} from spacing()
52+
if (hasSassInterpolation(parsedValue.toString())) {
53+
removeSassInterpolation(namespacedSpacing, parsedValue);
54+
}
55+
56+
// Now we can check if the value is a spacing() function
4157
if (!hasSassFunction(namespacedSpacing, parsedValue)) return;
4258

4359
parsedValue.walk((node) => {
@@ -56,15 +72,16 @@ const plugin = (options: PluginOptions = {}): Plugin => {
5672
sourceIndex: node.nodes[0]?.sourceIndex ?? 0,
5773
sourceEndIndex: spacingCustomProperty.length,
5874
},
59-
...node.nodes.slice(1),
6075
];
6176
});
6277

6378
if (hasNumericOperator(parsedValue)) {
6479
// Insert comment if the declaration value contains calculations
65-
decl.before(postcss.comment({text: POLARIS_MIGRATOR_COMMENT}));
6680
decl.before(
67-
postcss.comment({text: `${decl.prop}: ${parsedValue.toString()};`}),
81+
createInlineComment(POLARIS_MIGRATOR_COMMENT, {prose: true}),
82+
);
83+
decl.before(
84+
createInlineComment(`${decl.prop}: ${parsedValue.toString()};`),
6885
);
6986
} else {
7087
decl.value = parsedValue.toString();
Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
.Card {
2-
z-index: z-index(content) + 1;
3-
padding: spacing(loose) spacing(extra-loose);
4-
border-radius: var(--p-border-radius-2, border-radius());
5-
box-shadow: var(--p-shadow-card, shadow());
6-
margin: spacing(tight) 0 0 (-1 * spacing());
7-
}
1+
@use 'global-styles/legacy-polaris-v8';
82

9-
.ButtonContainer {
10-
right: spacing();
11-
top: spacing(loose);
12-
bottom: spacing() + spacing();
13-
left: spacing('tight');
3+
.my-class {
4+
// Standard replacement
5+
padding: spacing();
6+
padding: spacing(loose);
7+
// With multiple values
8+
padding: spacing() spacing();
9+
padding: spacing() 0 spacing() 0;
10+
// With interpolation
11+
padding: calc(50% - #{spacing()});
12+
// With negative interpolation
13+
padding: -#{spacing()};
14+
// With other functions
15+
padding: calc(#{rem(16px)} + #{spacing()});
1416
}
Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
.Card {
2-
z-index: z-index(content) + 1;
3-
padding: var(--p-space-5) var(--p-space-8);
4-
border-radius: var(--p-border-radius-2, border-radius());
5-
box-shadow: var(--p-shadow-card, shadow());
6-
/* polaris-migrator: Unable to migrate the following expression. Please upgrade manually. */
7-
/* margin: var(--p-space-2) 0 0 (-1 * var(--p-space-4)); */
8-
margin: spacing(tight) 0 0 (-1 * spacing());
9-
}
1+
@use 'global-styles/legacy-polaris-v8';
102

11-
.ButtonContainer {
12-
right: var(--p-space-4);
13-
top: var(--p-space-5);
14-
/* polaris-migrator: Unable to migrate the following expression. Please upgrade manually. */
15-
/* bottom: var(--p-space-4) + var(--p-space-4); */
16-
bottom: spacing() + spacing();
17-
left: var(--p-space-2);
3+
.my-class {
4+
// Standard replacement
5+
padding: var(--p-space-4);
6+
padding: var(--p-space-5);
7+
// With multiple values
8+
padding: var(--p-space-4) var(--p-space-4);
9+
padding: var(--p-space-4) 0 var(--p-space-4) 0;
10+
// With interpolation
11+
// polaris-migrator: Unable to migrate the following expression. Please upgrade manually.
12+
// padding: calc(50% - var(--p-space-4));
13+
padding: calc(50% - #{spacing()});
14+
// With negative interpolation
15+
// polaris-migrator: Unable to migrate the following expression. Please upgrade manually.
16+
// padding: -1 * var(--p-space-4);
17+
padding: -#{spacing()};
18+
// With other functions
19+
// polaris-migrator: Unable to migrate the following expression. Please upgrade manually.
20+
// padding: calc(#{rem(16px)} + var(--p-space-4));
21+
padding: calc(#{rem(16px)} + #{spacing()});
1822
}
Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
@use 'global-styles/legacy-polaris-v8';
22

3-
.Card {
4-
z-index: legacy-polaris-v8.z-index(content) + 1;
5-
padding: legacy-polaris-v8.spacing(loose)
6-
legacy-polaris-v8.spacing(extra-loose);
7-
border-radius: var(--p-border-radius-2, legacy-polaris-v8.border-radius());
8-
box-shadow: var(--p-shadow-card, legacy-polaris-v8.shadow());
9-
margin: legacy-polaris-v8.spacing(tight) 0 0
10-
(-1 * legacy-polaris-v8.spacing());
3+
.my-class {
4+
// Standard replacement
5+
padding: legacy-polaris-v8.spacing();
6+
padding: legacy-polaris-v8.spacing(loose);
7+
// With multiple values
8+
padding: legacy-polaris-v8.spacing() legacy-polaris-v8.spacing();
9+
padding: legacy-polaris-v8.spacing() 0 legacy-polaris-v8.spacing() 0;
10+
// With interpolation
11+
padding: calc(50% - #{legacy-polaris-v8.spacing()});
12+
// With negative interpolation
13+
padding: -#{legacy-polaris-v8.spacing()};
14+
// With other functions
15+
padding: calc(
16+
#{legacy-polaris-v8.rem(16px)} + #{legacy-polaris-v8.spacing()}
17+
);
1118
}
Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
@use 'global-styles/legacy-polaris-v8';
22

3-
.Card {
4-
z-index: legacy-polaris-v8.z-index(content) + 1;
5-
padding: var(--p-space-5) var(--p-space-8);
6-
border-radius: var(--p-border-radius-2, legacy-polaris-v8.border-radius());
7-
box-shadow: var(--p-shadow-card, legacy-polaris-v8.shadow());
8-
/* polaris-migrator: Unable to migrate the following expression. Please upgrade manually. */
9-
/* margin: var(--p-space-2) 0 0
10-
(-1 * var(--p-space-4)); */
11-
margin: legacy-polaris-v8.spacing(tight) 0 0
12-
(-1 * legacy-polaris-v8.spacing());
3+
.my-class {
4+
// Standard replacement
5+
padding: var(--p-space-4);
6+
padding: var(--p-space-5);
7+
// With multiple values
8+
padding: var(--p-space-4) var(--p-space-4);
9+
padding: var(--p-space-4) 0 var(--p-space-4) 0;
10+
// With interpolation
11+
// polaris-migrator: Unable to migrate the following expression. Please upgrade manually.
12+
// padding: calc(50% - var(--p-space-4));
13+
padding: calc(50% - #{legacy-polaris-v8.spacing()});
14+
// With negative interpolation
15+
// polaris-migrator: Unable to migrate the following expression. Please upgrade manually.
16+
// padding: -1 * var(--p-space-4);
17+
padding: -#{legacy-polaris-v8.spacing()};
18+
// With other functions
19+
// polaris-migrator: Unable to migrate the following expression. Please upgrade manually.
20+
// padding: calc(#{legacy-polaris-v8.rem(16px)} + var(--p-space-4));
21+
padding: calc(
22+
#{legacy-polaris-v8.rem(16px)} + #{legacy-polaris-v8.spacing()}
23+
);
1324
}

polaris-migrator/src/utilities/sass.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import type {Declaration} from 'postcss';
1+
import postcss, {Declaration} from 'postcss';
22
import valueParser, {
33
Node,
44
ParsedValue,
55
FunctionNode,
66
Dimension,
77
} from 'postcss-value-parser';
88
import {toPx} from '@shopify/polaris-tokens';
9+
import prettier from 'prettier';
910

1011
import {isKeyOf} from './type-guards';
1112

@@ -89,6 +90,75 @@ export function hasSassFunction(
8990
return containsSassFunction;
9091
}
9192

93+
/**
94+
* Check whether a string has Sass interpolation
95+
*/
96+
export function hasSassInterpolation(string: string) {
97+
return /#\{.+?\}/.test(string);
98+
}
99+
100+
/**
101+
* Check whether a string has negative Sass interpolation
102+
*/
103+
export function hasNegativeSassInterpolation(string: string) {
104+
return /-#\{.+?\}/.test(string);
105+
}
106+
107+
/**
108+
* Replace negative Sass interpolations with a multiplication operator and a negative number
109+
*
110+
* @example
111+
* // Before
112+
* -#{spacing()};
113+
*
114+
* // After
115+
* -1 * ${spacing()};
116+
*/
117+
export function replaceNegativeSassInterpolation(parsedValue: ParsedValue) {
118+
let newNodes: Node[] = [];
119+
parsedValue.walk((node, index, nodes) => {
120+
const containsInterpolation = /-#\{.+?/.test(node.value);
121+
if (!containsInterpolation) return;
122+
123+
node.value = node.value.replace('-#{', '#{');
124+
125+
const left = index > 0 ? nodes.slice(0, index) : [];
126+
const right = nodes.length - 1 > index ? nodes.slice(index + 1) : [];
127+
128+
newNodes = [
129+
...left,
130+
{type: 'word', value: '-1'},
131+
{type: 'space', value: ' '},
132+
{type: 'word', value: '*'},
133+
{type: 'space', value: ' '},
134+
node,
135+
...right,
136+
] as Node[];
137+
});
138+
parsedValue.nodes = newNodes;
139+
}
140+
141+
/**
142+
* Remove the Sass interpolation from parsedValue
143+
*/
144+
export function removeSassInterpolation(
145+
namespace: string,
146+
parsedValue: ParsedValue,
147+
) {
148+
parsedValue.walk((node, index, nodes) => {
149+
const regexp = new RegExp(`#{${namespace}`);
150+
const containsInterpolation = regexp.test(node.value);
151+
if (containsInterpolation) {
152+
node.value = node.value.replace(/#\{/, '');
153+
nodes[index + 1].value = nodes[index + 1].value.replace(/}/, '');
154+
}
155+
});
156+
157+
parsedValue.nodes = parsedValue.nodes.filter(
158+
(node) => !(node.type === 'word' && node.value === ''),
159+
);
160+
}
161+
92162
export function getFunctionArgs(node: FunctionNode): string[] {
93163
const args: string[] = [];
94164

@@ -208,3 +278,19 @@ export function replaceRemFunction(
208278
},
209279
);
210280
}
281+
282+
export function createInlineComment(text: string, options?: {prose?: boolean}) {
283+
const formatted = prettier
284+
.format(text, {
285+
parser: options?.prose ? 'markdown' : 'scss',
286+
proseWrap: 'never',
287+
printWidth: 9999,
288+
})
289+
.trim();
290+
const comment = postcss.comment({text: formatted});
291+
292+
comment.raws.left = ' ';
293+
comment.raws.inline = true;
294+
295+
return comment;
296+
}

polaris-migrator/src/utilities/tests/sass.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
import valueParser from 'postcss-value-parser';
22

3-
import {getFunctionArgs} from '../sass';
3+
import {createInlineComment, getFunctionArgs} from '../sass';
4+
5+
describe('createInlineComment', () => {
6+
it('inline and contains starting space', () => {
7+
const text = 'padding: var(--p-space-4);';
8+
const comment = createInlineComment(text);
9+
expect(comment.raws.inline).toBe(true);
10+
expect(comment.raws.left).toBe(' ');
11+
});
12+
13+
it('formats single line text', () => {
14+
const text = 'padding: var(--p-space-4);';
15+
const comment = createInlineComment(text);
16+
expect(comment.text).toStrictEqual(text);
17+
});
18+
19+
it('formats multiple line text', () => {
20+
const text = `
21+
padding: calc(-1 * var(--p-space-4))
22+
var(--p-space-4);
23+
`;
24+
const formatted = 'padding: calc(-1 * var(--p-space-4)) var(--p-space-4);';
25+
const comment = createInlineComment(text);
26+
expect(comment.text).toStrictEqual(formatted);
27+
});
28+
});
429

530
describe('getFunctionArgs', () => {
631
it('zero args', () => {

0 commit comments

Comments
 (0)