blob: ae640cf1b2ba8a0d9ee5b5c71042bd9ca9346f72 [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 Simef5ebc2e2022-10-27 18:30:53 -040056export const UsernameInput = ({ infoButtonProps, onChange: _onChange, tooltipTitle, ...props }: InputProps) => {
simond47ef9e2022-09-28 22:24:28 -040057 const [isSelected, setIsSelected] = useState(false);
58 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -040059 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040060 const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
idillon-sfl37c18df2022-08-26 18:44:27 -040061
simond47ef9e2022-09-28 22:24:28 -040062 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -040063 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -040064 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -040065 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -040066 },
simon80b7b3b2022-09-28 17:50:10 -040067 [_onChange]
simond47ef9e2022-09-28 22:24:28 -040068 );
idillon-sfl37c18df2022-08-26 18:44:27 -040069
simond47ef9e2022-09-28 22:24:28 -040070 useEffect(() => {
71 /* Handle startAdornment */
72 let Icon = StyledPersonIconLight;
73 let visibility = 'visible';
74 if (props.error) {
75 Icon = StyledRoundSaltireIconError;
76 } else if (props.success) {
77 Icon = StyledCheckedIconSuccess;
78 } else if (!isSelected && !input) {
79 visibility = 'hidden'; // keep icon's space so text does not move
idillon-sfl37c18df2022-08-26 18:44:27 -040080 }
simond47ef9e2022-09-28 22:24:28 -040081 setStartAdornment(<Icon sx={{ visibility }} />);
82 }, [props.error, props.success, isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -040083
simond47ef9e2022-09-28 22:24:28 -040084 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040085 <>
86 <RulesDialog openDialog={isDialogOpened} title="Username rules :" closeDialog={() => setIsDialogOpened(false)}>
87 <UsernameRules />
88 </RulesDialog>
89 <TextField
90 {...props}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040091 color={inputColor(props.error, props.success)}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040092 label={'Choose an identifier'}
93 variant="standard"
94 InputLabelProps={{ shrink: !!(isSelected || input) }}
95 onChange={onChange}
96 InputProps={{
97 startAdornment,
98 endAdornment: (
99 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
100 ),
101 }}
102 onFocus={() => setIsSelected(true)}
103 onBlur={() => setIsSelected(false)}
104 />
105 </>
simond47ef9e2022-09-28 22:24:28 -0400106 );
107};
idillon-sfl37c18df2022-08-26 18:44:27 -0400108
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400109export const PasswordInput = ({ infoButtonProps, onChange: _onChange, tooltipTitle, ...props }: InputProps) => {
simond47ef9e2022-09-28 22:24:28 -0400110 const [showPassword, setShowPassword] = useState(false);
111 const [isSelected, setIsSelected] = useState(false);
112 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400113 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400114 const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
idillon-sfl37c18df2022-08-26 18:44:27 -0400115
simond47ef9e2022-09-28 22:24:28 -0400116 const toggleShowPassword = () => {
117 setShowPassword((showPassword) => !showPassword);
118 };
119
120 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400121 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400122 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400123 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400124 },
simon80b7b3b2022-09-28 17:50:10 -0400125 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400126 );
127
128 useEffect(() => {
129 /* Handle startAdornment */
130 let Icon = StyledLockIcon;
131 let visibility = 'visible';
132 if (props.error) {
133 Icon = StyledRoundSaltireIconError;
134 } else if (props.success) {
135 Icon = StyledCheckedIconSuccess;
136 } else if (!isSelected && !input) {
137 visibility = 'hidden'; // keep icon's space so text does not move
138 }
139 setStartAdornment(<Icon sx={{ visibility }} />);
140 }, [props.error, props.success, isSelected, input]);
141
142 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400143 <>
144 <RulesDialog openDialog={isDialogOpened} title="Password rules :" closeDialog={() => setIsDialogOpened(false)}>
145 <PasswordRules />
146 </RulesDialog>
147 <TextField
148 {...props}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400149 color={inputColor(props.error, props.success)}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400150 label="Password"
151 type={showPassword ? 'text' : 'password'}
152 variant="standard"
153 autoComplete="current-password"
154 InputLabelProps={{ shrink: !!(isSelected || input) }}
155 onChange={onChange}
156 InputProps={{
157 startAdornment,
158 endAdornment: (
159 <Stack direction="row" spacing="14px" alignItems="center">
160 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
161 <ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
162 </Stack>
163 ),
164 }}
165 onFocus={() => setIsSelected(true)}
166 onBlur={() => setIsSelected(false)}
167 />
168 </>
simond47ef9e2022-09-28 22:24:28 -0400169 );
170};
idillon-sfl37c18df2022-08-26 18:44:27 -0400171
simon35378692022-10-02 23:25:57 -0400172export const NickNameInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400173 const [isSelected, setIsSelected] = useState(false);
174 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400175 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
idillon-sfl37c18df2022-08-26 18:44:27 -0400176
simond47ef9e2022-09-28 22:24:28 -0400177 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400178 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400179 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400180 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400181 },
simon80b7b3b2022-09-28 17:50:10 -0400182 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400183 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400184
simond47ef9e2022-09-28 22:24:28 -0400185 useEffect(() => {
186 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
187 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400188
simond47ef9e2022-09-28 22:24:28 -0400189 return (
190 <TextField
191 {...props}
192 label="Nickname, surname..."
193 variant="standard"
194 InputLabelProps={{ shrink: !!(isSelected || input) }}
195 onChange={onChange}
196 InputProps={{
197 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
198 }}
199 onFocus={() => setIsSelected(true)}
200 onBlur={() => setIsSelected(false)}
201 />
202 );
203};
idillon-sfl37c18df2022-08-26 18:44:27 -0400204
simon35378692022-10-02 23:25:57 -0400205export const RegularInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400206 const [isSelected, setIsSelected] = useState(false);
207 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400208 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
209 const [endAdornmentVisibility, setEndAdornmentVisibility] = useState<'visible' | 'hidden'>('visible');
idillon-sfl37c18df2022-08-26 18:44:27 -0400210
simond47ef9e2022-09-28 22:24:28 -0400211 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400212 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400213 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400214 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400215 },
simon80b7b3b2022-09-28 17:50:10 -0400216 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400217 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400218
simond47ef9e2022-09-28 22:24:28 -0400219 useEffect(() => {
220 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
221 setEndAdornmentVisibility(isSelected || input ? 'hidden' : 'visible');
222 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400223
simond47ef9e2022-09-28 22:24:28 -0400224 return (
225 <TextField
226 {...props}
227 variant="standard"
228 InputLabelProps={{ shrink: !!(isSelected || input) }}
229 onChange={onChange}
230 InputProps={{
231 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
232 endAdornment: <StyledPenIconDark sx={{ visibility: endAdornmentVisibility }} />,
233 }}
234 onFocus={() => setIsSelected(true)}
235 onBlur={() => setIsSelected(false)}
236 />
237 );
238};
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400239
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400240function inputColor(
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400241 error?: boolean,
242 success?: boolean
243): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
244 return error ? 'error' : success ? 'success' : 'primary';
245}
246
247const PasswordRules = () => {
248 return (
249 <Typography variant="body1">
250 <List>
251 <ListItem>
252 <ListItemIcon>
253 <GppMaybe />
254 </ListItemIcon>
255 The password must contain at least 1 lowercase alphabetical character.
256 </ListItem>
257 <ListItem>
258 <ListItemIcon>
259 <GppMaybe />
260 </ListItemIcon>
261 The password must contain at least 1 uppercase alphabetical character.
262 </ListItem>
263 <ListItem>
264 <ListItemIcon>
265 <GppMaybe />
266 </ListItemIcon>
267 The password must contain at least 1 numeric character.
268 </ListItem>
269 <ListItem>
270 <ListItemIcon>
271 <GppMaybe />
272 </ListItemIcon>
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400273 The password must contain at least 1 special character.
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400274 </ListItem>
275 <ListItem>
276 <ListItemIcon>
277 <GppMaybe />
278 </ListItemIcon>
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400279 The password must be 10 characters or longer to be considered strong.
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400280 </ListItem>
281 </List>
282 </Typography>
283 );
284};
285
286const UsernameRules = () => {
287 return (
288 <Typography variant="body1">
289 <List>
290 <ListItem>
291 <ListItemIcon>
292 <Warning />
293 </ListItemIcon>
294 The username must be 3 to 32 characters long.
295 </ListItem>
296 <ListItem>
297 <ListItemIcon>
298 <Warning />
299 </ListItemIcon>
300 The username may contain lowercase and uppercase alphabetical characters.
301 </ListItem>
302 <ListItem>
303 <ListItemIcon>
304 <Warning />
305 </ListItemIcon>
306 The username may contain hyphens {'(-)'}.
307 </ListItem>
308 <ListItem>
309 <ListItemIcon>
310 <Warning />
311 </ListItemIcon>
312 The username may contain underscores {'(_)'}.
313 </ListItem>
314 </List>
315 </Typography>
316 );
317};