1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
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';
13 type TabbedListClasses = 'root' | 'tabs' | 'listItem' | 'selected' | 'spinner' | 'notFoundLabel';
15 const tabbedListStyles: CustomStyleRulesCallback<TabbedListClasses> = (theme: ArvadosTheme) => ({
18 flexDirection: 'column',
21 scrollbarWidth: 'none',
22 '&::-webkit-scrollbar': {
27 backgroundColor: theme.palette.background.paper,
31 borderBottom: '1px solid lightgrey',
37 backgroundColor: theme.palette.grey[200],
41 backgroundColor: `${theme.palette.grey['300']} !important`
45 justifyContent: 'center',
51 padding: theme.spacing(1),
52 color: theme.palette.grey[700],
57 type TabPanelProps = {
58 children: React.ReactNode;
63 type TabbedListProps<T> = {
64 tabbedListContents: Record<string, T[]>;
65 injectedStyles?: string;
66 selectedIndex?: number;
68 includeContentsLength: boolean;
70 handleSelect?: (selection: T) => React.MouseEventHandler<HTMLElement> | undefined;
71 renderListItem?: (item: T) => React.ReactNode;
72 handleTabChange?: (event: React.SyntheticEvent, newValue: number) => void;
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]] || [];
83 if (selectedIndex !== undefined && listRefs.current[selectedTabLabel][selectedIndex]) {
84 listRefs.current[selectedTabLabel][selectedIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
88 const TabPanel = ({ children, value, index }: TabPanelProps) => {
89 return <div hidden={value !== index}>{value === index && children}</div>;
93 <div className={classNames(classes.root, injectedStyles)}>
95 className={classes.tabs}
97 onChange={handleTabChange}
100 {tabLabels.map((label) => (
101 <Tab key={label} data-cy={`${label}-tab-label`} label={includeContentsLength ? `${label} (${tabbedListContents[label].length})` : label} />
108 {isWorking ? <div className={classes.spinner}><InlinePulser /></div> :
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}>
114 className={classNames(classes.listItem, { [classes.selected]: i === selectedIndex })}
115 selected={i === selectedIndex}
116 onClick={handleSelect && handleSelect(item)}
118 {renderListItem ? renderListItem(item) : JSON.stringify(item)}