Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / hooks / useResponsiveHorizontalList.ts
blobc8337ab1f054a3290863f4ba3918fbb7eef7055a
1 import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
3 type UseResponsiveItemsOptions = { gap: number; maxChildWidth: number };
5 /** Tracks seen visible counts against children width to optimize reconciliation.
6  * Monitors previously observed visible counts and ensures they align with constraints.
7  * If the container width has not been reached and the current visible count has not been
8  * observed, attempts to add a visible item; otherwise, removes one. The process stops early
9  * when decreasing to a previously seen visible count. This incremental resolution optimizes
10  * the adjustment of the visible item count based on the container dimensions. */
11 export const useResponsiveHorizontalList = <T>(items: T[], options: UseResponsiveItemsOptions) => {
12     const [visibleCount, setVisibleCount] = useState(0);
13     const ref = useRef<HTMLDivElement>(null);
14     const seen = useRef<Map<number, number>>(new Map());
15     const max = useRef<number>(items.length);
16     max.current = items.length;
18     const reconciliate = useCallback(() => {
19         if (!ref.current) return;
21         const children = Array.from(ref.current?.children ?? []) as HTMLElement[];
22         const maxWidth = ref.current?.offsetWidth ?? 0;
23         if (maxWidth === 0) return;
25         const checkLower = (n: number): number => {
26             const widthForCount = seen.current.get(n);
27             if (widthForCount === undefined) return n;
28             return widthForCount > maxWidth ? checkLower(n - 1) : n;
29         };
31         const checkUpper = (n: number): number => {
32             const widthForCount = seen.current.get(n);
33             if (widthForCount === undefined) return n;
34             return widthForCount < maxWidth ? checkUpper(n + 1) : n - 1;
35         };
37         setVisibleCount((currentCount) => {
38             const cached = seen.current.get(currentCount);
39             const width = cached ?? children.reduce((total, { offsetWidth }) => total + offsetWidth + options.gap, 0);
41             /* children may have not rendered yet */
42             if (width !== 0) seen.current.set(currentCount, width);
44             if (width > maxWidth) return Math.max(0, checkLower(currentCount - 1));
45             if (width < maxWidth) return Math.min(max.current, checkUpper(currentCount + 1));
47             return currentCount;
48         });
49     }, []);
51     useLayoutEffect(() => {
52         setVisibleCount(() => {
53             const maxWidth = ref.current?.offsetWidth ?? 0;
54             return Math.min(max.current, Math.floor(maxWidth / (options.maxChildWidth + options.gap)));
55         });
57         window.addEventListener('resize', reconciliate);
58         return () => window.removeEventListener('resize', reconciliate);
59     }, []);
61     useLayoutEffect(() => {
62         seen.current.clear();
63         reconciliate();
64     }, [items]);
66     useLayoutEffect(reconciliate, [visibleCount]);
68     return useMemo(
69         () => ({
70             ref,
71             visible: items.slice(0, visibleCount),
72             hidden: items.slice(visibleCount),
73         }),
74         [visibleCount, items]
75     );