21842: fixed lost anchorEl bug
[arvados.git] / services / workbench2 / src / components / tabbedList / tabbed-list.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React, { useEffect, useRef } from 'react';
6 import { Tabs, Tab, List, ListItemButton } from '@mui/material';
7 import { CustomStyleRulesCallback } from 'common/custom-theme';
8 import { WithStyles, withStyles } from '@mui/styles';
9 import classNames from 'classnames';
10 import { ArvadosTheme } from 'common/custom-theme';
11 import { InlinePulser } from 'components/loading/inline-pulser';
12
13 type TabbedListClasses = 'root' | 'tabs' | 'tabPanel' | 'listItem' | 'selected' | 'spinner' | 'notFoundLabel';
14
15 const tabbedListStyles: CustomStyleRulesCallback<TabbedListClasses> = (theme: ArvadosTheme) => ({
16     root: {
17         display: 'flex',
18         flexDirection: 'column',
19         height: '100%',
20         overflowY: 'auto',
21     },
22     tabs: {
23         backgroundColor: theme.palette.background.paper,
24         position: 'sticky',
25         top: 0,
26         zIndex: 1,
27         borderBottom: '1px solid lightgrey',
28     },
29     tabPanel: {
30         overflowY: 'scroll',
31     },
32     listItem: {
33         cursor: 'pointer',
34         '&:hover': {
35             backgroundColor: theme.palette.grey[200],
36         }
37     },
38     selected: {
39         backgroundColor: `${theme.palette.grey['300']} !important`
40     },
41     spinner: {
42         display: 'flex',
43         justifyContent: 'center',
44         alignItems: 'center',
45         height: '4rem',
46     },
47     notFoundLabel: {
48         cursor: 'default',
49         padding: theme.spacing(1),
50         color: theme.palette.grey[700],
51         textAlign: 'center',
52     },
53 });
54
55 type TabPanelProps = {
56   children: React.ReactNode;
57   value: number;
58   index: number;
59 };
60
61 type TabbedListProps<T> = {
62     tabbedListContents: Record<string, T[]>;
63     injectedStyles?: string;
64     selectedIndex?: number;
65     selectedTab?: number;
66     includeContentsLength: boolean;
67     isWorking?: boolean;
68     handleSelect?: (selection: T) => React.MouseEventHandler<HTMLElement> | undefined;
69     renderListItem?: (item: T) => React.ReactNode;
70     handleTabChange?: (event: React.SyntheticEvent, newValue: number) => void;
71 };
72
73 export const TabbedList = withStyles(tabbedListStyles)(<T,>({ tabbedListContents, selectedIndex, selectedTab, isWorking, injectedStyles, classes, handleSelect, renderListItem, handleTabChange, includeContentsLength }: TabbedListProps<T> & WithStyles<TabbedListClasses>) => {
74     const tabNr = selectedTab || 0;
75     const listRefs = useRef<HTMLDivElement[]>([]);
76     const tabLabels = Object.keys(tabbedListContents);
77
78     useEffect(() => {
79         if (selectedIndex !== undefined && listRefs.current[selectedIndex]) {
80             listRefs.current[selectedIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
81         }
82     }, [selectedIndex]);
83
84     const TabPanel = ({ children, value, index }: TabPanelProps) => {
85         return <div className={classes.tabPanel} hidden={value !== index}>{value === index && children}</div>;
86     };
87
88     return (
89         <div className={classNames(classes.root, injectedStyles)}>
90             <div className={classes.tabs}>
91                 <Tabs
92                     value={tabNr}
93                     onChange={handleTabChange}
94                     variant='fullWidth'
95                 >
96                     {tabLabels.map((label) => (
97                         <Tab data-cy={`${label}-tab-label`} label={includeContentsLength ? `${label} (${tabbedListContents[label].length})` : label} />
98                     ))}
99                 </Tabs>
100             </div>
101             <TabPanel
102                 value={tabNr}
103                 index={tabNr}
104             >
105                 {isWorking ? <div className={classes.spinner}><InlinePulser /></div> :
106                     <List>
107                     {tabbedListContents[tabLabels[tabNr]].length === 0 && <div className={classes.notFoundLabel}>no matching {tabLabels[tabNr]} found</div>}
108                         {tabbedListContents[tabLabels[tabNr]].map((item, i) => (
109                         <div ref={(el) => { if (!!el) listRefs.current[i] = el}}>
110                             <ListItemButton
111                             className={classNames(classes.listItem, { [classes.selected]: i === selectedIndex })}
112                             selected={i === selectedIndex}
113                             onClick={handleSelect && handleSelect(item)}
114                             >
115                             {renderListItem ? renderListItem(item) : JSON.stringify(item)}
116                             </ListItemButton>
117                         </div>
118                         ))}
119                     </List>
120                 }
121             </TabPanel>
122         </div>
123     );
124 });