Fix issue with item name generation

This commit is contained in:
2024-08-06 17:49:02 -07:00
parent a9f9678133
commit dc95275086
5 changed files with 22 additions and 11 deletions

138
src/AnimatedListDemo.tsx Normal file
View File

@ -0,0 +1,138 @@
import React, { useReducer } from 'react';
import { useAnimatedListItems } from './useAnimatedListItems';
type Mutation = 'reverse' | 'randomize' | 'insert' | 'remove';
function getRandomChar(): string {
return String.fromCharCode(98 + Math.floor(Math.random() * 25));
}
function getRandomItemName(): string {
return `Serial Number - ${new Array(16)
.fill('')
.map(() => getRandomChar())
.join('')}`;
}
/**
* 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 = getRandomItemName();
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;
},
new Array(4).fill('').map(() => getRandomItemName()),
);
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
src/index.tsx Normal file
View 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);

112
src/useAnimatedListItems.ts Normal file
View 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;
}
});
},
};
};