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