Skip to content
This repository was archived by the owner on Sep 30, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-ladybugs-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris-migrator': minor
---

Add migrations for animation: declarations (and rename `replace-sass-transition` to the more generic `replace-sass-animatable`).
28 changes: 25 additions & 3 deletions polaris-migrator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,31 @@ Be aware that this may also create additional code changes in your codebase, we
npx @shopify/polaris-migrator replace-sass-spacing <path>
```

### `replace-sass-transition`
### `replace-sass-animatable`

Replace timings (`ms`, `s`) and legacy Sass functions (`duration()`,`easing()`) in transition declarations (`transition`, `transition-duration`, `transition-delay`, and `transition-timing-function`) with the corresponding Polaris [motion](https://polaris.shopify.com/tokens/motion) token.
Replace timings (`ms`, `s`) and legacy Sass functions (`duration()`,`easing()`) with the corresponding Polaris [motion](https://polaris.shopify.com/tokens/motion) token.

Accepts two command line arguments:

- `--with-transition=<bool>`: (_default: `true`_) Migrate `transition`
declarations.
- `--with-animation=<bool>`: (_default: `true`_) Migrate `animation`
declarations.

Declarations targeted:

```
transition
transition-duration
transition-delay
transition-timing-function
animation
animation-duration
animation-delay
animation-timing-function
```

Example changes:

```diff
- transition-duration: 100ms;
Expand All @@ -263,7 +285,7 @@ Replace timings (`ms`, `s`) and legacy Sass functions (`duration()`,`easing()`)
```

```sh
npx @shopify/polaris-migrator replace-sass-transition <path>
npx @shopify/polaris-migrator replace-sass-animatable <path>
```

## Creating Migrations
Expand Down
1 change: 1 addition & 0 deletions polaris-migrator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"dependencies": {
"@shopify/polaris-tokens": "^6.2.1",
"chalk": "^4.1.0",
"falsey": "^1.0.0",
"globby": "11.0.1",
"is-git-clean": "^1.1.0",
"jscodeshift": "^0.13.1",
Expand Down
3 changes: 3 additions & 0 deletions polaris-migrator/src/falsey.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module 'falsey' {
export default function (val: any, keywords?: any | any[]): boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import valueParser, {
Node,
FunctionNode,
} from 'postcss-value-parser';
import falsey from 'falsey';

import {
namespace,
Expand All @@ -13,6 +14,7 @@ import {
isTransformableDuration,
isPolarisVar,
createSassMigrator,
PolarisMigrator,
} from '../../utilities/sass';
import {isKeyOf} from '../../utilities/type-guards';

Expand Down Expand Up @@ -71,7 +73,7 @@ const easingFuncConstantsMap = {

const deprecatedEasingFuncs = ['anticipate', 'excite', 'overshoot'];

// Per the spec for transition easing functions:
// Per the spec for easing functions:
// https://w3c.github.io/csswg-drafts/css-easing/#easing-functions
const cssEasingBuiltinFuncs = [
'linear',
Expand Down Expand Up @@ -100,10 +102,36 @@ function setNodeValue(node: Node, value: string): void {
node.sourceEndIndex += sourceIndex;
}

interface Options {
namespace?: string;
// later cooerced by falsey()
withTransition?: string;
// later cooerced by falsey()
withAnimation?: string;
}

export default createSassMigrator(
'replace-sass-transition',
(_, {methods, options}, context) => {
'replace-sass-animatable',
(
_,
{
methods,
options,
}: {
methods: Parameters<PolarisMigrator>[1]['methods'];
options: Options;
},
context,
) => {
const durationFunc = namespace('duration', options);
const withTransition =
typeof options.withTransition === 'undefined'
? true
: !falsey(options.withTransition);
const withAnimation =
typeof options.withAnimation === 'undefined'
? true
: !falsey(options.withAnimation);

function migrateLegacySassEasingFunction(
node: FunctionNode,
Expand Down Expand Up @@ -150,10 +178,7 @@ export default createSassMigrator(
});
}

function mutateTransitionDurationValue(
node: Node,
decl: Declaration,
): void {
function mutateDurationValue(node: Node, decl: Declaration): void {
if (isPolarisVar(node)) {
return;
}
Expand Down Expand Up @@ -226,9 +251,10 @@ export default createSassMigrator(
}
}

function mutateTransitionFunctionValue(
function mutateTimingFunctionValue(
node: Node,
decl: Declaration,
{ignoreUnknownFunctions}: {ignoreUnknownFunctions: boolean},
): void {
if (isPolarisVar(node)) {
return;
Expand All @@ -244,17 +270,27 @@ export default createSassMigrator(
}

if (node.type === 'function') {
const easingFuncHandlers = {
[namespace('easing', options)]: migrateLegacySassEasingFunction,
// Per the spec, these can all be functions:
// https://w3c.github.io/csswg-drafts/css-easing/#easing-functions
linear: insertUnexpectedEasingFunctionComment,
'cubic-bezier': insertUnexpectedEasingFunctionComment,
steps: insertUnexpectedEasingFunctionComment,
};
if (node.value === namespace('easing', options)) {
migrateLegacySassEasingFunction(node, decl);
return;
}

if (isKeyOf(easingFuncHandlers, node.value)) {
easingFuncHandlers[node.value](node, decl);
if (ignoreUnknownFunctions) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discovered a bug where transition-timing-function: nonsense was not flagged as an error. This flag (and surrounding logic catches it).

const easingFuncHandlers = {
[namespace('easing', options)]: migrateLegacySassEasingFunction,
// Per the spec, these can all be functions:
// https://w3c.github.io/csswg-drafts/css-easing/#easing-functions
linear: insertUnexpectedEasingFunctionComment,
'cubic-bezier': insertUnexpectedEasingFunctionComment,
steps: insertUnexpectedEasingFunctionComment,
};

if (isKeyOf(easingFuncHandlers, node.value)) {
easingFuncHandlers[node.value](node, decl);
return;
}
} else {
insertUnexpectedEasingFunctionComment(node, decl);
return;
}
}
Expand All @@ -276,18 +312,21 @@ export default createSassMigrator(
return;
}

if (cssEasingBuiltinFuncs.includes(node.value)) {
if (
!ignoreUnknownFunctions ||
cssEasingBuiltinFuncs.includes(node.value)
) {
insertUnexpectedEasingFunctionComment(node, decl);
}
}
}

function mutateTransitionDelayValue(node: Node, decl: Declaration): void {
function mutateDelayValue(node: Node, decl: Declaration): void {
// For now, we treat delays like durations
return mutateTransitionDurationValue(node, decl);
return mutateDurationValue(node, decl);
}

function mutateTransitionShorthandValue(
function mutateAnimatableShorthandValue(
decl: Declaration,
parsedValue: ParsedValue,
): void {
Expand All @@ -309,8 +348,8 @@ export default createSassMigrator(
//
// Note that order is important within the items in this property: the
// first value that can be parsed as a time is assigned to the
// transition-duration, and the second value that can be parsed as a
// time is assigned to transition-delay.
// transition-duration/animation-duration, and the second value that can
// be parsed as a time is assigned to transition-delay/animation-delay.
// https://w3c.github.io/csswg-drafts/css-transitions-1/#transition-shorthand-property
//
// That sounds like an array to me! [0] is duration, [1] is delay.
Expand All @@ -327,41 +366,69 @@ export default createSassMigrator(
// This node could be either the property to animate, or an easing
// function. We try mutate the easing function, but if not we assume
// it's the property to animate and therefore do not leave a comment.
mutateTransitionFunctionValue(node, decl);
mutateTimingFunctionValue(node, decl, {
ignoreUnknownFunctions: true,
});
}
});

if (timings[0]) {
mutateTransitionDurationValue(timings[0], decl);
mutateDurationValue(timings[0], decl);
}

if (timings[1]) {
mutateTransitionDelayValue(timings[1], decl);
mutateDelayValue(timings[1], decl);
}
});
}

return (root) => {
methods.walkDecls(root, (decl) => {
const handlers: {[key: string]: () => void} = {
'transition-duration': () => {
parsedValue.nodes.forEach((node) => {
mutateTransitionDurationValue(node, decl);
});
},
'transition-delay': () => {
parsedValue.nodes.forEach((node) => {
mutateTransitionDelayValue(node, decl);
});
},
'transition-timing-function': () => {
parsedValue.nodes.forEach((node) => {
mutateTransitionFunctionValue(node, decl);
});
},
transition: () => {
mutateTransitionShorthandValue(decl, parsedValue);
},
...(withTransition && {
'transition-duration': () => {
parsedValue.nodes.forEach((node) => {
mutateDurationValue(node, decl);
});
},
'transition-delay': () => {
parsedValue.nodes.forEach((node) => {
mutateDelayValue(node, decl);
});
},
'transition-timing-function': () => {
parsedValue.nodes.forEach((node) => {
mutateTimingFunctionValue(node, decl, {
ignoreUnknownFunctions: false,
});
});
},
transition: () => {
mutateAnimatableShorthandValue(decl, parsedValue);
},
}),
...(withAnimation && {
'animation-duration': () => {
parsedValue.nodes.forEach((node) => {
mutateDurationValue(node, decl);
});
},
'animation-delay': () => {
parsedValue.nodes.forEach((node) => {
mutateDelayValue(node, decl);
});
},
'animation-timing-function': () => {
parsedValue.nodes.forEach((node) => {
mutateTimingFunctionValue(node, decl, {
ignoreUnknownFunctions: false,
});
});
},
animation: () => {
mutateAnimatableShorthandValue(decl, parsedValue);
},
}),
};

if (!handlers[decl.prop]) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
.duration-simple-string-arg {
opacity: 1;
animation-duration: legacy-polaris-v8.duration('none');
animation-duration: legacy-polaris-v8.duration('fast');
animation-duration: legacy-polaris-v8.duration('base');
animation-duration: legacy-polaris-v8.duration('slow');
animation-duration: legacy-polaris-v8.duration('slower');
animation-duration: legacy-polaris-v8.duration('slowest');
animation-duration: legacy-polaris-v8.duration('nonsense');
}

.duration-simple-non-string-arg {
opacity: 1;
animation-duration: legacy-polaris-v8.duration();
animation-duration: legacy-polaris-v8.duration(none);
animation-duration: legacy-polaris-v8.duration(fast);
animation-duration: legacy-polaris-v8.duration(base);
animation-duration: legacy-polaris-v8.duration(slow);
animation-duration: legacy-polaris-v8.duration(slower);
animation-duration: legacy-polaris-v8.duration(slowest);
animation-duration: legacy-polaris-v8.duration(nonsense);
}

.duration-simple-constant {
opacity: 1;
animation-duration: 0;
animation-duration: 0ms;
animation-duration: 0s;
animation-duration: 50ms;
animation-duration: 0.05s;
animation-duration: 100ms;
animation-duration: 0.1s;
animation-duration: 150ms;
animation-duration: 0.15s;
animation-duration: 200ms;
animation-duration: 0.2s;
animation-duration: 250ms;
animation-duration: 0.25s;
animation-duration: 300ms;
animation-duration: 0.3s;
animation-duration: 350ms;
animation-duration: 0.35s;
animation-duration: 400ms;
animation-duration: 0.4s;
animation-duration: 450ms;
animation-duration: 0.45s;
animation-duration: 500ms;
animation-duration: 0.5s;
animation-duration: 5s;
animation-duration: 16.7s;
}

.edges {
// foobar isn't a valid duration key
animation-duration: legacy-polaris-v8.duration(foobar);
// can't process variables
animation-duration: $foo;
}
Loading