CEC-4564: add visual nesting to CAN signal control (#406)

* add TrieSelect

* setup menu button

* CEC-4564: add trie select component

* add visual nesting

* remove unused imports
This commit is contained in:
Tristan Timblin
2023-08-02 15:44:08 -04:00
committed by GitHub
parent c118f676ee
commit 27ffdf01b0
7 changed files with 852 additions and 936 deletions

View File

@@ -338,76 +338,9 @@ exports[`Render Render 1`] = `
<div <div
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12" class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
> >
<div <ul
class="MuiBox-root MuiBox-root-0" class="MuiList-root MuiList-padding"
> />
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
type="button"
>
<span
class="MuiButton-label"
>
Select CAN Signals
</span>
</button>
<label
class="MuiFormControlLabel-root MuiFormControlLabel-labelPlacementStart"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorSecondary PrivateSwitchBase-checked-0 Mui-checked MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
checked=""
class="PrivateSwitchBase-input-0"
data-indeterminate="false"
type="checkbox"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Select All 0
</span>
</label>
</div>
<div
class="MuiCollapse-root MuiCollapse-hidden"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<ul
class="MuiList-root MuiList-padding"
/>
</div>
</div>
</div>
<ul <ul
class="makeStyles-chipList-0" class="makeStyles-chipList-0"
/> />

View File

@@ -169,7 +169,7 @@ const MainForm = ({ id }) => {
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<TrieSelect <TrieSelect
label="Select CAN Signals" label="All CAN Signals"
classification="Signals" classification="Signals"
options={canSignals.map((signal => signal.signal_name))} options={canSignals.map((signal => signal.signal_name))}
onChange={setSelectedCanSignals} onChange={setSelectedCanSignals}

View File

@@ -1,10 +1,10 @@
import React, { useState } from "react"; import React from "react";
import { import {
Box, Box,
Button,
Checkbox, Checkbox,
Chip, Chip,
Collapse, Collapse,
Divider,
FormControlLabel, FormControlLabel,
List, List,
ListItem, ListItem,
@@ -41,50 +41,19 @@ const TrieSelectList = ({
classification, classification,
options, options,
}) => { }) => {
const { selected, setSelected, remove } = useTrieSelect(); const { selected, remove } = useTrieSelect();
const [open, setOpen] = useState(false);
const classes = useStyles(); const classes = useStyles();
const trie = new Trie(options); const trie = new Trie(options);
const handleExpand = () => {
setOpen(open => !open);
};
const handleSelectAll = () => {
setSelected((selected) => {
if (selected.length === 0) {
return options;
}
return [];
})
}
return ( return (
<> <>
<Box sx={{ display: "flex", alignItems: "center" }}> <List>
<Button onClick={handleExpand} variant="contained" color="primary" disabled={options.length === 0}> <TrieSelectLevel
{label} node={trie.getRoot()}
</Button> classification={classification}
<FormControlLabel label={label}
label={`Select All ${options.length}`}
labelPlacement="start"
control={
<Checkbox
checked={selected.length === options.length}
onClick={handleSelectAll}
/>
}
/> />
</Box> </List>
<Collapse in={open}>
<List>
<TrieSelectLevel
node={trie.getRoot()}
classification={classification}
/>
</List>
</Collapse>
<ul className={classes.chipList}> <ul className={classes.chipList}>
{selected.map((signal) => { {selected.map((signal) => {
return ( return (
@@ -102,13 +71,18 @@ const TrieSelectLevel = ({
prefix = "", prefix = "",
node, node,
children, children,
classification classification,
label,
level = -1,
}) => { }) => {
const classes = useStyles(); const classes = useStyles();
const { selected, add, remove } = useTrieSelect(); const { selected, add, remove } = useTrieSelect();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const completeChildren = Object.values(node.children).filter(child => child.isComplete); const completeChildren = Object.values(node.children).filter(child => child.isComplete);
const hasCompleteChildren = completeChildren.length > 0;
const descendantCount = `${node.count} ${classification}`; const descendantCount = `${node.count} ${classification}`;
const isParentOfMultiple = completeChildren.length > 1;
const isAdoptiveParentOfMultiple = completeChildren.length <= 1 && node.count > 1;
const handleExpand = () => { const handleExpand = () => {
setOpen(open => !open); setOpen(open => !open);
@@ -140,7 +114,9 @@ const TrieSelectLevel = ({
return Array.from(result); return Array.from(result);
} }
const listItems = ( const indent = (level) => `${12 * level}px`;
const listItems = (level) => (
<> <>
{children} {children}
{Object.values(node.children).map((child) => { {Object.values(node.children).map((child) => {
@@ -152,13 +128,16 @@ const TrieSelectLevel = ({
node={child} node={child}
key={fullName} key={fullName}
classification={classification} classification={classification}
level={level}
> >
{child.isComplete && ( {child.isComplete && (
<ListItem className={classes.whiteBackground}> <ListItem className={classes.whiteBackground}>
<ListItemIcon> <Box sx={{ paddingLeft: indent(level), display: "flex", alignItems: "center" }}>
<Checkbox checked={isChecked} onClick={() => handleCheck([fullName], isChecked)} /> <ListItemIcon>
</ListItemIcon> <Checkbox checked={isChecked} onClick={() => handleCheck([fullName], isChecked)} />
<ListItemText primary={fullName} /> </ListItemIcon>
<ListItemText primary={fullName} />
</Box>
</ListItem> </ListItem>
)} )}
</TrieSelectLevel> </TrieSelectLevel>
@@ -167,15 +146,15 @@ const TrieSelectLevel = ({
</> </>
); );
const isParentOfMultiple = completeChildren.length > 1;
const isAdoptiveParentOfMultiple = completeChildren.length <= 1 && node.count > 1;
if (isParentOfMultiple || isAdoptiveParentOfMultiple) { if (isParentOfMultiple || isAdoptiveParentOfMultiple) {
const allDescendants = getWords(node, prefix); const allDescendants = getWords(node, prefix);
const isSelectAll = allDescendants.every(descendant => selected.includes(descendant)); const isSelectAll = allDescendants.every(descendant => selected.includes(descendant));
return ( return (
<> <>
<ListItem onClick={handleExpand} divider> <ListItem onClick={handleExpand} divider className={classes.defaultBackground}>
<ListItemText primary={prefix} secondary={descendantCount} /> <Box sx={{ paddingLeft: indent(level) }}>
<ListItemText primary={prefix === "" ? label : prefix} secondary={descendantCount} />
</Box>
<ListItemSecondaryAction> <ListItemSecondaryAction>
<Box sx={{ display: "flex", alignItems: "center", gap: "16px" }}> <Box sx={{ display: "flex", alignItems: "center", gap: "16px" }}>
<FormControlLabel <FormControlLabel
@@ -188,18 +167,19 @@ const TrieSelectLevel = ({
/> />
} }
/> />
{open ? <ExpandLess /> : <ExpandMore />} {open ? (<ExpandLess />) : (<ExpandMore />)}
</Box> </Box>
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItem> </ListItem>
<Collapse in={open}> <Collapse in={open}>
<List> <List>
{listItems} {listItems(level + 1)}
</List> </List>
{hasCompleteChildren && <Divider />}
</Collapse> </Collapse>
</> </>
) )
} }
return listItems; return listItems(level);
} }

View File

@@ -38,7 +38,7 @@ describe("TrieSelect", () => {
it("properly passes payload to callback", async () => { it("properly passes payload to callback", async () => {
const mockCallback = jest.fn(); const mockCallback = jest.fn();
const { getByText } = render( const { getAllByText } = render(
<TrieSelect <TrieSelect
label={"The input label"} label={"The input label"}
classification="Signal" classification="Signal"
@@ -47,7 +47,7 @@ describe("TrieSelect", () => {
/> />
); );
const selectAll = getByText("Select All 8"); const selectAll = getAllByText("Select All")[0];
userEvent.click(selectAll); userEvent.click(selectAll);
expect(mockCallback).toHaveBeenCalledWith(options); expect(mockCallback).toHaveBeenCalledWith(options);
}); });

View File

@@ -1 +1 @@
export { TrieSelect } from "./TrieSelect"; export * from "./TrieSelect";

View File

@@ -274,6 +274,7 @@ const useStyles = makeStyles((theme) => ({
maxWidth: "100%", maxWidth: "100%",
}, },
whiteBackground: { backgroundColor: "White" }, whiteBackground: { backgroundColor: "White" },
defaultBackground: { backgroundColor: "#fafafa" },
progressIcon: { width: 40, height: 40 }, progressIcon: { width: 40, height: 40 },
progressSuccess: { color: "green" }, progressSuccess: { color: "green" },
progressError: { color: "red" }, progressError: { color: "red" },