blob: 063b350e6259ac9a3166b4069a49240e3ac3896c [file] [log] [blame]
simon26e79f72022-10-05 22:16:08 -04001/*
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 */
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040018import { GppMaybe, Warning } from '@mui/icons-material';
19import {
20 IconButtonProps,
21 List,
22 ListItem,
23 ListItemIcon,
24 Stack,
25 TextField,
26 TextFieldProps,
27 Typography,
28} from '@mui/material';
simond47ef9e2022-09-28 22:24:28 -040029import { styled } from '@mui/material/styles';
simon35378692022-10-02 23:25:57 -040030import { ChangeEvent, ReactElement, useCallback, useEffect, useState } from 'react';
simon07b4eb02022-09-29 17:50:26 -040031
simon35378692022-10-02 23:25:57 -040032import { InfoButton, ToggleVisibilityButton } from './Button';
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040033import RulesDialog from './RulesDialog';
simon35378692022-10-02 23:25:57 -040034import { CheckedIcon, LockIcon, PenIcon, PersonIcon, RoundSaltireIcon } from './SvgIcon';
idillon-sfl37c18df2022-08-26 18:44:27 -040035
simond47ef9e2022-09-28 22:24:28 -040036const iconsHeight = '16px';
37const StyledCheckedIconSuccess = styled(CheckedIcon)(({ theme }) => ({
38 height: iconsHeight,
39 color: theme.palette.success.main,
40}));
41const StyledRoundSaltireIconError = styled(RoundSaltireIcon)(({ theme }) => ({
42 height: iconsHeight,
43 color: theme.palette.error.main,
44}));
45const StyledPenIconLight = styled(PenIcon)({ height: iconsHeight, color: '#03B9E9' });
46const StyledPenIconDark = styled(PenIcon)(({ theme }) => ({ height: iconsHeight, color: theme.palette.primary.dark }));
47const StyledPersonIconLight = styled(PersonIcon)({ height: iconsHeight, color: '#03B9E9' });
48const StyledLockIcon = styled(LockIcon)({ height: iconsHeight, color: '#03B9E9' });
idillon-sfl37c18df2022-08-26 18:44:27 -040049
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040050export type InputProps = TextFieldProps & {
simon35378692022-10-02 23:25:57 -040051 infoButtonProps?: IconButtonProps;
52 success?: boolean;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040053 tooltipTitle: string;
simon35378692022-10-02 23:25:57 -040054};
55
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040056export const UsernameInput = ({
57 infoButtonProps,
58 onChange: _onChange,
59 success,
60 tooltipTitle,
61 ...props
62}: InputProps) => {
simond47ef9e2022-09-28 22:24:28 -040063 const [isSelected, setIsSelected] = useState(false);
64 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -040065 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040066 const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
idillon-sfl37c18df2022-08-26 18:44:27 -040067
simond47ef9e2022-09-28 22:24:28 -040068 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -040069 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -040070 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -040071 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -040072 },
simon80b7b3b2022-09-28 17:50:10 -040073 [_onChange]
simond47ef9e2022-09-28 22:24:28 -040074 );
idillon-sfl37c18df2022-08-26 18:44:27 -040075
simond47ef9e2022-09-28 22:24:28 -040076 useEffect(() => {
77 /* Handle startAdornment */
78 let Icon = StyledPersonIconLight;
79 let visibility = 'visible';
80 if (props.error) {
81 Icon = StyledRoundSaltireIconError;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040082 } else if (success) {
simond47ef9e2022-09-28 22:24:28 -040083 Icon = StyledCheckedIconSuccess;
84 } else if (!isSelected && !input) {
85 visibility = 'hidden'; // keep icon's space so text does not move
idillon-sfl37c18df2022-08-26 18:44:27 -040086 }
simond47ef9e2022-09-28 22:24:28 -040087 setStartAdornment(<Icon sx={{ visibility }} />);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040088 }, [props.error, success, isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -040089
simond47ef9e2022-09-28 22:24:28 -040090 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040091 <>
92 <RulesDialog openDialog={isDialogOpened} title="Username rules :" closeDialog={() => setIsDialogOpened(false)}>
93 <UsernameRules />
94 </RulesDialog>
95 <TextField
96 {...props}
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040097 color={inputColor(props.error, success)}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040098 label={'Choose an identifier'}
99 variant="standard"
100 InputLabelProps={{ shrink: !!(isSelected || input) }}
101 onChange={onChange}
102 InputProps={{
103 startAdornment,
104 endAdornment: (
105 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
106 ),
107 }}
108 onFocus={() => setIsSelected(true)}
109 onBlur={() => setIsSelected(false)}
110 />
111 </>
simond47ef9e2022-09-28 22:24:28 -0400112 );
113};
idillon-sfl37c18df2022-08-26 18:44:27 -0400114
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400115export const PasswordInput = ({
116 infoButtonProps,
117 onChange: _onChange,
118 success,
119 tooltipTitle,
120 ...props
121}: InputProps) => {
simond47ef9e2022-09-28 22:24:28 -0400122 const [showPassword, setShowPassword] = useState(false);
123 const [isSelected, setIsSelected] = useState(false);
124 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400125 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400126 const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
idillon-sfl37c18df2022-08-26 18:44:27 -0400127
simond47ef9e2022-09-28 22:24:28 -0400128 const toggleShowPassword = () => {
129 setShowPassword((showPassword) => !showPassword);
130 };
131
132 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400133 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400134 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400135 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400136 },
simon80b7b3b2022-09-28 17:50:10 -0400137 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400138 );
139
140 useEffect(() => {
141 /* Handle startAdornment */
142 let Icon = StyledLockIcon;
143 let visibility = 'visible';
144 if (props.error) {
145 Icon = StyledRoundSaltireIconError;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400146 } else if (success) {
simond47ef9e2022-09-28 22:24:28 -0400147 Icon = StyledCheckedIconSuccess;
148 } else if (!isSelected && !input) {
149 visibility = 'hidden'; // keep icon's space so text does not move
150 }
151 setStartAdornment(<Icon sx={{ visibility }} />);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400152 }, [props.error, success, isSelected, input]);
simond47ef9e2022-09-28 22:24:28 -0400153
154 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400155 <>
156 <RulesDialog openDialog={isDialogOpened} title="Password rules :" closeDialog={() => setIsDialogOpened(false)}>
157 <PasswordRules />
158 </RulesDialog>
159 <TextField
160 {...props}
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400161 color={inputColor(props.error, success)}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400162 label="Password"
163 type={showPassword ? 'text' : 'password'}
164 variant="standard"
165 autoComplete="current-password"
166 InputLabelProps={{ shrink: !!(isSelected || input) }}
167 onChange={onChange}
168 InputProps={{
169 startAdornment,
170 endAdornment: (
171 <Stack direction="row" spacing="14px" alignItems="center">
172 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
173 <ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
174 </Stack>
175 ),
176 }}
177 onFocus={() => setIsSelected(true)}
178 onBlur={() => setIsSelected(false)}
179 />
180 </>
simond47ef9e2022-09-28 22:24:28 -0400181 );
182};
idillon-sfl37c18df2022-08-26 18:44:27 -0400183
simon35378692022-10-02 23:25:57 -0400184export const NickNameInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400185 const [isSelected, setIsSelected] = useState(false);
186 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400187 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
idillon-sfl37c18df2022-08-26 18:44:27 -0400188
simond47ef9e2022-09-28 22:24:28 -0400189 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400190 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400191 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400192 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400193 },
simon80b7b3b2022-09-28 17:50:10 -0400194 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400195 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400196
simond47ef9e2022-09-28 22:24:28 -0400197 useEffect(() => {
198 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
199 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400200
simond47ef9e2022-09-28 22:24:28 -0400201 return (
202 <TextField
203 {...props}
204 label="Nickname, surname..."
205 variant="standard"
206 InputLabelProps={{ shrink: !!(isSelected || input) }}
207 onChange={onChange}
208 InputProps={{
209 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
210 }}
211 onFocus={() => setIsSelected(true)}
212 onBlur={() => setIsSelected(false)}
213 />
214 );
215};
idillon-sfl37c18df2022-08-26 18:44:27 -0400216
simon35378692022-10-02 23:25:57 -0400217export const RegularInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400218 const [isSelected, setIsSelected] = useState(false);
219 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400220 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
221 const [endAdornmentVisibility, setEndAdornmentVisibility] = useState<'visible' | 'hidden'>('visible');
idillon-sfl37c18df2022-08-26 18:44:27 -0400222
simond47ef9e2022-09-28 22:24:28 -0400223 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400224 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400225 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400226 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400227 },
simon80b7b3b2022-09-28 17:50:10 -0400228 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400229 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400230
simond47ef9e2022-09-28 22:24:28 -0400231 useEffect(() => {
232 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
233 setEndAdornmentVisibility(isSelected || input ? 'hidden' : 'visible');
234 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400235
simond47ef9e2022-09-28 22:24:28 -0400236 return (
237 <TextField
238 {...props}
239 variant="standard"
240 InputLabelProps={{ shrink: !!(isSelected || input) }}
241 onChange={onChange}
242 InputProps={{
243 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
244 endAdornment: <StyledPenIconDark sx={{ visibility: endAdornmentVisibility }} />,
245 }}
246 onFocus={() => setIsSelected(true)}
247 onBlur={() => setIsSelected(false)}
248 />
249 );
250};
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400251
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400252function inputColor(
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400253 error?: boolean,
254 success?: boolean
255): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
256 return error ? 'error' : success ? 'success' : 'primary';
257}
258
259const PasswordRules = () => {
260 return (
261 <Typography variant="body1">
262 <List>
263 <ListItem>
264 <ListItemIcon>
265 <GppMaybe />
266 </ListItemIcon>
267 The password must contain at least 1 lowercase alphabetical character.
268 </ListItem>
269 <ListItem>
270 <ListItemIcon>
271 <GppMaybe />
272 </ListItemIcon>
273 The password must contain at least 1 uppercase alphabetical character.
274 </ListItem>
275 <ListItem>
276 <ListItemIcon>
277 <GppMaybe />
278 </ListItemIcon>
279 The password must contain at least 1 numeric character.
280 </ListItem>
281 <ListItem>
282 <ListItemIcon>
283 <GppMaybe />
284 </ListItemIcon>
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400285 The password must contain at least 1 special character.
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400286 </ListItem>
287 <ListItem>
288 <ListItemIcon>
289 <GppMaybe />
290 </ListItemIcon>
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400291 The password must be 10 characters or longer to be considered strong.
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400292 </ListItem>
293 </List>
294 </Typography>
295 );
296};
297
298const UsernameRules = () => {
299 return (
300 <Typography variant="body1">
301 <List>
302 <ListItem>
303 <ListItemIcon>
304 <Warning />
305 </ListItemIcon>
306 The username must be 3 to 32 characters long.
307 </ListItem>
308 <ListItem>
309 <ListItemIcon>
310 <Warning />
311 </ListItemIcon>
312 The username may contain lowercase and uppercase alphabetical characters.
313 </ListItem>
314 <ListItem>
315 <ListItemIcon>
316 <Warning />
317 </ListItemIcon>
318 The username may contain hyphens {'(-)'}.
319 </ListItem>
320 <ListItem>
321 <ListItemIcon>
322 <Warning />
323 </ListItemIcon>
324 The username may contain underscores {'(_)'}.
325 </ListItem>
326 </List>
327 </Typography>
328 );
329};