| /* |
| * Copyright (C) 2022 Savoir-faire Linux Inc. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU Affero General Public License as |
| * published by the Free Software Foundation; either version 3 of the |
| * License, or (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU Affero General Public License for more details. |
| * |
| * You should have received a copy of the GNU Affero General Public |
| * 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 { ArrowHeadDown, ArrowHeadUp } from './SvgIcon'; |
| |
| export interface SelectOption<Payload> { |
| label: string; |
| payload: Payload; |
| } |
| |
| const width = '250px'; |
| |
| 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 ( |
| <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> |
| ); |
| }; |