From 4bff2f515fb76ebc728df051606012439c6ca2ff Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Tue, 25 Jun 2024 13:26:29 -0400 Subject: [PATCH] Update Select and Dropdown components (#1160) * Update select component to match designs * update menuitem to match designs * fix FormFieldWrapper margin * use new menuitem component * fix icon positioning * simplify simpleselect * do not export Select from ol-components * add a test, reorganize stories * tweak mdx * use simpleselect in resource drawer * 4px menu border * 4px autocomplete border --- .../ManageListDialogs/ManageListDialogs.tsx | 4 + .../Profile/EducationLevelChoice.tsx | 40 +++--- .../Profile/LearningFormatChoice.tsx | 38 +++--- .../Profile/TimeCommitmentChoice.tsx | 36 ++--- .../SearchDisplay/SearchDisplay.tsx | 45 ++----- .../FieldPage/FieldSearchFacetDisplay.tsx | 21 ++- .../components/FormHelpers/FormHelpers.tsx | 8 +- .../src/components/Input/Input.tsx | 58 ++++---- .../LearningResourceExpanded.tsx | 27 ++-- .../src/components/MenuItem/MenuItem.tsx | 60 +++++++++ .../components/SelectField/SelectField.mdx | 4 +- .../SelectField/SelectField.stories.tsx | 122 ++++++++--------- .../components/SelectField/SelectField.tsx | 111 +++++++++++++-- .../SimpleMenu/SimpleMenu.stories.tsx | 2 +- .../components/SimpleMenu/SimpleMenu.test.tsx | 19 ++- .../src/components/SimpleMenu/SimpleMenu.tsx | 2 +- .../SimpleSelect/SimpleSelect.stories.tsx | 36 ++--- .../SimpleSelect/SimpleSelect.test.tsx | 81 +++++++++++ .../components/SimpleSelect/SimpleSelect.tsx | 127 +++++++++--------- .../ThemeProvider/ThemeProvider.tsx | 6 + frontends/ol-components/src/index.ts | 12 +- 21 files changed, 554 insertions(+), 305 deletions(-) create mode 100644 frontends/ol-components/src/components/MenuItem/MenuItem.tsx create mode 100644 frontends/ol-components/src/components/SimpleSelect/SimpleSelect.test.tsx diff --git a/frontends/mit-open/src/page-components/ManageListDialogs/ManageListDialogs.tsx b/frontends/mit-open/src/page-components/ManageListDialogs/ManageListDialogs.tsx index a66f5f9e10..1d419dad21 100644 --- a/frontends/mit-open/src/page-components/ManageListDialogs/ManageListDialogs.tsx +++ b/frontends/mit-open/src/page-components/ManageListDialogs/ManageListDialogs.tsx @@ -10,6 +10,7 @@ import { BasicDialog, styled, RadioChoiceField, + MenuItem, } from "ol-components" import * as Yup from "yup" import { PrivacyLevelEnum, type LearningPathResource, UserList } from "api" @@ -222,6 +223,9 @@ const UpsertLearningPathDialog = NiceModal.create( } /> )} + renderOption={(props, opt) => { + return {opt.name} + }} /> Please select, + disabled: true, + value: "", + }, + ...Object.values(CurrentEducationEnum).map((value) => ({ + value, + label: CurrentEducationEnumDescriptions[value], + })), +] + const EducationLevelSelect: React.FC< ProfileFieldUpdateProps<"current_education"> > = ({ label, value, onUpdate }) => { @@ -18,7 +25,7 @@ const EducationLevelSelect: React.FC< CurrentEducationEnum | "" >(value || "") - const handleChange = (event: SelectChangeEvent) => { + const handleChange: SimpleSelectFieldProps["onChange"] = (event) => { setEducationLevel(event.target.value as CurrentEducationEnum) } @@ -29,18 +36,11 @@ const EducationLevelSelect: React.FC< return ( {label} - + ) } diff --git a/frontends/mit-open/src/page-components/Profile/LearningFormatChoice.tsx b/frontends/mit-open/src/page-components/Profile/LearningFormatChoice.tsx index 15bfa49134..de481d1c5d 100644 --- a/frontends/mit-open/src/page-components/Profile/LearningFormatChoice.tsx +++ b/frontends/mit-open/src/page-components/Profile/LearningFormatChoice.tsx @@ -1,12 +1,11 @@ import React from "react" import { - RadioChoiceBoxField, - Select, - SelectChangeEvent, - MenuItem, FormControl, FormLabel, + SimpleSelect, + RadioChoiceBoxField, } from "ol-components" +import type { SimpleSelectFieldProps, SimpleSelectOption } from "ol-components" import { LearningFormatEnum, LearningFormatEnumDescriptions } from "api/v0" import { ProfileFieldUpdateProps, ProfileFieldStateHook } from "./types" @@ -20,6 +19,15 @@ const CHOICES = [ label: LearningFormatEnumDescriptions[value], })) +const SELECT_OPTIONS: SimpleSelectOption[] = [ + { + label: Please select, + disabled: true, + value: "", + }, + ...CHOICES, +] + type Props = ProfileFieldUpdateProps<"learning_format"> type State = LearningFormatEnum | "" @@ -66,11 +74,8 @@ const LearningFormatChoiceBoxField: React.FC = ({ const LearningFormatSelect: React.FC = ({ label, value, onUpdate }) => { const [learningFormat, setLearningFormat] = React.useState(value || "") - const handleChange = (event: SelectChangeEvent) => { - setLearningFormat(() => { - const target = event.target as HTMLInputElement - return target.value as LearningFormatEnum - }) + const handleChange: SimpleSelectFieldProps["onChange"] = (event) => { + setLearningFormat(event.target.value as LearningFormatEnum) } React.useEffect(() => { onUpdate("learning_format", learningFormat) @@ -79,16 +84,11 @@ const LearningFormatSelect: React.FC = ({ label, value, onUpdate }) => { return ( {label} - + ) } diff --git a/frontends/mit-open/src/page-components/Profile/TimeCommitmentChoice.tsx b/frontends/mit-open/src/page-components/Profile/TimeCommitmentChoice.tsx index 26248d1bc7..5b25afc9db 100644 --- a/frontends/mit-open/src/page-components/Profile/TimeCommitmentChoice.tsx +++ b/frontends/mit-open/src/page-components/Profile/TimeCommitmentChoice.tsx @@ -2,11 +2,10 @@ import React from "react" import { FormControl, FormLabel, - Select, - SelectChangeEvent, - MenuItem, + SimpleSelect, RadioChoiceBoxField, } from "ol-components" +import type { SimpleSelectFieldProps, SimpleSelectOption } from "ol-components" import { TimeCommitmentEnum, TimeCommitmentEnumDescriptions } from "api/v0" import { ProfileFieldUpdateProps } from "./types" @@ -22,6 +21,15 @@ const CHOICES = [ label: TimeCommitmentEnumDescriptions[value], })) +const SELECT_OPTIONS: SimpleSelectOption[] = [ + { + label: Please select, + disabled: true, + value: "", + }, + ...CHOICES, +] + type Props = ProfileFieldUpdateProps<"time_commitment"> type State = TimeCommitmentEnum | "" @@ -57,11 +65,8 @@ const TimeCommitmentRadioChoiceBoxField: React.FC = ({ const TimeCommitmentSelect: React.FC = ({ label, value, onUpdate }) => { const [timeCommitment, setTimeCommitment] = React.useState(value || "") - const handleChange = (event: SelectChangeEvent) => { - setTimeCommitment(() => { - const target = event.target as HTMLInputElement - return target.value as TimeCommitmentEnum - }) + const handleChange: SimpleSelectFieldProps["onChange"] = (event) => { + setTimeCommitment(event.target.value as TimeCommitmentEnum) } React.useEffect(() => { onUpdate("time_commitment", timeCommitment) @@ -70,16 +75,11 @@ const TimeCommitmentSelect: React.FC = ({ label, value, onUpdate }) => { return ( {label} - + ) } diff --git a/frontends/mit-open/src/page-components/SearchDisplay/SearchDisplay.tsx b/frontends/mit-open/src/page-components/SearchDisplay/SearchDisplay.tsx index 85850e2387..6068321bb9 100644 --- a/frontends/mit-open/src/page-components/SearchDisplay/SearchDisplay.tsx +++ b/frontends/mit-open/src/page-components/SearchDisplay/SearchDisplay.tsx @@ -46,24 +46,8 @@ import type { TabConfig } from "./ResourceTypeTabs" import { ResourceListCard } from "../ResourceCard/ResourceCard" import { useSearchParams } from "@mitodl/course-search-utils/react-router" -export const StyledDropdown = styled(SimpleSelect)` - margin-left: 8px; - margin-right: 0; - margin-top: 0; +export const StyledSelect = styled(SimpleSelect)` min-width: 160px; - height: 32px; - background: ${({ theme }) => theme.custom.colors.white}; - - svg { - width: 0.75em; - height: 0.75em; - } - - div { - min-height: 0 px; - padding-right: 1px !important; - font-size: 12px !important; - } ` export const StyledResourceTabs = styled(ResourceTypeTabs.TabList)` @@ -73,11 +57,6 @@ export const StyledResourceTabs = styled(ResourceTypeTabs.TabList)` export const DesktopSortContainer = styled.div` float: right; - div { - height: 32px; - bottom: 1px; - } - ${({ theme }) => theme.breakpoints.down("md")} { display: none; } @@ -87,11 +66,6 @@ export const MobileSortContainer = styled.div` ${({ theme }) => theme.breakpoints.up("md")} { display: none; } - - div { - height: 32px; - bottom: -2px; - } ` export const FacetStyles = styled.div` @@ -484,19 +458,19 @@ export const ALL_RESOURCE_TABS = TABS.map((t) => t.resource_type) export const SORT_OPTIONS = [ { label: "Best Match", - key: "", + value: "", }, { label: "New", - key: "new", + value: "new", }, { label: "Popular", - key: "-views", + value: "-views", }, { label: "Upcoming", - key: "upcoming", + value: "upcoming", }, ] @@ -597,15 +571,14 @@ const SearchDisplay: React.FC = ({ ) const sortDropdown = ( - setParamValue("sortby", e.target.value)} options={SORT_OPTIONS} className="sort-dropdown" - sx={{ fontSize: "small" }} renderValue={(value) => { - const opt = SORT_OPTIONS.find((option) => option.key === value) + const opt = SORT_OPTIONS.find((option) => option.value === value) return `Sort by: ${opt?.label}` }} /> diff --git a/frontends/mit-open/src/pages/FieldPage/FieldSearchFacetDisplay.tsx b/frontends/mit-open/src/pages/FieldPage/FieldSearchFacetDisplay.tsx index ca9fd29920..2da87b65d5 100644 --- a/frontends/mit-open/src/pages/FieldPage/FieldSearchFacetDisplay.tsx +++ b/frontends/mit-open/src/pages/FieldPage/FieldSearchFacetDisplay.tsx @@ -9,9 +9,8 @@ import type { } from "@mitodl/course-search-utils" import { BOOLEAN_FACET_NAMES } from "@mitodl/course-search-utils" import { Skeleton, styled } from "ol-components" -import { StyledDropdown } from "@/page-components/SearchDisplay/SearchDisplay" - -export type KeyWithLabel = { key: string; label: string } +import type { SimpleSelectOption } from "ol-components" +import { StyledSelect } from "@/page-components/SearchDisplay/SearchDisplay" const StyledSkeleton = styled(Skeleton)` display: inline-flex; @@ -51,19 +50,19 @@ const filteredResultsWithLabels = ( results: Aggregation, labelFunction: ((value: string) => string) | null | undefined, constantsForFacet: string[] | null, -): KeyWithLabel[] => { - const newResults = [] as KeyWithLabel[] +): SimpleSelectOption[] => { + const newResults = [] as SimpleSelectOption[] if (constantsForFacet) { constantsForFacet.map((key: string) => { newResults.push({ - key: key, + value: key, label: labelFunction ? labelFunction(key) : key, }) }) } else { results.map((singleFacet: Bucket) => { newResults.push({ - key: singleFacet.key, + value: singleFacet.key, label: labelFunction ? labelFunction(singleFacet.key) : singleFacet.key, }) }) @@ -113,15 +112,15 @@ const AvailableFacetsDropdowns: React.FC< } if (!isMultiple) { - facetItems.unshift({ key: "", label: "no selection" }) + facetItems.unshift({ value: "", label: "no selection" }) } return ( facetItems.length && ( - onFacetChange(facetSetting.name, e.target.value)} renderValue={() => { return facetSetting.title diff --git a/frontends/ol-components/src/components/FormHelpers/FormHelpers.tsx b/frontends/ol-components/src/components/FormHelpers/FormHelpers.tsx index 447443cbf3..0e1e31c06f 100644 --- a/frontends/ol-components/src/components/FormHelpers/FormHelpers.tsx +++ b/frontends/ol-components/src/components/FormHelpers/FormHelpers.tsx @@ -50,7 +50,13 @@ const ControlLabel: React.FC = ({ id, }) => { return ( - + {label} {required ? : null} diff --git a/frontends/ol-components/src/components/Input/Input.tsx b/frontends/ol-components/src/components/Input/Input.tsx index a60224f837..61f30fa706 100644 --- a/frontends/ol-components/src/components/Input/Input.tsx +++ b/frontends/ol-components/src/components/Input/Input.tsx @@ -3,6 +3,7 @@ import styled from "@emotion/styled" import { pxToRem } from "../ThemeProvider/typography" import InputBase from "@mui/material/InputBase" import type { InputBaseProps } from "@mui/material/InputBase" +import type { Theme } from "@mui/material/styles" const defaultProps = { size: "medium", @@ -15,6 +16,35 @@ const buttonPadding = { heroMobile: 4, } +/** + * Base styles for Input and Select components. Includes border, color, hover effects. + */ +const baseInputStyles = (theme: Theme) => ({ + backgroundColor: "white", + color: theme.custom.colors.silverGrayDark, + borderColor: theme.custom.colors.silverGrayLight, + borderWidth: "1px", + borderStyle: "solid", + "&:hover:not(.Mui-disabled)": { + borderColor: theme.custom.colors.darkGray2, + }, + "&.Mui-focused": { + borderWidth: "2px", + color: theme.custom.colors.darkGray2, + borderColor: "currentcolor", + }, + "&.Mui-error": { + borderColor: theme.custom.colors.red, + }, + "& input::placeholder": { + opacity: "0.3", + color: theme.custom.colors.black, + }, + "& input:placeholder-shown": { + textOverflow: "ellipsis", + }, +}) + /** * A styled input that supports start and end adornments. In most cases, the * higher-level TextField component should be used instead of this component. @@ -25,31 +55,7 @@ const Input = styled(InputBase)(({ multiline, }) => { return [ - { - backgroundColor: "white", - color: theme.custom.colors.silverGrayDark, - borderColor: theme.custom.colors.silverGrayLight, - borderWidth: "1px", - borderStyle: "solid", - "&:hover:not(.Mui-disabled)": { - borderColor: theme.custom.colors.darkGray2, - }, - "&.Mui-focused": { - borderWidth: "2px", - color: theme.custom.colors.darkGray2, - borderColor: "currentcolor", - }, - "&.Mui-error": { - borderColor: theme.custom.colors.red, - }, - "& input::placeholder": { - opacity: "0.3", - color: theme.custom.colors.black, - }, - "& input:placeholder-shown": { - textOverflow: "ellipsis", - }, - }, + baseInputStyles(theme), size === "medium" && { "& .MuiInputBase-input": { ...theme.typography.body2, @@ -207,5 +213,5 @@ const AdornmentButton: React.FC = (props) => { type InputProps = Omit -export { AdornmentButton, Input } +export { AdornmentButton, Input, baseInputStyles } export type { InputProps, AdornmentButtonProps } diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.tsx index e078891719..c529f5d7e8 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.tsx @@ -3,7 +3,6 @@ import styled from "@emotion/styled" import Skeleton from "@mui/material/Skeleton" import Typography from "@mui/material/Typography" import { ButtonLink } from "../Button/Button" -import MenuItem from "@mui/material/MenuItem" import Chip from "@mui/material/Chip" import type { LearningResource, LearningResourceTopic } from "api" import { ResourceTypeEnum, PlatformEnum } from "api" @@ -13,9 +12,9 @@ import { getReadableResourceType, } from "ol-utilities" import type { EmbedlyConfig } from "ol-utilities" -import type { SelectChangeEvent } from "@mui/material/Select" import { theme } from "../ThemeProvider/ThemeProvider" -import { SelectField } from "../SelectField/SelectField" +import { SimpleSelect } from "../SimpleSelect/SimpleSelect" +import type { SimpleSelectProps } from "../SimpleSelect/SimpleSelect" import { EmbedlyCard } from "../EmbedlyCard/EmbedlyCard" import { PlatformLogo, PLATFORMS } from "../Logo/Logo" import { ChipLink } from "../Chips/ChipLink" @@ -319,7 +318,7 @@ const LearningResourceExpanded: React.FC = ({ } }, [resource]) - const onDateChange = (event: SelectChangeEvent) => { + const onDateChange: SimpleSelectProps["onChange"] = (event) => { const run = resource?.runs?.find( (run) => run.id === Number(event.target.value), ) @@ -333,6 +332,11 @@ const LearningResourceExpanded: React.FC = ({ if (!resource) { return } + const dateOptions: SimpleSelectProps["options"] = + resource.runs?.map((run) => ({ + value: run.id.toString(), + label: formatDate(run.start_date!, "MMMM DD, YYYY"), + })) ?? [] if ( [ResourceTypeEnum.Course, ResourceTypeEnum.Program].includes( @@ -343,18 +347,11 @@ const LearningResourceExpanded: React.FC = ({ return ( Start Date: - - {resource.runs?.map((run) => ( - - {formatDate(run.start_date!, "MMMM DD, YYYY")} - - ))} - + options={dateOptions} + /> ) } diff --git a/frontends/ol-components/src/components/MenuItem/MenuItem.tsx b/frontends/ol-components/src/components/MenuItem/MenuItem.tsx new file mode 100644 index 0000000000..41a3db4ee9 --- /dev/null +++ b/frontends/ol-components/src/components/MenuItem/MenuItem.tsx @@ -0,0 +1,60 @@ +import MuiMenuItem from "@mui/material/MenuItem" +import type { MenuItemProps as MuiMenuItemProps } from "@mui/material/MenuItem" +import styled from "@emotion/styled" + +type MenuItemProps = MuiMenuItemProps & { + size?: "small" | "medium" | "large" +} + +const DEFAULT_SIZE = "medium" + +const MenuItem = styled(MuiMenuItem)( + ({ theme, size = DEFAULT_SIZE }) => [ + { + padding: "8px 12px", + color: theme.custom.colors.darkGray2, + backgroundColor: theme.custom.colors.white, + "&:hover": { + backgroundColor: theme.custom.colors.lightGray1, + }, + "&.Mui-disabled": { + opacity: 1, + color: theme.custom.colors.silverGrayDark, + backgroundColor: theme.custom.colors.white, + }, + '&.Mui-selected:not(.Mui-disabled), .MuiAutocomplete-listbox &.MuiAutocomplete-option[aria-selected="true"].Mui-focused, .MuiAutocomplete-listbox &.MuiAutocomplete-option[aria-selected="true"]': + { + backgroundColor: theme.custom.colors.red, + color: theme.custom.colors.white, + "&:hover": { + backgroundColor: theme.custom.colors.mitRed, + }, + }, + ".MuiAutocomplete-listbox &.MuiAutocomplete-option.Mui-focusVisible": { + /** + * For autocomplete fields in particular, the input field maintains + * focus while the menu is open, so browser does not provide its default + * focus styling. MUI does provide its own styling for focusVisible in + * this case, but we tend to use the outline generally. + */ + outline: [ + "2px auto rgb(0, 95, 204)", // fallback + "2px auto Highlight", // firefox + "2px auto -webkit-focus-ring-color", // webkit, + ], + }, + }, + size === "small" && { + ...theme.typography.body3, + }, + size === "medium" && { + ...theme.typography.body2, + }, + size === "large" && { + ...theme.typography.body1, + }, + ], +) + +export { MenuItem } +export type { MenuItemProps } diff --git a/frontends/ol-components/src/components/SelectField/SelectField.mdx b/frontends/ol-components/src/components/SelectField/SelectField.mdx index 51234b07ab..a096af2a3c 100644 --- a/frontends/ol-components/src/components/SelectField/SelectField.mdx +++ b/frontends/ol-components/src/components/SelectField/SelectField.mdx @@ -6,9 +6,11 @@ import * as SelectField from "./SelectField.stories"; # SelectField +**NOTE: Prefer using `SimpleSelectField`, wherein options are specified by an array of objects rather than using `MenuItem` Children.** + `SelectField` includes an input, label, help text, and error message. Internally, it uses ` + {options.map(({ label, value, ...itemProps }) => ( + + {label} + + ))} + + ) +} + +type SimpleSelectFieldProps = Pick< + SelectFieldProps, + | "fullWidth" + | "label" + | "helpText" + | "errorText" + | "required" + | "size" + | "value" + | "onChange" + | "name" + | "className" +> & { + /** + * The options for the dropdown + */ + options: SimpleSelectOption[] +} + +/** + * A form field for text input via select dropdowns. Supports labels, help text, + * error text, and start/end adornments. + */ +const SimpleSelectField: React.FC = ({ options, - renderValue, - sx, + ...others }) => { return ( - + ) } -export { SimpleSelect } -export type { SimpleSelectProps, SimpleSelectOptionProps } +export { SimpleSelect, SimpleSelectField } +export type { SimpleSelectProps, SimpleSelectFieldProps, SimpleSelectOption } diff --git a/frontends/ol-components/src/components/ThemeProvider/ThemeProvider.tsx b/frontends/ol-components/src/components/ThemeProvider/ThemeProvider.tsx index b1a9f68151..b3814b2623 100644 --- a/frontends/ol-components/src/components/ThemeProvider/ThemeProvider.tsx +++ b/frontends/ol-components/src/components/ThemeProvider/ThemeProvider.tsx @@ -73,6 +73,12 @@ const themeOptions: ThemeOptions = { }, }, }, + MuiMenu: { + styleOverrides: { paper: { borderRadius: "4px" } }, + }, + MuiAutocomplete: { + styleOverrides: { paper: { borderRadius: "4px" } }, + }, MuiChip: chips.chipComponent, }, } diff --git a/frontends/ol-components/src/index.ts b/frontends/ol-components/src/index.ts index 9e74e84efe..f8107a07b7 100644 --- a/frontends/ol-components/src/index.ts +++ b/frontends/ol-components/src/index.ts @@ -145,7 +145,7 @@ export { default as Collapse } from "@mui/material/Collapse" export { default as Menu } from "@mui/material/Menu" export type { MenuProps } from "@mui/material/Menu" -export { default as MenuItem } from "@mui/material/MenuItem" +export * from "./components/MenuItem/MenuItem" export { default as Stepper } from "@mui/material/Stepper" export { default as Step } from "@mui/material/Step" @@ -186,14 +186,18 @@ export * from "./constants/imgConfigs" export { Input, AdornmentButton } from "./components/Input/Input" export type { InputProps, AdornmentButtonProps } from "./components/Input/Input" export { TextField } from "./components/TextField/TextField" -export { SimpleSelect } from "./components/SimpleSelect/SimpleSelect" +export { + SimpleSelect, + SimpleSelectField, +} from "./components/SimpleSelect/SimpleSelect" export type { SimpleSelectProps, - SimpleSelectOptionProps, + SimpleSelectFieldProps, + SimpleSelectOption, } from "./components/SimpleSelect/SimpleSelect" export type { TextFieldProps } from "./components/TextField/TextField" -export { Select, SelectField } from "./components/SelectField/SelectField" +export { SelectField } from "./components/SelectField/SelectField" export type { SelectChangeEvent, SelectProps,