1import { useState } from "react";
2
3/** Sorts `items` into `order` by key. Items missing from `order` follow, in their own order. */
4export function keepOrder<T>(
5 items: ReadonlyArray<T>,
6 order: ReadonlyArray<string> | null,
7 key: (item: T) => string,
8): T[] {
9 if (order === null) return [...items];
10 const rank = new Map(order.map((itemKey, index) => [itemKey, index]));
11 return items
12 .map((item, index) => ({ item, rank: rank.get(key(item)) ?? order.length + index }))
13 .toSorted((a, b) => a.rank - b.rank)
14 .map(({ item }) => item);
15}
16
17/** `order` with keys it has not seen added at the end, or `order` itself when nothing is new. */
18export function withArrivedKeys(
19 order: ReadonlyArray<string> | null,
20 keys: ReadonlyArray<string>,
21): ReadonlyArray<string> | null {
22 const known = new Set(order);
23 const arrived = keys.filter((itemKey) => !known.has(itemKey));
24 return arrived.length === 0 ? order : [...(order ?? []), ...arrived];
25}
26
27/**
28 * Keeps the order a list had when it opened, so a row that a pin moves does not slide out
29 * from under the pointer or the keyboard highlight. Rows that load later join at the end and
30 * then hold their place too. The new order shows on the next open.
31 */
32export function useOrderWhileOpen<T>(
33 items: ReadonlyArray<T>,
34 key: (item: T) => string,
35 open: boolean,
36): T[] {
37 const [order, setOrder] = useState<ReadonlyArray<string> | null>(null);
38 if (!open) {
39 if (order !== null) setOrder(null);
40 return [...items];
41 }
42 const ordered = keepOrder(items, order, key);
43 const nextOrder = withArrivedKeys(order, ordered.map(key));
44 if (nextOrder !== order) setOrder(nextOrder);
45 return ordered;
46}