diff --git a/editor.planx.uk/src/@planx/components/FileUploadAndLabel/Modal.tsx b/editor.planx.uk/src/@planx/components/FileUploadAndLabel/Modal.tsx index 1b91833b3e..f9a4fdb0a3 100644 --- a/editor.planx.uk/src/@planx/components/FileUploadAndLabel/Modal.tsx +++ b/editor.planx.uk/src/@planx/components/FileUploadAndLabel/Modal.tsx @@ -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[]; @@ -83,7 +83,7 @@ export const FileTaggingModal = ({ key={slot.id} removeFile={() => removeFile(slot)} /> - , -)(({ 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) => ( - -); - /** * Function which returns the groups (ul elements) used by Autocomplete */ @@ -180,8 +79,6 @@ const renderOption: AutocompleteProps< ); -const PopupIcon = ; - /** * Custom Listbox component * Used to wrap options within the autocomplete and append a custom element above the option list @@ -199,7 +96,7 @@ const ListboxComponent = forwardRef( ), ); -export const SelectMultiple = (props: SelectMultipleProps) => { +export const SelectMultipleFileTypes = (props: SelectMultipleProps) => { const { uploadedFile, fileList, setFileList } = props; const initialTags = getTagsForSlot(uploadedFile.id, fileList); @@ -224,7 +121,7 @@ export const SelectMultiple = (props: SelectMultipleProps) => { const value: Option[] = useMemo( () => initialTags.flatMap((tag) => options.filter(({ name }) => name === tag)), - [initialTags], + [initialTags, options], ); /** @@ -266,46 +163,19 @@ export const SelectMultiple = (props: SelectMultipleProps) => { }; return ( - + 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" }} - > - 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, - }, - }, - }} - /> - + label="What does this file show? (select all that apply)" + ListboxComponent={ListboxComponent} + onChange={handleChange} + options={options} + renderGroup={renderGroup} + renderOption={renderOption} + value={value} + /> ); }; diff --git a/editor.planx.uk/src/ui/shared/SelectMultiple.tsx b/editor.planx.uk/src/ui/shared/SelectMultiple.tsx new file mode 100644 index 0000000000..8d3624b86e --- /dev/null +++ b/editor.planx.uk/src/ui/shared/SelectMultiple.tsx @@ -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 = ( + ({ color: theme.palette.primary.main })} + fontSize="large" + /> +); + +type RequiredAutocompleteProps = Pick< + AutocompleteProps, + "options" | "onChange" +>; + +type OptionalAutocompleteProps = Partial< + Omit, "multiple"> +>; + +type Props = { + label: string; +} & RequiredAutocompleteProps & + OptionalAutocompleteProps; + +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(props: Props) { + return ( + + + role="status" + aria-atomic={true} + aria-live="polite" + disableClearable + disableCloseOnSelect + multiple + popupIcon={PopupIcon} + renderInput={(params) => ( + + )} + ChipProps={{ + variant: "uploadedFileTag", + size: "small", + sx: { pointerEvents: "none" }, + onDelete: undefined, + }} + componentsProps={{ + popupIndicator: { + disableRipple: true, + }, + popper: { + sx: { + boxShadow: 10, + }, + }, + }} + {...props} + /> + + ); +}