Skip to content

Commit c5e00e9

Browse files
committed
Add polyfill loader
1 parent 5546301 commit c5e00e9

File tree

12 files changed

+209
-3
lines changed

12 files changed

+209
-3
lines changed

.storybook/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export default {
3737
directory: "../packages/sdk-components-react-radix",
3838
titlePrefix: "SDK Components React Radix",
3939
},
40+
{
41+
directory: "../packages/sdk-components-animation",
42+
titlePrefix: "SDK Components Animation",
43+
},
4044
],
4145
framework: {
4246
name: "@storybook/react-vite",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Scroll } from "@webstudio-is/sdk-components-animation";
2+
3+
const Playground = () => {
4+
return (
5+
<>
6+
<Scroll debug={true} />
7+
</>
8+
);
9+
};
10+
11+
export default Playground;
12+
13+
// Reduces Vercel function size from 29MB to 9MB for unknown reasons; effective when used in limited files.
14+
export const config = {
15+
maxDuration: 30,
16+
};

packages/sdk-components-animation/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,12 @@
5151
"react-dom": "18.3.0-canary-14898b6a9-20240318"
5252
},
5353
"dependencies": {
54+
"@webstudio-is/css-engine": "workspace:*",
5455
"@webstudio-is/icons": "workspace:*",
5556
"@webstudio-is/react-sdk": "workspace:*",
56-
"@webstudio-is/sdk": "workspace:*"
57+
"@webstudio-is/sdk": "workspace:*",
58+
"react-error-boundary": "^5.0.0",
59+
"scroll-timeline-polyfill": "^1.1.0"
5760
},
5861
"devDependencies": {
5962
"@types/react": "^18.2.70",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export {};
1+
export { Scroll } from "./scroll";
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const Component = () => {
2+
return <div>JO</div>;
3+
};
4+
5+
export default {
6+
title: "Components/Animate",
7+
};
8+
9+
const Story = {
10+
render() {
11+
return (
12+
<>
13+
<Component />
14+
</>
15+
);
16+
},
17+
};
18+
19+
export { Story as Animate };
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { Suspense, useId, useLayoutEffect } from "react";
2+
import { ErrorBoundary } from "react-error-boundary";
3+
import { ClientOnly } from "./shared/client-only";
4+
import { PolyfillLoader } from "./shared/scroll-polyfill";
5+
import { DebugDelayLoader } from "./shared/debug-delay-loader";
6+
import { escapeCssSpecifier } from "./shared/css-utils";
7+
8+
const componentAttributeId = `data-ws-animate-id`;
9+
10+
const Animate = ({ id }: { id: string }) => {
11+
useLayoutEffect(() => {
12+
const elt = document.querySelector(`[${componentAttributeId}="${id}"]`);
13+
if (elt === null) {
14+
return;
15+
}
16+
17+
if (elt instanceof HTMLElement) {
18+
const animation = elt.animate(
19+
[{ offset: 0, transform: "translate(0, 100px)" }],
20+
{
21+
fill: "backwards",
22+
duration: 400,
23+
easing: "linear",
24+
}
25+
);
26+
27+
return () => {
28+
animation.cancel();
29+
};
30+
}
31+
}, [id]);
32+
33+
return undefined;
34+
};
35+
36+
const StylesBeforeAnimate = ({ id }: { id: string }) => {
37+
const styleContent = `
38+
@keyframes ws-scroll-animation-${id} {
39+
from {
40+
transform: translate(0, 100px);
41+
}
42+
}
43+
44+
[${componentAttributeId}="${id}"] {
45+
animation-name: ws-scroll-animation-${id};
46+
animation-fill-mode: backwards;
47+
animation-play-state: paused;
48+
animation-duration: 1ms;
49+
50+
}
51+
`;
52+
return <style dangerouslySetInnerHTML={{ __html: styleContent }} />;
53+
};
54+
55+
type ScrollProps = { debug?: boolean };
56+
57+
export const Scroll = ({ debug = false }: ScrollProps) => {
58+
const nakedId = useId();
59+
const id = escapeCssSpecifier(nakedId);
60+
61+
return (
62+
<>
63+
<ClientOnly fallback={<StylesBeforeAnimate id={id} />}>
64+
<Suspense fallback={<StylesBeforeAnimate id={id} />}>
65+
<ErrorBoundary
66+
fallback={null}
67+
onError={(error) => {
68+
console.error(`Polyfill loading error`, error);
69+
}}
70+
>
71+
<PolyfillLoader />
72+
{debug && <DebugDelayLoader />}
73+
</ErrorBoundary>
74+
<Animate id={id} />
75+
</Suspense>
76+
</ClientOnly>
77+
<div
78+
{...{ [componentAttributeId]: id }}
79+
style={{ height: "200px", width: "200px", backgroundColor: "red" }}
80+
></div>
81+
<div
82+
data-ws-id={id}
83+
style={{
84+
height: "100px",
85+
width: "100px",
86+
backgroundColor: "yellow",
87+
position: "fixed",
88+
left: 0,
89+
top: 0,
90+
}}
91+
></div>
92+
</>
93+
);
94+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useSyncExternalStore, type ReactNode } from "react";
2+
3+
export const ClientOnly = ({
4+
fallback,
5+
children,
6+
}: {
7+
fallback?: ReactNode;
8+
children: ReactNode;
9+
}) => {
10+
// https://tkdodo.eu/blog/avoiding-hydration-mismatches-with-use-sync-external-store
11+
const isServer = useSyncExternalStore(
12+
() => () => {},
13+
() => false,
14+
() => true
15+
);
16+
if (isServer) {
17+
return fallback;
18+
}
19+
return children;
20+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const escapeCssSpecifier = (name: string) => {
2+
return name.replace(/[^a-zA-Z0-9-_]/g, "_");
3+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Artificial delay for loading the component for debugging purposes
2+
3+
import { use } from "react";
4+
5+
const wait = () => new Promise((resolve) => setTimeout(resolve, 1000));
6+
7+
const waitglobal = wait();
8+
9+
export const DebugDelayLoader = () => {
10+
use(waitglobal);
11+
return undefined;
12+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { use } from "react";
2+
3+
let clientPolyfillCache: null | Promise<void> = null;
4+
5+
const polyfill = () => {
6+
if (clientPolyfillCache === null) {
7+
clientPolyfillCache = import(
8+
// @ts-expect-error no types exists
9+
"scroll-timeline-polyfill/dist/scroll-timeline.js"
10+
);
11+
}
12+
return clientPolyfillCache;
13+
};
14+
15+
export const PolyfillLoader = () => {
16+
use(polyfill());
17+
return undefined;
18+
};

0 commit comments

Comments
 (0)