Skip to content

Commit

Permalink
refactor: Make SelectMultiple component independent of `FileUploadA…
Browse files Browse the repository at this point in the history
…ndLabel` (#3760)
  • Loading branch information
DafyddLlyr authored Oct 4, 2024
1 parent ae7d417 commit 934b242
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { FileUploadSlot } from "../FileUpload/model";
import { UploadedFileCard } from "../shared/PrivateFileUpload/UploadedFileCard";
import { FileList } from "./model";
import { fileLabelSchema, formatFileLabelSchemaErrors } from "./schema";
import { SelectMultiple } from "./SelectMultiple";
import { SelectMultipleFileTypes } from "./SelectMultipleFileTypes";

interface FileTaggingModalProps {
uploadedFiles: FileUploadSlot[];
Expand Down Expand Up @@ -83,7 +83,7 @@ export const FileTaggingModal = ({
key={slot.id}
removeFile={() => removeFile(slot)}
/>
<SelectMultiple
<SelectMultipleFileTypes
uploadedFile={slot}
fileList={fileList}
setFileList={setFileList}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import ArrowIcon from "@mui/icons-material/KeyboardArrowDown";
import Autocomplete, {
import {
AutocompleteChangeReason,
autocompleteClasses,
AutocompleteProps,
} from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import FormControl from "@mui/material/FormControl";
import { inputLabelClasses } from "@mui/material/InputLabel";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListSubheader from "@mui/material/ListSubheader";
import { outlinedInputClasses } from "@mui/material/OutlinedInput";
import { styled } from "@mui/material/styles";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import capitalize from "lodash/capitalize";
import React, { forwardRef, PropsWithChildren, useMemo } from "react";
import { borderedFocusStyle } from "theme";
import { CustomCheckbox, SelectMultiple } from "ui/shared/SelectMultiple";

import { FileUploadSlot } from "../FileUpload/model";
import {
Expand All @@ -37,100 +30,6 @@ interface Option extends UserFile {
category: keyof FileList;
}

const StyledAutocomplete = styled(
Autocomplete<Option, true, true, false, "div">,
)(({ theme }) => ({
marginTop: theme.spacing(2),
// Prevent label from overlapping expand icon
"& > div > label": {
paddingRight: theme.spacing(3),
},
// Vertically center "large" size caret icon
[`& .${autocompleteClasses.endAdornment}`]: {
top: "unset",
},
"&:focus-within": {
"& svg": {
color: "black",
},
},
}));

const StyledTextField = styled(TextField)(({ theme }) => ({
"&:focus-within": {
...borderedFocusStyle,
[`& .${outlinedInputClasses.notchedOutline}`]: {
border: "1px solid transparent !important",
},
},
[`& .${outlinedInputClasses.notchedOutline}`]: {
borderRadius: 0,
border: `1px solid${theme.palette.border.main} !important`,
},
"& fieldset": {
borderColor: theme.palette.border.main,
},
backgroundColor: theme.palette.background.paper,
[`& .${outlinedInputClasses.root}, input`]: {
cursor: "pointer",
},
[`& .${inputLabelClasses.root}`]: {
textDecoration: "underline",
color: theme.palette.primary.main,
"&[data-shrink=true]": {
textDecoration: "none",
color: theme.palette.text.primary,
paddingY: 0,
transform: "translate(0px, -22px) scale(0.85)",
},
},
}));

const CustomCheckbox = styled("span")(({ theme }) => ({
display: "inline-flex",
flexShrink: 0,
position: "relative",
width: 40,
height: 40,
borderColor: theme.palette.text.primary,
border: "2px solid",
background: "transparent",
marginRight: theme.spacing(1.5),
"&.selected::after": {
content: "''",
position: "absolute",
height: 24,
width: 12,
borderColor: theme.palette.text.primary,
borderBottom: "5px solid",
borderRight: "5px solid",
left: "50%",
top: "42%",
transform: "translate(-50%, -50%) rotate(45deg)",
cursor: "pointer",
},
}));

/**
* Function which returns the Input component used by Autocomplete
*/
const renderInput: AutocompleteProps<
Option,
true,
true,
false,
"div"
>["renderInput"] = (params) => (
<StyledTextField
{...params}
InputProps={{
...params.InputProps,
notched: false,
}}
label="What does this file show? (select all that apply)"
/>
);

/**
* Function which returns the groups (ul elements) used by Autocomplete
*/
Expand Down Expand Up @@ -180,8 +79,6 @@ const renderOption: AutocompleteProps<
</ListItem>
);

const PopupIcon = <ArrowIcon sx={{ color: "primary.main" }} fontSize="large" />;

/**
* Custom Listbox component
* Used to wrap options within the autocomplete and append a custom element above the option list
Expand All @@ -199,7 +96,7 @@ const ListboxComponent = forwardRef<typeof Box, PropsWithChildren>(
),
);

export const SelectMultiple = (props: SelectMultipleProps) => {
export const SelectMultipleFileTypes = (props: SelectMultipleProps) => {
const { uploadedFile, fileList, setFileList } = props;

const initialTags = getTagsForSlot(uploadedFile.id, fileList);
Expand All @@ -224,7 +121,7 @@ export const SelectMultiple = (props: SelectMultipleProps) => {
const value: Option[] = useMemo(
() =>
initialTags.flatMap((tag) => options.filter(({ name }) => name === tag)),
[initialTags],
[initialTags, options],
);

/**
Expand Down Expand Up @@ -266,46 +163,19 @@ export const SelectMultiple = (props: SelectMultipleProps) => {
};

return (
<FormControl
<SelectMultiple<Option>
getOptionLabel={(option) => option.name}
groupBy={(option) => option.category}
id={`select-multiple-file-tags-${uploadedFile.id}`}
isOptionEqualToValue={(option, value) => option.name === value.name}
key={`form-${uploadedFile.id}`}
sx={{ display: "flex", flexDirection: "column" }}
>
<StyledAutocomplete
role="status"
aria-atomic={true}
aria-live="polite"
disableClearable
disableCloseOnSelect
getOptionLabel={(option) => option.name}
groupBy={(option) => option.category}
id={`select-multiple-file-tags-${uploadedFile.id}`}
isOptionEqualToValue={(option, value) => option.name === value.name}
ListboxComponent={ListboxComponent}
multiple
onChange={handleChange}
options={options}
popupIcon={PopupIcon}
renderGroup={renderGroup}
renderInput={renderInput}
renderOption={renderOption}
value={value}
ChipProps={{
variant: "uploadedFileTag",
size: "small",
sx: { pointerEvents: "none" },
onDelete: undefined,
}}
componentsProps={{
popupIndicator: {
disableRipple: true,
},
popper: {
sx: {
boxShadow: 10,
},
},
}}
/>
</FormControl>
label="What does this file show? (select all that apply)"
ListboxComponent={ListboxComponent}
onChange={handleChange}
options={options}
renderGroup={renderGroup}
renderOption={renderOption}
value={value}
/>
);
};
146 changes: 146 additions & 0 deletions editor.planx.uk/src/ui/shared/SelectMultiple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import ArrowIcon from "@mui/icons-material/KeyboardArrowDown";
import Autocomplete, {
autocompleteClasses,
AutocompleteProps,
} from "@mui/material/Autocomplete";
import FormControl from "@mui/material/FormControl";
import { inputLabelClasses } from "@mui/material/InputLabel";
import { outlinedInputClasses } from "@mui/material/OutlinedInput";
import { styled } from "@mui/material/styles";
import TextField from "@mui/material/TextField";
import React from "react";
import { borderedFocusStyle } from "theme";

const PopupIcon = (
<ArrowIcon
sx={(theme) => ({ color: theme.palette.primary.main })}
fontSize="large"
/>
);

type RequiredAutocompleteProps<T> = Pick<
AutocompleteProps<T, true, true, false, "div">,
"options" | "onChange"
>;

type OptionalAutocompleteProps<T> = Partial<
Omit<AutocompleteProps<T, true, true, false, "div">, "multiple">
>;

type Props<T> = {
label: string;
} & RequiredAutocompleteProps<T> &
OptionalAutocompleteProps<T>;

const StyledAutocomplete = styled(Autocomplete)(({ theme }) => ({
marginTop: theme.spacing(2),
"& > div > label": {
paddingRight: theme.spacing(3),
},
[`& .${autocompleteClasses.endAdornment}`]: {
top: "unset",
},
"&:focus-within": {
"& svg": {
color: "black",
},
},
})) as typeof Autocomplete;

const StyledTextField = styled(TextField)(({ theme }) => ({
"&:focus-within": {
...borderedFocusStyle,
[`& .${outlinedInputClasses.notchedOutline}`]: {
border: "1px solid transparent !important",
},
},
[`& .${outlinedInputClasses.notchedOutline}`]: {
borderRadius: 0,
border: `1px solid${theme.palette.border.main} !important`,
},
"& fieldset": {
borderColor: theme.palette.border.main,
},
backgroundColor: theme.palette.background.paper,
[`& .${outlinedInputClasses.root}, input`]: {
cursor: "pointer",
},
[`& .${inputLabelClasses.root}`]: {
textDecoration: "underline",
color: theme.palette.primary.main,
"&[data-shrink=true]": {
textDecoration: "none",
color: theme.palette.text.primary,
paddingY: 0,
transform: "translate(0px, -22px) scale(0.85)",
},
},
}));

export const CustomCheckbox = styled("span")(({ theme }) => ({
display: "inline-flex",
flexShrink: 0,
position: "relative",
width: 40,
height: 40,
borderColor: theme.palette.text.primary,
border: "2px solid",
background: "transparent",
marginRight: theme.spacing(1.5),
"&.selected::after": {
content: "''",
position: "absolute",
height: 24,
width: 12,
borderColor: theme.palette.text.primary,
borderBottom: "5px solid",
borderRight: "5px solid",
left: "50%",
top: "42%",
transform: "translate(-50%, -50%) rotate(45deg)",
cursor: "pointer",
},
}));

export function SelectMultiple<T>(props: Props<T>) {
return (
<FormControl sx={{ display: "flex", flexDirection: "column" }}>
<StyledAutocomplete<T, true, true, false, "div">
role="status"
aria-atomic={true}
aria-live="polite"
disableClearable
disableCloseOnSelect
multiple
popupIcon={PopupIcon}
renderInput={(params) => (
<StyledTextField
{...params}
InputProps={{
...params.InputProps,
notched: false,
}}
label={props.label}
/>
)}
ChipProps={{
variant: "uploadedFileTag",
size: "small",
sx: { pointerEvents: "none" },
onDelete: undefined,
}}
componentsProps={{
popupIndicator: {
disableRipple: true,
},
popper: {
sx: {
boxShadow: 10,
},
},
}}
{...props}
/>
</FormControl>
);
}

0 comments on commit 934b242

Please sign in to comment.