diff --git a/ui/package.json b/ui/package.json index 9f546f5..d712d06 100644 --- a/ui/package.json +++ b/ui/package.json @@ -56,6 +56,7 @@ ] }, "devDependencies": { + "@types/react-window": "1.8.5", "redux-devtools": "3.7.0" }, "homepage": "/[[.RootPath]]" diff --git a/ui/src/components/GroupSelect.tsx b/ui/src/components/GroupSelect.tsx index 35ad5ca..a6efd86 100644 --- a/ui/src/components/GroupSelect.tsx +++ b/ui/src/components/GroupSelect.tsx @@ -1,7 +1,10 @@ import React from "react"; -import { makeStyles } from "@material-ui/core/styles"; +import { makeStyles, useTheme } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import Autocomplete from "@material-ui/lab/Autocomplete"; +import useMediaQuery from "@material-ui/core/useMediaQuery"; +import ListSubheader from "@material-ui/core/ListSubheader"; +import { VariableSizeList, ListChildComponentProps } from "react-window"; import { GroupInfo } from "../api"; import { isDarkTheme } from "../theme"; @@ -34,6 +37,12 @@ export default function GroupSelect(props: Props) { return ( + > + } options={props.groups} getOptionLabel={(option: GroupInfo) => option.group} style={{ width: 300 }} @@ -49,3 +58,83 @@ export default function GroupSelect(props: Props) { /> ); } + +// Virtualized list. +// Reference: https://v4.mui.com/components/autocomplete/#virtualization + +const LISTBOX_PADDING = 8; // px + +function renderRow(props: ListChildComponentProps) { + const { data, index, style } = props; + return React.cloneElement(data[index], { + style: { + ...style, + top: (style.top as number) + LISTBOX_PADDING, + }, + }); +} + +const OuterElementContext = React.createContext({}); + +const OuterElementType = React.forwardRef((props, ref) => { + const outerProps = React.useContext(OuterElementContext); + return
; +}); + +function useResetCache(data: any) { + const ref = React.useRef(null); + React.useEffect(() => { + if (ref.current != null) { + ref.current.resetAfterIndex(0, true); + } + }, [data]); + return ref; +} + +// Adapter for react-window +const ListboxComponent = React.forwardRef( + function ListboxComponent(props, ref) { + const { children, ...other } = props; + const itemData = React.Children.toArray(children); + const theme = useTheme(); + const smUp = useMediaQuery(theme.breakpoints.up("sm"), { noSsr: true }); + const itemCount = itemData.length; + const itemSize = smUp ? 36 : 48; + + const getChildSize = (child: React.ReactNode) => { + if (React.isValidElement(child) && child.type === ListSubheader) { + return 48; + } + return itemSize; + }; + + const getHeight = () => { + if (itemCount > 8) { + return 8 * itemSize; + } + return itemData.map(getChildSize).reduce((a, b) => a + b, 0); + }; + + const gridRef = useResetCache(itemCount); + + return ( +
+ + getChildSize(itemData[index])} + overscanCount={5} + itemCount={itemCount} + > + {renderRow} + + +
+ ); + } +); diff --git a/ui/yarn.lock b/ui/yarn.lock index 597f23c..d99401e 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -2036,6 +2036,13 @@ dependencies: "@types/react" "*" +"@types/react-window@1.8.5": + version "1.8.5" + resolved "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" + integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^17.0.29": version "17.0.29" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.29.tgz#9535f3fc01a4981ce9cadcf0daa2593c0c2f2251"