Skip to content

Commit 0467ebf

Browse files
Merge pull request #3747 from verifywise-ai/refactor/autocomplete-field-validation-styling
Refactor/autocomplete field validation styling
2 parents 552bf49 + e20a888 commit 0467ebf

File tree

2 files changed

+135
-176
lines changed

2 files changed

+135
-176
lines changed

Clients/src/presentation/components/Inputs/Autocomplete/index.tsx

Lines changed: 125 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -1,205 +1,151 @@
1-
import { Autocomplete, TextField, Typography, useTheme, Stack } from "@mui/material";
1+
import {
2+
Autocomplete,
3+
AutocompleteProps,
4+
Stack,
5+
SxProps,
6+
TextField,
7+
Theme,
8+
Typography,
9+
useTheme,
10+
} from "@mui/material";
211
import "./index.css";
3-
import { AutoCompleteFieldProps } from "../../../types/widget.types";
4-
import { AutoCompleteOption } from "../../../../domain/interfaces/i.widget";
512
import { getAutocompleteStyles } from "../../../utils/inputStyles";
613

7-
function AutoCompleteField({
8-
id,
9-
type,
10-
options = [],
11-
placeholder = "Type to search",
12-
disabled,
13-
sx,
14-
width,
15-
autoCompleteValue,
16-
setAutoCompleteValue,
17-
error,
18-
multiple = false,
19-
value,
20-
onChange,
14+
interface AutoCompleteFieldProps<
15+
T,
16+
Multiple extends boolean | undefined = undefined,
17+
DisableClearable extends boolean | undefined = undefined,
18+
FreeSolo extends boolean | undefined = undefined
19+
> extends Omit<AutocompleteProps<T, Multiple, DisableClearable, FreeSolo>, "renderInput" | "sx"> {
20+
label?: string;
21+
placeholder?: string;
22+
error?: string;
23+
isRequired?: boolean;
24+
isOptional?: boolean;
25+
optionalLabel?: string;
26+
sx?: SxProps<Theme>;
27+
}
28+
29+
function AutoCompleteField<
30+
T,
31+
Multiple extends boolean | undefined = undefined,
32+
DisableClearable extends boolean | undefined = undefined,
33+
FreeSolo extends boolean | undefined = undefined
34+
>({
2135
label,
22-
isRequired = false,
23-
}: AutoCompleteFieldProps) {
36+
placeholder,
37+
error,
38+
isRequired,
39+
isOptional,
40+
optionalLabel,
41+
sx,
42+
disabled,
43+
...autocompleteProps
44+
}: AutoCompleteFieldProps<T, Multiple, DisableClearable, FreeSolo>) {
2445
const theme = useTheme();
2546

26-
// For multiple selection with string options
27-
if (multiple && options.length > 0 && typeof options[0] === 'string') {
28-
const stringOptions = options as string[];
29-
const stringValue = (value || []) as string[];
47+
// Extract layout props from sx to apply to wrapper Stack
48+
const extractedLayoutProps = (() => {
49+
if (!sx || typeof sx !== "object" || Array.isArray(sx)) return {};
50+
const s = sx as Record<string, unknown>;
51+
return {
52+
width: s.width as string | number | undefined,
53+
flexGrow: s.flexGrow as number | undefined,
54+
minWidth: s.minWidth as string | number | undefined,
55+
maxWidth: s.maxWidth as string | number | undefined,
56+
};
57+
})();
3058

31-
return (
32-
<Stack gap={theme.spacing(2)} sx={sx}>
33-
{label && (
34-
<Typography
35-
component="p"
36-
variant="body1"
37-
color={theme.palette.text.secondary}
38-
fontWeight={500}
39-
fontSize={"13px"}
40-
sx={{
41-
margin: 0,
42-
height: '22px',
43-
display: "flex",
44-
alignItems: "center",
45-
}}
46-
>
47-
{label}
48-
{isRequired && (
49-
<Typography
50-
component="span"
51-
ml={theme.spacing(1)}
52-
color={theme.palette.error.text}
53-
>
54-
*
55-
</Typography>
56-
)}
57-
</Typography>
58-
)}
59-
<Autocomplete
60-
multiple
61-
id={id}
62-
options={stringOptions}
63-
value={stringValue}
64-
onChange={(_event, newValue) => {
65-
onChange?.(newValue);
66-
}}
67-
disabled={disabled}
68-
renderInput={(params) => (
69-
<TextField
70-
{...params}
71-
size="small"
72-
placeholder={placeholder}
73-
error={!!error}
74-
sx={{
75-
"& .MuiOutlinedInput-root": {
76-
minHeight: "34px",
77-
paddingTop: "2px !important",
78-
paddingBottom: "2px !important",
79-
},
80-
"& ::placeholder": {
81-
fontSize: "13px",
82-
},
83-
}}
84-
/>
85-
)}
86-
sx={{
87-
...getAutocompleteStyles(theme, { hasError: !!error }),
88-
width: "100%",
89-
backgroundColor: theme.palette.background.main,
90-
"& .MuiOutlinedInput-root": {
91-
borderRadius: theme.shape.borderRadius,
92-
},
93-
"& .MuiChip-root": {
94-
borderRadius: theme.shape.borderRadius,
95-
height: "22px",
96-
margin: "1px 2px",
97-
fontSize: "13px",
98-
},
99-
}}
100-
slotProps={{
101-
popper: {
102-
sx: {
103-
"& ul": { p: 0 },
104-
"& li": {
105-
fontSize: 13,
106-
borderRadius: theme.shape.borderRadius,
107-
transition: "color 0.2s ease, background-color 0.2s ease",
108-
"&:hover": {
109-
color: theme.palette.primary.main,
110-
backgroundColor: theme.palette.background.accent,
111-
},
112-
},
113-
},
114-
},
115-
paper: {
116-
sx: {
117-
p: 2,
118-
fontSize: 13,
119-
borderRadius: theme.shape.borderRadius,
120-
boxShadow: theme.boxShadow,
121-
},
122-
},
123-
}}
124-
/>
125-
{error && (
126-
<Typography
127-
className="input-error"
128-
color={theme.palette.status.error.text}
129-
sx={{
130-
opacity: 0.8,
131-
fontSize: 11,
132-
}}
133-
>
134-
{error}
135-
</Typography>
136-
)}
137-
</Stack>
59+
// Pass remaining sx props to the Autocomplete (excluding layout props already on wrapper)
60+
const sxWithoutLayoutProps = (() => {
61+
if (!sx || typeof sx !== "object" || Array.isArray(sx)) return sx;
62+
const s = sx as Record<string, unknown>;
63+
return Object.fromEntries(
64+
Object.entries(s).filter(
65+
([key]) => !["width", "flexGrow", "minWidth", "maxWidth"].includes(key)
66+
)
13867
);
139-
}
68+
})();
14069

141-
// Original single selection with object options
14270
return (
143-
<>
144-
<Autocomplete
145-
sx={{
146-
...getAutocompleteStyles(theme, { hasError: !!error }),
147-
cursor: 'pointer',
148-
...sx,
149-
}}
150-
className="auto-complete-field"
151-
id={id}
152-
value={autoCompleteValue}
153-
onChange={(_, newValue) => {
154-
setAutoCompleteValue?.(newValue);
155-
}}
156-
options={options as AutoCompleteOption[]}
157-
getOptionLabel={(option) => (option && option.name ? option.name : "")}
158-
disableClearable
71+
<Stack gap={theme.spacing(2)} sx={extractedLayoutProps}>
72+
{label && (
73+
<Typography
74+
component="p"
75+
variant="body1"
76+
color={theme.palette.text.secondary}
77+
fontWeight={500}
78+
fontSize={"13px"}
79+
sx={{ margin: 0, height: "22px" }}
80+
>
81+
{label}
82+
{isRequired && (
83+
<Typography
84+
component="span"
85+
ml={theme.spacing(1)}
86+
color={theme.palette.error.text}
87+
>
88+
*
89+
</Typography>
90+
)}
91+
{isOptional && (
92+
<Typography
93+
component="span"
94+
fontSize="inherit"
95+
fontWeight={400}
96+
ml={theme.spacing(2)}
97+
sx={{ opacity: 0.6 }}
98+
>
99+
{optionalLabel || "(optional)"}
100+
</Typography>
101+
)}
102+
</Typography>
103+
)}
104+
<Autocomplete<T, Multiple, DisableClearable, FreeSolo>
159105
disabled={disabled}
160-
isOptionEqualToValue={(option, value) => option._id === value._id}
161106
renderInput={(params) => (
162107
<TextField
163-
error={!!error}
164108
{...params}
165-
type={type}
109+
size="small"
166110
placeholder={placeholder}
167-
InputProps={{
168-
...params.InputProps,
169-
readOnly: true,
170-
sx: {
171-
width: width,
172-
height: 34,
173-
fontSize: 13,
174-
p: 0,
175-
borderRadius: theme.shape.borderRadius,
176-
"& input": {
177-
p: 0,
178-
},
179-
"&.Mui-disabled input": {
180-
cursor: "default",
181-
},
111+
sx={{
112+
"& .MuiOutlinedInput-root": {
113+
minHeight: "34px",
114+
paddingTop: "2px !important",
115+
paddingBottom: "2px !important",
116+
},
117+
"& ::placeholder": {
118+
fontSize: "13px",
182119
},
183120
}}
184121
/>
185122
)}
186-
renderOption={(props, option) => {
187-
const { key: _key, ...optionProps } = props;
188-
return (
189-
<li key={option._id} {...optionProps}>
190-
<div>{<span>{option.name}</span>}</div>
191-
</li>
192-
);
123+
sx={{
124+
...getAutocompleteStyles(theme, { hasError: !!error }),
125+
backgroundColor: theme.palette.background.main,
126+
"& .MuiOutlinedInput-root": {
127+
...getAutocompleteStyles(theme, { hasError: !!error })["& .MuiOutlinedInput-root"],
128+
borderRadius: theme.shape.borderRadius,
129+
},
130+
"& .MuiChip-root": {
131+
borderRadius: theme.shape.borderRadius,
132+
height: "22px",
133+
margin: "1px 2px",
134+
fontSize: "13px",
135+
},
136+
...sxWithoutLayoutProps,
193137
}}
194138
slotProps={{
195139
popper: {
196140
sx: {
197141
"& ul": { p: 0 },
198142
"& li": {
143+
fontSize: 13,
199144
borderRadius: theme.shape.borderRadius,
200145
transition: "color 0.2s ease, background-color 0.2s ease",
201146
"&:hover": {
202147
color: theme.palette.primary.main,
148+
backgroundColor: theme.palette.background.accent,
203149
},
204150
},
205151
},
@@ -208,25 +154,28 @@ function AutoCompleteField({
208154
sx: {
209155
p: 2,
210156
fontSize: 13,
157+
borderRadius: theme.shape.borderRadius,
158+
boxShadow: theme.boxShadow,
211159
},
212160
},
213161
}}
162+
{...autocompleteProps}
214163
/>
215164
{error && (
216165
<Typography
217166
component="span"
218167
className="input-error"
219-
color={theme.palette.error.main}
168+
color={theme.palette.status.error.text}
220169
mt={theme.spacing(2)}
221170
sx={{
222171
opacity: 0.8,
223-
fontSize: 13,
172+
fontSize: 11,
224173
}}
225174
>
226175
{error}
227176
</Typography>
228177
)}
229-
</>
178+
</Stack>
230179
);
231180
}
232181

Clients/src/presentation/utils/inputStyles.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,16 @@ export const getAutocompleteStyles = (theme: Theme, options: InputStylesOptions
237237
},
238238
},
239239
},
240+
241+
...(hasError && {
242+
'& .MuiOutlinedInput-root fieldset': {
243+
borderColor: `${errorBorder} !important`,
244+
},
245+
'& .MuiOutlinedInput-root.Mui-focused fieldset': {
246+
borderWidth: '2px',
247+
boxShadow: `0 0 0 3px ${errorBorder}1A`,
248+
},
249+
}),
240250
};
241251
};
242252

0 commit comments

Comments
 (0)