-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseAnimatedList.ts
More file actions
72 lines (60 loc) · 2.29 KB
/
Copy pathuseAnimatedList.ts
File metadata and controls
72 lines (60 loc) · 2.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
'use client';
import { useEffect, useRef, useState } from 'react';
interface Identifiable {
id: string;
}
export interface AnimatedEntry<T> {
item: T;
/** True while the item has been removed from the source list but is still
* animating out — kept in the DOM until its exit animation ends. */
leaving: boolean;
}
/**
* Drives CSS-only enter/leave animations for a keyed list — the small piece a
* library like react-transition-group would otherwise provide.
*
* When an item disappears from `items` it isn't dropped immediately; it's kept
* in the rendered set (flagged `leaving`) at its previous position so a CSS
* exit animation can play. The consumer calls `handleExited(id)` from
* `onAnimationEnd` to finally remove it. Present items always follow the
* incoming (already-sorted) order, so live inserts still land correctly.
*/
export function useAnimatedList<T extends Identifiable>(items: T[]): {
entries: AnimatedEntry<T>[];
handleExited: (id: string) => void;
} {
const [entries, setEntries] = useState<AnimatedEntry<T>[]>(() =>
items.map((item) => ({ item, leaving: false })),
);
// Index each present id held last render, so a leaving card fades out where
// it was instead of jumping to the end of the column.
const lastIndex = useRef<Map<string, number>>(
new Map(items.map((item, i) => [item.id, i])),
);
useEffect(() => {
setEntries((current) => {
const nextIds = new Set(items.map((i) => i.id));
// Present items, in the incoming sorted order.
const result: AnimatedEntry<T>[] = items.map((item) => ({
item,
leaving: false,
}));
// Anything currently rendered but gone from the new list → still leaving.
const leaving = current.filter((e) => !nextIds.has(e.item.id));
for (const entry of leaving) {
const idx = Math.min(
lastIndex.current.get(entry.item.id) ?? result.length,
result.length,
);
result.splice(idx, 0, { item: entry.item, leaving: true });
}
return result;
});
lastIndex.current = new Map(items.map((item, i) => [item.id, i]));
}, [items]);
const handleExited = (id: string) =>
setEntries((current) =>
current.filter((e) => !(e.item.id === id && e.leaving)),
);
return { entries, handleExited };
}