Fix issue with item name generation
This commit is contained in:
138
src/AnimatedListDemo.tsx
Normal file
138
src/AnimatedListDemo.tsx
Normal 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
20
src/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);
|
112
src/useAnimatedListItems.ts
Normal file
112
src/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;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
Reference in New Issue
Block a user