blob: ca34d5337781cf3b32d447adfdc7168c5454cb86 [file] [log] [blame]
idillon3470d072022-11-22 15:22:34 -05001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
idillon4012a702022-11-24 13:45:43 -050018import { Box, InputBase, InputBaseProps, styled, useAutocomplete } from '@mui/material';
19import { MUIStyledCommonProps } from '@mui/system';
20import { useState } from 'react';
21import { useTranslation } from 'react-i18next';
idillon3470d072022-11-22 15:22:34 -050022
idillon4012a702022-11-24 13:45:43 -050023import { ArrowHeadDown, ArrowHeadUp } from './SvgIcon';
idillon3470d072022-11-22 15:22:34 -050024
idillon4012a702022-11-24 13:45:43 -050025export interface SelectOption<Payload> {
idillon3470d072022-11-22 15:22:34 -050026 label: string;
idillon4012a702022-11-24 13:45:43 -050027 payload: Payload;
idillon3470d072022-11-22 15:22:34 -050028}
29
idillon4012a702022-11-24 13:45:43 -050030const width = '250px';
idillon3470d072022-11-22 15:22:34 -050031
idillon4012a702022-11-24 13:45:43 -050032type InputProps = InputBaseProps & {
33 popupOpen: boolean; // Sometimes the popover is opened but the Input is not focused
34};
35
36const Input = styled(({ popupOpen, ...props }: InputProps) => <InputBase {...props} />)(({ theme, popupOpen }) => ({
37 width,
38 height: '46px',
39 borderRadius: '5px',
40 border: `1px solid ${popupOpen ? theme.palette.primary.dark : '#0056995C'}`,
41 boxSizing: 'border-box',
42 fontSize: '15px',
43 padding: '16px',
44 color: theme.palette.primary.dark,
45 fontWeight: 'medium',
46 '&.Mui-focused': {
47 // Sometimes the popover is closed but the Input is still focused
48 borderColor: theme.palette.primary.dark,
49 color: 'black',
50 },
51 '&::placeholder': {
52 color: '#666666',
53 },
54 '&.MuiInputBase-sizeSmall': {
55 height: '36px',
56 },
57}));
58
59const Listbox = styled('ul')(({ theme }) => ({
60 width,
61 maxHeight: '230px',
62 boxSizing: 'border-box',
63 margin: 0,
64 padding: 0,
65 paddingTop: '5px',
66 paddingBottom: '8.5px',
idillona3c2fad2022-12-18 23:49:10 -050067 zIndex: 100,
idillon4012a702022-11-24 13:45:43 -050068 position: 'absolute',
69 top: '-5px',
70 listStyle: 'none',
71 backgroundColor: 'white',
72 overflow: 'auto',
73 border: `1px solid ${theme.palette.primary.dark}`,
74 borderEndEndRadius: '5px',
75 borderEndStartRadius: '5px',
76 borderTop: 'none',
77}));
78
79const Option = styled('li')(({ theme }) => ({
80 paddingLeft: '16px',
81 paddingRight: '16px',
82 paddingTop: '7.5px',
83 paddingBottom: '7.5px',
84 fontSize: '15px',
85 '&.Mui-focused': {
86 backgroundColor: theme.palette.primary.light,
87 color: theme.palette.primary.dark,
88 cursor: 'pointer',
89 },
90}));
91
92const getArrowStyles = ({ theme }: MUIStyledCommonProps) => ({
93 height: '6px',
94 color: theme?.palette.primary.dark,
95 '&:hover': {
96 cursor: 'pointer',
97 },
98});
99const StyledArrowHeadUp = styled(ArrowHeadUp)(getArrowStyles);
100const StyledArrowHeadDown = styled(ArrowHeadDown)(getArrowStyles);
101
102export type CustomSelectProps<OptionPayload> = {
103 option?: SelectOption<OptionPayload>;
104 options: SelectOption<OptionPayload>[];
105 onChange: (newValue: SelectOption<OptionPayload> | null) => void;
106 size?: 'small' | 'medium';
107};
108
109export const CustomSelect = <OptionPayload,>({ option, options, onChange, size }: CustomSelectProps<OptionPayload>) => {
110 const { t } = useTranslation();
111
112 const [open, setOpen] = useState(false);
113
114 const { getRootProps, getInputProps, getListboxProps, getOptionProps, groupedOptions, popupOpen } = useAutocomplete<
115 SelectOption<OptionPayload>
116 >({
117 value: option,
118 options,
119 getOptionLabel: (option) => option.label,
120 onChange: (_, value) => onChange(value),
121 openOnFocus: true,
122 open,
123 onOpen: () => setOpen(true),
124 onClose: () => setOpen(false),
125 });
126
idillon3470d072022-11-22 15:22:34 -0500127 return (
idillon4012a702022-11-24 13:45:43 -0500128 <div>
129 <Box {...getRootProps()}>
130 <Input
131 inputProps={getInputProps()}
132 placeholder={t('select_placeholder')}
133 endAdornment={popupOpen ? <StyledArrowHeadUp /> : <StyledArrowHeadDown />}
134 size={size}
135 // onMouseDown fires before onBlur, contrary to onClick which fires after and causes glitches
136 onMouseDown={() => setOpen(!popupOpen)}
137 popupOpen={popupOpen}
138 />
139 </Box>
140 <Box position="relative">
141 {groupedOptions.length > 0 ? (
142 <Listbox {...getListboxProps()}>
143 {(groupedOptions as SelectOption<OptionPayload>[]).map((option, index) => (
144 <Option key={option.label} {...getOptionProps({ option, index })}>
145 {option.label}
146 </Option>
147 ))}
148 </Listbox>
149 ) : null}
150 </Box>
151 </div>
idillon3470d072022-11-22 15:22:34 -0500152 );
153};