21842: removed word wrap on listItems
[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' | '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         scrollbarWidth: 'none',
22         '&::-webkit-scrollbar': {
23             display: 'none',
24         },
25     },
26     tabs: {
27         backgroundColor: theme.palette.background.paper,
28         position: 'sticky',
29         top: 0,
30         zIndex: 1,
31         borderBottom: '1px solid lightgrey',
32     },
33     listItem: {
34         height: '2rem',
35         cursor: 'pointer',
36         '&:hover': {
37             backgroundColor: theme.palette.grey[200],
38         }
39     },
40     selected: {
41         backgroundColor: `${theme.palette.grey['300']} !important`
42     },
43     spinner: {
44         display: 'flex',
45         justifyContent: 'center',
46         alignItems: 'center',
47         height: '4rem',
48     },
49     notFoundLabel: {
50         cursor: 'default',
51         padding: theme.spacing(1),
52         color: theme.palette.grey[700],
53         textAlign: 'center',
54     },
55 });
56
57 type TabPanelProps = {
58   children: React.ReactNode;
59   value: number;
60   index: number;
61 };
62
63 type TabbedListProps<T> = {
64     tabbedListContents: Record<string, T[]>;
65     injectedStyles?: string;
66     selectedIndex?: number;
67     selectedTab?: number;
68     includeContentsLength: boolean;
69     isWorking?: boolean;
70     handleSelect?: (selection: T) => React.MouseEventHandler<HTMLElement> | undefined;
71     renderListItem?: (item: T) => React.ReactNode;
72     handleTabChange?: (event: React.SyntheticEvent, newValue: number) => void;
73 };
74
75 export const TabbedList = withStyles(tabbedListStyles)(<T,>({ tabbedListContents, selectedIndex = 0, selectedTab, isWorking, injectedStyles, classes, handleSelect, renderListItem, handleTabChange, includeContentsLength }: TabbedListProps<T> & WithStyles<TabbedListClasses>) => {
76     const tabNr = selectedTab || 0;
77     const tabLabels = Object.keys(tabbedListContents);
78     const listRefs = useRef<Record<string, HTMLElement[]>>(tabLabels.reduce((acc, label) => ({ ...acc, [label]: [] }), {}));
79     const selectedTabLabel = tabLabels[tabNr];
80     const listContents = tabbedListContents[tabLabels[tabNr]] || [];
81
82     useEffect(() => {
83         if (selectedIndex !== undefined && listRefs.current[selectedTabLabel][selectedIndex]) {
84             listRefs.current[selectedTabLabel][selectedIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
85         }
86     }, [selectedIndex]);
87
88     const TabPanel = ({ children, value, index }: TabPanelProps) => {
89         return <div hidden={value !== index}>{value === index && children}</div>;
90     };
91
92     return (
93         <div className={classNames(classes.root, injectedStyles)}>
94             <Tabs
95                 className={classes.tabs}
96                 value={tabNr}
97                 onChange={handleTabChange}
98                 variant='fullWidth'
99             >
100                 {tabLabels.map((label) => (
101                     <Tab key={label} data-cy={`${label}-tab-label`} label={includeContentsLength ? `${label} (${tabbedListContents[label].length})` : label} />
102                 ))}
103             </Tabs>
104             <TabPanel
105                 value={tabNr}
106                 index={tabNr}
107             >
108                 {isWorking ? <div className={classes.spinner}><InlinePulser /></div> :
109                     <List dense>
110                     {listContents.length === 0 && <div className={classes.notFoundLabel}>no matching {tabLabels[tabNr]} found</div>}
111                         {listContents.map((item, i) => (
112                         <div ref={(el) => { if (el) listRefs.current[selectedTabLabel][i] = el}} key={i}>
113                             <ListItemButton
114                                 className={classNames(classes.listItem, { [classes.selected]: i === selectedIndex })}
115                                 selected={i === selectedIndex}
116                                 onClick={handleSelect && handleSelect(item)}
117                                 >
118                                 {renderListItem ? renderListItem(item) : JSON.stringify(item)}
119                             </ListItemButton>
120                         </div>
121                         ))}
122                     </List>
123                 }
124             </TabPanel>
125         </div>
126     );
127 });