Set styles for 'Select' component
Change-Id: I279d28e478a3d63b6d620f5362580854a0e62b3f
diff --git a/client/src/components/CustomSelect.tsx b/client/src/components/CustomSelect.tsx
index fc377db..c696eab 100644
--- a/client/src/components/CustomSelect.tsx
+++ b/client/src/components/CustomSelect.tsx
@@ -15,29 +15,139 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
+import { Box, InputBase, InputBaseProps, styled, useAutocomplete } from '@mui/material';
+import { MUIStyledCommonProps } from '@mui/system';
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
-import { MenuItem, MenuItemProps, Select, SelectProps } from '@mui/material';
+import { ArrowHeadDown, ArrowHeadUp } from './SvgIcon';
-export interface CustomSelectOption {
+export interface SelectOption<Payload> {
label: string;
- value: MenuItemProps['value'];
+ payload: Payload;
}
-export interface CustomSelectProps extends Omit<SelectProps, 'label'> {
- value: CustomSelectOption['value'];
- options: CustomSelectOption[];
- onChange: SelectProps['onChange'];
-}
+const width = '250px';
-const CustomSelect = ({ value, options, onChange }: CustomSelectProps) => {
+type InputProps = InputBaseProps & {
+ popupOpen: boolean; // Sometimes the popover is opened but the Input is not focused
+};
+
+const Input = styled(({ popupOpen, ...props }: InputProps) => <InputBase {...props} />)(({ theme, popupOpen }) => ({
+ width,
+ height: '46px',
+ borderRadius: '5px',
+ border: `1px solid ${popupOpen ? theme.palette.primary.dark : '#0056995C'}`,
+ boxSizing: 'border-box',
+ fontSize: '15px',
+ padding: '16px',
+ color: theme.palette.primary.dark,
+ fontWeight: 'medium',
+ '&.Mui-focused': {
+ // Sometimes the popover is closed but the Input is still focused
+ borderColor: theme.palette.primary.dark,
+ color: 'black',
+ },
+ '&::placeholder': {
+ color: '#666666',
+ },
+ '&.MuiInputBase-sizeSmall': {
+ height: '36px',
+ },
+}));
+
+const Listbox = styled('ul')(({ theme }) => ({
+ width,
+ maxHeight: '230px',
+ boxSizing: 'border-box',
+ margin: 0,
+ padding: 0,
+ paddingTop: '5px',
+ paddingBottom: '8.5px',
+ zIndex: 1,
+ position: 'absolute',
+ top: '-5px',
+ listStyle: 'none',
+ backgroundColor: 'white',
+ overflow: 'auto',
+ border: `1px solid ${theme.palette.primary.dark}`,
+ borderEndEndRadius: '5px',
+ borderEndStartRadius: '5px',
+ borderTop: 'none',
+}));
+
+const Option = styled('li')(({ theme }) => ({
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ paddingTop: '7.5px',
+ paddingBottom: '7.5px',
+ fontSize: '15px',
+ '&.Mui-focused': {
+ backgroundColor: theme.palette.primary.light,
+ color: theme.palette.primary.dark,
+ cursor: 'pointer',
+ },
+}));
+
+const getArrowStyles = ({ theme }: MUIStyledCommonProps) => ({
+ height: '6px',
+ color: theme?.palette.primary.dark,
+ '&:hover': {
+ cursor: 'pointer',
+ },
+});
+const StyledArrowHeadUp = styled(ArrowHeadUp)(getArrowStyles);
+const StyledArrowHeadDown = styled(ArrowHeadDown)(getArrowStyles);
+
+export type CustomSelectProps<OptionPayload> = {
+ option?: SelectOption<OptionPayload>;
+ options: SelectOption<OptionPayload>[];
+ onChange: (newValue: SelectOption<OptionPayload> | null) => void;
+ size?: 'small' | 'medium';
+};
+
+export const CustomSelect = <OptionPayload,>({ option, options, onChange, size }: CustomSelectProps<OptionPayload>) => {
+ const { t } = useTranslation();
+
+ const [open, setOpen] = useState(false);
+
+ const { getRootProps, getInputProps, getListboxProps, getOptionProps, groupedOptions, popupOpen } = useAutocomplete<
+ SelectOption<OptionPayload>
+ >({
+ value: option,
+ options,
+ getOptionLabel: (option) => option.label,
+ onChange: (_, value) => onChange(value),
+ openOnFocus: true,
+ open,
+ onOpen: () => setOpen(true),
+ onClose: () => setOpen(false),
+ });
+
return (
- <Select onChange={onChange} value={value}>
- {options.map((option) => (
- <MenuItem key={option.label} value={option.value}>
- {option.label}
- </MenuItem>
- ))}
- </Select>
+ <div>
+ <Box {...getRootProps()}>
+ <Input
+ inputProps={getInputProps()}
+ placeholder={t('select_placeholder')}
+ endAdornment={popupOpen ? <StyledArrowHeadUp /> : <StyledArrowHeadDown />}
+ size={size}
+ // onMouseDown fires before onBlur, contrary to onClick which fires after and causes glitches
+ onMouseDown={() => setOpen(!popupOpen)}
+ popupOpen={popupOpen}
+ />
+ </Box>
+ <Box position="relative">
+ {groupedOptions.length > 0 ? (
+ <Listbox {...getListboxProps()}>
+ {(groupedOptions as SelectOption<OptionPayload>[]).map((option, index) => (
+ <Option key={option.label} {...getOptionProps({ option, index })}>
+ {option.label}
+ </Option>
+ ))}
+ </Listbox>
+ ) : null}
+ </Box>
+ </div>
);
};
-export { CustomSelect };
diff --git a/client/src/components/Settings.tsx b/client/src/components/Settings.tsx
index 81333de..ff3ac90 100644
--- a/client/src/components/Settings.tsx
+++ b/client/src/components/Settings.tsx
@@ -55,12 +55,17 @@
);
};
-interface SettingSelectProps extends Setting, CustomSelectProps {}
+export interface SettingSelectProps<OptionPayload> extends Setting, CustomSelectProps<OptionPayload> {}
-export const SettingSelect = ({ options, value, onChange, ...settingBaseProps }: SettingSelectProps) => {
+export const SettingSelect = <OptionPayload,>({
+ options,
+ option,
+ onChange,
+ ...settingBaseProps
+}: SettingSelectProps<OptionPayload>) => {
return (
<SettingBase {...settingBaseProps}>
- <CustomSelect value={value} options={options} onChange={onChange} />
+ <CustomSelect<OptionPayload> option={option} options={options} onChange={onChange} size="small" />
</SettingBase>
);
};
diff --git a/client/src/components/SvgIcon.tsx b/client/src/components/SvgIcon.tsx
index bfce146..0a0e6bb 100644
--- a/client/src/components/SvgIcon.tsx
+++ b/client/src/components/SvgIcon.tsx
@@ -106,6 +106,23 @@
);
};
+export const ArrowHeadDown = (props: SvgIconProps) => {
+ return (
+ <SvgIcon {...props} viewBox="6 8 12 7.5">
+ <path d="M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z" />
+ </SvgIcon>
+ );
+};
+
+export const ArrowHeadUp = (props: SvgIconProps) => {
+ return (
+ <SvgIcon {...props} viewBox="6 8 12 7.5">
+ <path fill="none" d="M0 0h24v24H0z" />
+ <path d="m12 8-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z" />
+ </SvgIcon>
+ );
+};
+
export const AudioCallIcon = (props: SvgIconProps) => {
return (
<SvgIcon {...props} viewBox="0 0 15.338 16">