Compare commits
6 Commits
b70759fa4b
...
a9f9678133
Author | SHA1 | Date | |
---|---|---|---|
a9f9678133 | |||
9b566d7cb3 | |||
b996d3c3f4 | |||
e3635677d5 | |||
648f9464cb | |||
1d0f0275b6 |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
public/scripts
|
||||
dist
|
1
.tool-versions
Normal file
1
.tool-versions
Normal file
@ -0,0 +1 @@
|
||||
nodejs 22.5.1
|
133
AnimatedListDemo.tsx
Normal file
133
AnimatedListDemo.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import React, { useReducer } from 'react';
|
||||
import { useAnimatedListItems } from './useAnimatedListItems';
|
||||
|
||||
type Mutation = 'reverse' | 'randomize' | 'insert' | 'remove';
|
||||
|
||||
/**
|
||||
* Randomizer function shamelessly copied unmodified from https://stackoverflow.com/a/12646864
|
||||
*
|
||||
* License: https://creativecommons.org/licenses/by-sa/4.0/
|
||||
*
|
||||
* Author(s):
|
||||
* - https://stackoverflow.com/users/310500/laurens-holst
|
||||
* - https://stackoverflow.com/users/8112776/ashleedawg
|
||||
*/
|
||||
function shuffleArray(array) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
}
|
||||
|
||||
function insertRandomlyIntoArray(array: string[]): string[] {
|
||||
const insertBefore = Math.floor(Math.random() * (array.length + 1));
|
||||
const newItem = `Serial Number ${crypto.randomUUID()}`;
|
||||
if (insertBefore === 0) {
|
||||
return [newItem, ...array];
|
||||
}
|
||||
return [
|
||||
...array.slice(0, insertBefore),
|
||||
newItem,
|
||||
...array.slice(insertBefore),
|
||||
];
|
||||
}
|
||||
|
||||
function removeRandomlyFromArray(array: string[]): string[] {
|
||||
const removeIndex = Math.floor(Math.random() * array.length);
|
||||
if (removeIndex === 0) {
|
||||
return [...array.slice(1)];
|
||||
}
|
||||
if (removeIndex === array.length) {
|
||||
return [...array.slice(0, -1)];
|
||||
}
|
||||
return [...array.slice(0, removeIndex), ...array.slice(removeIndex + 1)];
|
||||
}
|
||||
|
||||
export const AnimatedListDemo = () => {
|
||||
const [items, mutateItems] = useReducer(
|
||||
(prevState: string[], mutation: Mutation) => {
|
||||
let newState: string[] = [...prevState];
|
||||
switch (mutation) {
|
||||
case 'reverse':
|
||||
newState.reverse();
|
||||
break;
|
||||
case 'randomize':
|
||||
shuffleArray(newState);
|
||||
break;
|
||||
case 'insert':
|
||||
newState = insertRandomlyIntoArray(newState);
|
||||
break;
|
||||
case 'remove':
|
||||
newState = removeRandomlyFromArray(newState);
|
||||
break;
|
||||
default:
|
||||
return prevState;
|
||||
}
|
||||
return newState;
|
||||
},
|
||||
[
|
||||
`Serial Number ${crypto.randomUUID()}`,
|
||||
`Serial Number ${crypto.randomUUID()}`,
|
||||
`Serial Number ${crypto.randomUUID()}`,
|
||||
`Serial Number ${crypto.randomUUID()}`,
|
||||
],
|
||||
);
|
||||
|
||||
const { updateItemRef, updateItemPositions, itemStyles } =
|
||||
useAnimatedListItems({ keys: items });
|
||||
|
||||
return (
|
||||
<main>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateItemPositions();
|
||||
mutateItems('reverse');
|
||||
}}
|
||||
>
|
||||
Reverse
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateItemPositions();
|
||||
mutateItems('randomize');
|
||||
}}
|
||||
>
|
||||
Randomize
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateItemPositions();
|
||||
mutateItems('insert');
|
||||
}}
|
||||
>
|
||||
Insert
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateItemPositions();
|
||||
mutateItems('remove');
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
|
||||
<ol>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<li
|
||||
key={item}
|
||||
ref={(li) => updateItemRef(item, li)}
|
||||
style={itemStyles[item]}
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</main>
|
||||
);
|
||||
};
|
20
index.tsx
Normal file
20
index.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Root, createRoot } from 'react-dom/client';
|
||||
import { AnimatedListDemo } from './AnimatedListDemo';
|
||||
|
||||
class AnimatedListComponent extends HTMLElement {
|
||||
root: Root;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const rootNode = document.createElement('main');
|
||||
this.appendChild(rootNode);
|
||||
this.root = createRoot(rootNode);
|
||||
this.root.render(<AnimatedListDemo />);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('react-animated-list-demo', AnimatedListComponent);
|
557
package-lock.json
generated
Normal file
557
package-lock.json
generated
Normal file
@ -0,0 +1,557 @@
|
||||
{
|
||||
"name": "demo-react-animated-list",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "demo-react-animated-list",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"esbuild": "0.23.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz",
|
||||
"integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz",
|
||||
"integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz",
|
||||
"integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz",
|
||||
"integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz",
|
||||
"integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz",
|
||||
"integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz",
|
||||
"integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz",
|
||||
"integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz",
|
||||
"integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz",
|
||||
"integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz",
|
||||
"integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz",
|
||||
"integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz",
|
||||
"integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz",
|
||||
"integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz",
|
||||
"integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz",
|
||||
"integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz",
|
||||
"integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz",
|
||||
"integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz",
|
||||
"integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz",
|
||||
"integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz",
|
||||
"integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz",
|
||||
"integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz",
|
||||
"integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz",
|
||||
"integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
|
||||
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
|
||||
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz",
|
||||
"integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.23.0",
|
||||
"@esbuild/android-arm": "0.23.0",
|
||||
"@esbuild/android-arm64": "0.23.0",
|
||||
"@esbuild/android-x64": "0.23.0",
|
||||
"@esbuild/darwin-arm64": "0.23.0",
|
||||
"@esbuild/darwin-x64": "0.23.0",
|
||||
"@esbuild/freebsd-arm64": "0.23.0",
|
||||
"@esbuild/freebsd-x64": "0.23.0",
|
||||
"@esbuild/linux-arm": "0.23.0",
|
||||
"@esbuild/linux-arm64": "0.23.0",
|
||||
"@esbuild/linux-ia32": "0.23.0",
|
||||
"@esbuild/linux-loong64": "0.23.0",
|
||||
"@esbuild/linux-mips64el": "0.23.0",
|
||||
"@esbuild/linux-ppc64": "0.23.0",
|
||||
"@esbuild/linux-riscv64": "0.23.0",
|
||||
"@esbuild/linux-s390x": "0.23.0",
|
||||
"@esbuild/linux-x64": "0.23.0",
|
||||
"@esbuild/netbsd-x64": "0.23.0",
|
||||
"@esbuild/openbsd-arm64": "0.23.0",
|
||||
"@esbuild/openbsd-x64": "0.23.0",
|
||||
"@esbuild/sunos-x64": "0.23.0",
|
||||
"@esbuild/win32-arm64": "0.23.0",
|
||||
"@esbuild/win32-ia32": "0.23.0",
|
||||
"@esbuild/win32-x64": "0.23.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
package.json
Normal file
26
package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "demo-react-animated-list",
|
||||
"version": "1.0.0",
|
||||
"description": "A demo of an implementation of an animated list in React Typescript.",
|
||||
"main": "index.tsx",
|
||||
"scripts": {
|
||||
"build": "esbuild index.tsx --bundle --sourcemap --outfile=./dist/demo-react-animated-list.js",
|
||||
"dev": "esbuild index.tsx --bundle --watch --serve --outdir=./public/scripts --servedir=./public",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://scm.matthewcardarelli.com/mcsolutions/demo-react-animated-list.git"
|
||||
},
|
||||
"author": "Matthew Cardarelli",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"esbuild": "0.23.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
}
|
||||
}
|
11
public/index.html
Normal file
11
public/index.html
Normal file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>React animated list demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="./scripts/index.js"></script>
|
||||
<react-animated-list-demo />
|
||||
</body>
|
||||
</html>
|
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"noEmit": true,
|
||||
"alwaysStrict": true
|
||||
},
|
||||
"include": ["*.ts", "*.tsx"]
|
||||
}
|
112
useAnimatedListItems.ts
Normal file
112
useAnimatedListItems.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
interface ItemTops {
|
||||
[id: string]: number | undefined;
|
||||
}
|
||||
|
||||
interface ListItemRefsById {
|
||||
[id: string]: HTMLLIElement | undefined;
|
||||
}
|
||||
|
||||
interface ItemOffsets {
|
||||
[id: string]: number | undefined;
|
||||
}
|
||||
|
||||
interface ItemStyles {
|
||||
position: 'relative';
|
||||
top: number;
|
||||
transition: string;
|
||||
}
|
||||
|
||||
interface UseAnimatedListItemArgs {
|
||||
/**
|
||||
* **IMPORTANT - This list must be memoized. The array reference should only change when the
|
||||
* list of items changes.**
|
||||
*
|
||||
* The list of item keys. Each key should uniquely identify a `<ListItem>` component, and
|
||||
* should be the same value passed to the `ListItem`'s `key` prop.
|
||||
*/
|
||||
keys: string[] | undefined;
|
||||
}
|
||||
|
||||
interface UseAnimatedListItemReturns {
|
||||
/**
|
||||
* Item styles that should be passed to the `sx` prop of each `ListItem`. The styles are
|
||||
* mapped per item key.
|
||||
*/
|
||||
itemStyles: { [key: string]: ItemStyles | undefined };
|
||||
|
||||
/**
|
||||
* Function that must be called within the `ref` callback prop of each `ListItem`.
|
||||
*
|
||||
* @param {string} key - the `ListItem`'s unique key.
|
||||
* @param {HTMLLIElement} li - the list item element reference.
|
||||
* @returns {void}
|
||||
*/
|
||||
updateItemRef: (key: string, li: HTMLLIElement | null) => void;
|
||||
|
||||
/**
|
||||
* Function that must be called within each and every callback that causes the list of items
|
||||
* to refresh or update. This is used to take a "snapshot" of the items current positions prior
|
||||
* to any change.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
updateItemPositions: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook can be plugged into a MUI <List> to animate the <ListItem> components whenever
|
||||
* change their order.
|
||||
*/
|
||||
export const useAnimatedListItems = ({
|
||||
keys,
|
||||
}: UseAnimatedListItemArgs): UseAnimatedListItemReturns => {
|
||||
const itemRefs = useRef<ListItemRefsById>({});
|
||||
const itemTops = useRef<ItemTops>({});
|
||||
const [itemOffsets, setItemOffsets] = useState<ItemOffsets>({});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!keys) return;
|
||||
const newItemOffsets: ItemOffsets = {};
|
||||
keys.forEach((key) => {
|
||||
const itemRef = itemRefs.current[key];
|
||||
if (itemRef) {
|
||||
const currentTop = itemRef.getBoundingClientRect().top;
|
||||
const prevTop = itemTops.current[key] || 0;
|
||||
const offset = -(currentTop - prevTop);
|
||||
newItemOffsets[key] = offset;
|
||||
}
|
||||
});
|
||||
setItemOffsets(newItemOffsets);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
setItemOffsets({});
|
||||
});
|
||||
}, [keys]);
|
||||
|
||||
let itemStyles: UseAnimatedListItemReturns['itemStyles'] = {};
|
||||
keys?.forEach((key) => {
|
||||
itemStyles[key] = {
|
||||
position: 'relative',
|
||||
top: itemOffsets[key] || 0,
|
||||
transition:
|
||||
!itemTops.current[key] || itemOffsets[key] ? 'top 0s' : 'top 1s',
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
itemStyles,
|
||||
updateItemRef: (key, li) => {
|
||||
li === null ? delete itemRefs.current[key] : (itemRefs.current[key] = li);
|
||||
},
|
||||
updateItemPositions: () => {
|
||||
keys?.forEach((key) => {
|
||||
const itemRef = itemRefs.current[key];
|
||||
if (itemRef) {
|
||||
itemTops.current[key] = itemRef.getBoundingClientRect().top;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user