Get a hello world set up

This commit is contained in:
Matthew Cardarelli 2024-08-06 16:26:30 -07:00
parent 1d0f0275b6
commit 648f9464cb
8 changed files with 203 additions and 0 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules node_modules
public/scripts

5
AnimatedListDemo.tsx Normal file
View File

@ -0,0 +1,5 @@
import React from 'react';
export const AnimatedListDemo = () => {
return <main>Hello, world</main>;
};

21
index.tsx Normal file
View File

@ -0,0 +1,21 @@
import React from 'react';
import { Root, createRoot } from 'react-dom/client';
import { AnimatedListDemo } from './AnimatedListDemo';
class AnimatedListComponent extends HTMLElement {
root: Root;
constructor() {
super();
}
connectedCallback() {
console.log('Hello, world!');
const rootNode = document.createElement('main');
this.appendChild(rootNode);
this.root = createRoot(rootNode);
this.root.render(<AnimatedListDemo />);
}
}
customElements.define('react-animated-list-demo', AnimatedListComponent);

37
package-lock.json generated
View File

@ -13,6 +13,8 @@
"react-dom": "18.3.1" "react-dom": "18.3.1"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"esbuild": "0.23.0" "esbuild": "0.23.0"
} }
}, },
@ -424,6 +426,41 @@
"node": ">=18" "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": { "node_modules/esbuild": {
"version": "0.23.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz",

View File

@ -4,6 +4,8 @@
"description": "A demo of an implementation of an animated list in React Typescript.", "description": "A demo of an implementation of an animated list in React Typescript.",
"main": "index.tsx", "main": "index.tsx",
"scripts": { "scripts": {
"build": "esbuild index.tsx --bundle",
"dev": "esbuild index.tsx --bundle --watch --serve --outdir=./public/scripts --servedir=./public",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
@ -13,6 +15,8 @@
"author": "Matthew Cardarelli", "author": "Matthew Cardarelli",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"esbuild": "0.23.0" "esbuild": "0.23.0"
}, },
"dependencies": { "dependencies": {

11
public/index.html Normal file
View 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>

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"jsx": "preserve",
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["DOM", "ESNext"],
"noEmit": true,
"alwaysStrict": true
}
}

113
useAnimatedListItems.ts Normal file
View File

@ -0,0 +1,113 @@
import { useLayoutEffect, useRef, useState } from 'react';
interface ItemTops {
[id: number]: number | undefined;
}
interface ListItemRefsById {
[id: number]: HTMLLIElement | undefined;
}
interface ItemOffsets {
[id: number]: 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: number[] | 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: number]: ItemStyles | undefined };
/**
* Function that must be called within the `ref` callback prop of each `ListItem`.
*
* @param {number} key - the `ListItem`'s unique key.
* @param {HTMLLIElement} li - the list item element reference.
* @returns {void}
*/
updateItemRef: (key: number, 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;
}
});
},
};
};