21842: merge cleanup, changed ListItem to ListItemButton
[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
12 type TabbedListClasses = 'root' | 'tabs' | 'list' | 'listItem' | 'selected' | 'notFoundLabel';
13
14 const tabbedListStyles: CustomStyleRulesCallback<TabbedListClasses> = (theme: ArvadosTheme) => ({
15     root: {
16         overflowY: 'auto',
17     },
18     tabs: {
19         backgroundColor: theme.palette.background.paper,
20         position: 'sticky',
21         top: 0,
22         zIndex: 1,
23         borderBottom: '1px solid lightgrey',
24     },
25     list: {
26         overflowY: 'scroll',
27     },
28     listItem: {
29         cursor: 'pointer',
30         '&:hover': {
31             backgroundColor: theme.palette.grey[200],
32         }
33     },
34     selected: {
35         backgroundColor: `${theme.palette.grey['300']} !important`
36     },
37     notFoundLabel: {
38         cursor: 'default',
39         padding: theme.spacing(1),
40         color: theme.palette.grey[700],
41         textAlign: 'center',
42     },
43 });
44
45 type TabPanelProps = {
46   children: React.ReactNode;
47   value: number;
48   index: number;
49 };
50
51 type TabbedListProps<T> = {
52     tabbedListContents: Record<string, T[]>;
53     injectedStyles?: string;
54     selectedIndex?: number;
55     selectedTab?: number;
56     includeContentsLength: boolean;
57     handleSelect?: (selection: T) => React.MouseEventHandler<HTMLElement> | undefined;
58     renderListItem?: (item: T) => React.ReactNode;
59     handleTabChange?: (event: React.SyntheticEvent, newValue: number) => void;
60 };
61
62 export const TabbedList = withStyles(tabbedListStyles)(<T,>({ tabbedListContents, selectedIndex, selectedTab, injectedStyles, classes, handleSelect, renderListItem, handleTabChange, includeContentsLength }: TabbedListProps<T> & WithStyles<TabbedListClasses>) => {
63     const tabNr = selectedTab || 0;
64     const listRefs = useRef<HTMLDivElement[]>([]);
65     const tabLabels = Object.keys(tabbedListContents);
66
67     useEffect(() => {
68         if (selectedIndex !== undefined && listRefs.current[selectedIndex]) {
69             listRefs.current[selectedIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
70         }
71     }, [selectedIndex]);
72
73     return (
74         <div className={classNames(classes.root, injectedStyles)}>
75             <div className={classes.tabs}>
76                 <Tabs
77                     value={tabNr}
78                     onChange={handleTabChange}
79                     variant='fullWidth'
80                 >
81                     {tabLabels.map((label) => (
82                         <Tab data-cy={`${label}-tab-label`} label={includeContentsLength ? `${label} (${tabbedListContents[label].length})` : label} />
83                     ))}
84                 </Tabs>
85             </div>
86             <TabPanel
87                 value={tabNr}
88                 index={tabNr}
89             >
90                 <List className={classes.list}>
91                   {tabbedListContents[tabLabels[tabNr]].length === 0 && <div className={classes.notFoundLabel}>no matching {tabLabels[tabNr]} found</div>}
92                     {tabbedListContents[tabLabels[tabNr]].map((item, i) => (
93                       <div ref={(el) => { if (!!el) listRefs.current[i] = el}}>
94                         <ListItemButton
95                         className={classNames(classes.listItem, { [classes.selected]: i === selectedIndex })}
96                         selected={i === selectedIndex}
97                         onClick={handleSelect && handleSelect(item)}
98                         >
99                           {renderListItem ? renderListItem(item) : JSON.stringify(item)}
100                         </ListItemButton>
101                       </div>
102                     ))}
103                 </List>
104             </TabPanel>
105         </div>
106     );
107 });
108
109 const TabPanel = ({ children, value, index }: TabPanelProps) => {
110     return <div hidden={value !== index}>{value === index && children}</div>;
111 };