Merge branch '21705-go-deps-update-all'
[arvados.git] / services / workbench2 / src / components / multiselect-toolbar / ms-toolbar-overflow-wrapper.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React, { useState, useRef, useEffect } from 'react';
6 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
7 import classnames from 'classnames';
8 import { ArvadosTheme } from 'common/custom-theme';
9 import { OverflowMenu, OverflowChild } from './ms-toolbar-overflow-menu';
10
11 type CssRules = 'visible' | 'inVisible' | 'toolbarWrapper' | 'overflowStyle';
12
13 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
14     visible: {
15         order: 0,
16         visibility: 'visible',
17         opacity: 1,
18     },
19     inVisible: {
20         order: 100,
21         visibility: 'hidden',
22         pointerEvents: 'none',
23     },
24     toolbarWrapper: {
25         display: 'flex',
26         overflow: 'hidden',
27         padding: '0 0px 0 20px',
28         width: '100%',
29     },
30     overflowStyle: {
31         order: 99,
32         position: 'sticky',
33         right: '-2rem',
34         width: 0,
35     },
36 });
37
38 type WrapperProps = {
39     children: OverflowChild[];
40     menuLength: number;
41 };
42
43 export const IntersectionObserverWrapper = withStyles(styles)((props: WrapperProps & WithStyles<CssRules>) => {
44     const { classes, children, menuLength } = props;
45     const lastEntryId = (children[menuLength - 1] as any).props['data-targetid'];
46     const navRef = useRef<any>(null);
47     const [visibilityMap, setVisibilityMap] = useState<Record<string, boolean>>({});
48     const [numHidden, setNumHidden] = useState(() => findNumHidden(visibilityMap));
49     const prevNumHidden = useRef(numHidden);
50     
51     const handleIntersection = (entries) => {
52         const updatedEntries: Record<string, boolean> = {};
53         entries.forEach((entry) => {
54             const targetid = entry.target.dataset.targetid as string;
55             //if true, the element is visible
56             if (entry.isIntersecting) {
57                 updatedEntries[targetid] = true;
58             } else {
59                 updatedEntries[targetid] = false;
60             }
61         });
62
63         setVisibilityMap((prev) => ({
64             ...prev,
65             ...updatedEntries,
66             [lastEntryId]: Object.keys(updatedEntries)[0] === lastEntryId,
67         }));
68     };
69
70     //ensures that the last element is always visible if the second to last is visible
71     useEffect(() => {
72         if ((prevNumHidden.current > 1 || prevNumHidden.current === 0) && numHidden === 1) {
73             setVisibilityMap((prev) => ({
74                 ...prev,
75                 [lastEntryId]: true,
76             }));
77         }
78         prevNumHidden.current = numHidden;
79     }, [numHidden, lastEntryId]);
80
81     useEffect(() => {
82         setNumHidden(findNumHidden(visibilityMap));
83     }, [visibilityMap]);
84
85     useEffect((): any => {
86         setVisibilityMap({});
87         const observer = new IntersectionObserver(handleIntersection, {
88             root: navRef.current,
89             rootMargin: '0px -30px 0px 0px',
90             threshold: 1,
91         });
92         // We are adding observers to child elements of the container div
93         // with ref as navRef. Notice that we are adding observers
94         // only if we have the data attribute targetid on the child element
95         if (navRef.current)
96             Array.from(navRef.current.children).forEach((item: any) => {
97                 if (item.dataset.targetid) {
98                     observer.observe(item);
99                 }
100             });
101         return () => {
102             observer.disconnect();
103         };
104         // eslint-disable-next-line
105     }, [menuLength]);
106
107     function findNumHidden(visMap: {}) {
108         return Object.values(visMap).filter((x) => x === false).length;
109     }
110
111     return (
112         <div
113             className={classes.toolbarWrapper}
114             ref={navRef}
115         >
116             {React.Children.map(children, (child) => {
117                 return React.cloneElement(child, {
118                     className: classnames(child.props.className, {
119                         [classes.visible]: !!visibilityMap[child.props['data-targetid']],
120                         [classes.inVisible]: !visibilityMap[child.props['data-targetid']],
121                     }),
122                 });
123             })}
124             {numHidden >= 2 && (
125                 <OverflowMenu
126                     visibilityMap={visibilityMap}
127                     className={classes.overflowStyle}
128                 >
129                     {children.filter((child) => !child.props['data-targetid'].includes("Divider"))}
130                 </OverflowMenu>
131             )}
132         </div>
133     );
134 });