21842: selected items now scroll into view
[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, useState, 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';
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     },
30 });
31
32 type SingleTabProps<T> = {
33     label: string;
34     items: T[];
35 };
36
37 type TabPanelProps = {
38   children: React.ReactNode;
39   value: number;
40   index: number;
41 };
42
43 type TabbedListProps<T> = {
44     tabbedListContents: SingleTabProps<T>[];
45     renderListItem?: (item: T) => React.ReactNode;
46     injectedStyles?: string;
47     keypress?: { key: string };
48     selectedIndex?: number;
49 };
50
51 export const TabbedList = withStyles(tabbedListStyles)(<T, _>({ tabbedListContents, renderListItem, selectedIndex, keypress, injectedStyles, classes }: TabbedListProps<T> & WithStyles<TabbedListClasses>) => {
52     const [tabNr, setTabNr] = useState(0);
53     const listRefs = useRef<HTMLDivElement[]>([]);
54
55     useEffect(() => {
56       if (keypress) handleKeyPress(keypress.key);
57     }, [keypress]);
58
59     useEffect(() => {
60         if (selectedIndex !== undefined && listRefs.current[selectedIndex]) {
61             listRefs.current[selectedIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
62         }
63     }, [selectedIndex]);
64
65     const handleKeyPress = (keypress: string) => {
66         const numTabs = tabbedListContents.length;
67         if (keypress === 'Tab') {
68             setTabNr((tabNr + 1) % numTabs);
69         }
70     };
71
72     const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
73         event.preventDefault();
74         setTabNr(newValue);
75     };
76
77     return (
78         <div className={classNames(classes.root, injectedStyles)}>
79             <div className={classes.tabs}>
80                 <Tabs
81                     value={tabNr}
82                     onChange={handleTabChange}
83                     fullWidth
84                 >
85                     {tabbedListContents.map((tab) => (
86                         <Tab label={tab.label} />
87                     ))}
88                 </Tabs>
89             </div>
90             <TabPanel
91                 value={tabNr}
92                 index={tabNr}
93             >
94                 <List className={classes.list}>
95                     {tabbedListContents[tabNr].items.map((item, i) => (
96                       <div ref={(el) => { if (!!el) listRefs.current[i] = el}}>
97                         <ListItem
98                         className={classes.listItem}
99                         selected={i === selectedIndex}
100                         >
101                           {renderListItem ? renderListItem(item) : JSON.stringify(item)}
102                         </ListItem>
103                       </div>
104                     ))}
105                 </List>
106             </TabPanel>
107         </div>
108     );
109 });
110
111 const TabPanel = ({ children, value, index }: TabPanelProps) => {
112     return <div hidden={value !== index}>{value === index && children}</div>;
113 };