blob: e867e268ecbbf29a74d0a8ff07eba411ad8c3049 [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}
91 label={'Choose an identifier'}
92 variant="standard"
93 InputLabelProps={{ shrink: !!(isSelected || input) }}
94 onChange={onChange}
95 InputProps={{
96 startAdornment,
97 endAdornment: (
98 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
99 ),
100 }}
101 onFocus={() => setIsSelected(true)}
102 onBlur={() => setIsSelected(false)}
103 />
104 </>
simond47ef9e2022-09-28 22:24:28 -0400105 );
106};
idillon-sfl37c18df2022-08-26 18:44:27 -0400107
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400108export const PasswordInput = ({ infoButtonProps, onChange: _onChange, tooltipTitle, ...props }: InputProps) => {
simond47ef9e2022-09-28 22:24:28 -0400109 const [showPassword, setShowPassword] = useState(false);
110 const [isSelected, setIsSelected] = useState(false);
111 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400112 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400113 const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
idillon-sfl37c18df2022-08-26 18:44:27 -0400114
simond47ef9e2022-09-28 22:24:28 -0400115 const toggleShowPassword = () => {
116 setShowPassword((showPassword) => !showPassword);
117 };
118
119 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400120 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400121 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400122 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400123 },
simon80b7b3b2022-09-28 17:50:10 -0400124 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400125 );
126
127 useEffect(() => {
128 /* Handle startAdornment */
129 let Icon = StyledLockIcon;
130 let visibility = 'visible';
131 if (props.error) {
132 Icon = StyledRoundSaltireIconError;
133 } else if (props.success) {
134 Icon = StyledCheckedIconSuccess;
135 } else if (!isSelected && !input) {
136 visibility = 'hidden'; // keep icon's space so text does not move
137 }
138 setStartAdornment(<Icon sx={{ visibility }} />);
139 }, [props.error, props.success, isSelected, input]);
140
141 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400142 <>
143 <RulesDialog openDialog={isDialogOpened} title="Password rules :" closeDialog={() => setIsDialogOpened(false)}>
144 <PasswordRules />
145 </RulesDialog>
146 <TextField
147 {...props}
148 label="Password"
149 type={showPassword ? 'text' : 'password'}
150 variant="standard"
151 autoComplete="current-password"
152 InputLabelProps={{ shrink: !!(isSelected || input) }}
153 onChange={onChange}
154 InputProps={{
155 startAdornment,
156 endAdornment: (
157 <Stack direction="row" spacing="14px" alignItems="center">
158 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
159 <ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
160 </Stack>
161 ),
162 }}
163 onFocus={() => setIsSelected(true)}
164 onBlur={() => setIsSelected(false)}
165 />
166 </>
simond47ef9e2022-09-28 22:24:28 -0400167 );
168};
idillon-sfl37c18df2022-08-26 18:44:27 -0400169
simon35378692022-10-02 23:25:57 -0400170export const NickNameInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400171 const [isSelected, setIsSelected] = useState(false);
172 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400173 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
idillon-sfl37c18df2022-08-26 18:44:27 -0400174
simond47ef9e2022-09-28 22:24:28 -0400175 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400176 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400177 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400178 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400179 },
simon80b7b3b2022-09-28 17:50:10 -0400180 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400181 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400182
simond47ef9e2022-09-28 22:24:28 -0400183 useEffect(() => {
184 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
185 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400186
simond47ef9e2022-09-28 22:24:28 -0400187 return (
188 <TextField
189 {...props}
190 label="Nickname, surname..."
191 variant="standard"
192 InputLabelProps={{ shrink: !!(isSelected || input) }}
193 onChange={onChange}
194 InputProps={{
195 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
196 }}
197 onFocus={() => setIsSelected(true)}
198 onBlur={() => setIsSelected(false)}
199 />
200 );
201};
idillon-sfl37c18df2022-08-26 18:44:27 -0400202
simon35378692022-10-02 23:25:57 -0400203export const RegularInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400204 const [isSelected, setIsSelected] = useState(false);
205 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400206 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
207 const [endAdornmentVisibility, setEndAdornmentVisibility] = useState<'visible' | 'hidden'>('visible');
idillon-sfl37c18df2022-08-26 18:44:27 -0400208
simond47ef9e2022-09-28 22:24:28 -0400209 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400210 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400211 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400212 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400213 },
simon80b7b3b2022-09-28 17:50:10 -0400214 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400215 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400216
simond47ef9e2022-09-28 22:24:28 -0400217 useEffect(() => {
218 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
219 setEndAdornmentVisibility(isSelected || input ? 'hidden' : 'visible');
220 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400221
simond47ef9e2022-09-28 22:24:28 -0400222 return (
223 <TextField
224 {...props}
225 variant="standard"
226 InputLabelProps={{ shrink: !!(isSelected || input) }}
227 onChange={onChange}
228 InputProps={{
229 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
230 endAdornment: <StyledPenIconDark sx={{ visibility: endAdornmentVisibility }} />,
231 }}
232 onFocus={() => setIsSelected(true)}
233 onBlur={() => setIsSelected(false)}
234 />
235 );
236};
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400237
238export function inputColor(
239 error?: boolean,
240 success?: boolean
241): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
242 return error ? 'error' : success ? 'success' : 'primary';
243}
244
245const PasswordRules = () => {
246 return (
247 <Typography variant="body1">
248 <List>
249 <ListItem>
250 <ListItemIcon>
251 <GppMaybe />
252 </ListItemIcon>
253 The password must contain at least 1 lowercase alphabetical character.
254 </ListItem>
255 <ListItem>
256 <ListItemIcon>
257 <GppMaybe />
258 </ListItemIcon>
259 The password must contain at least 1 uppercase alphabetical character.
260 </ListItem>
261 <ListItem>
262 <ListItemIcon>
263 <GppMaybe />
264 </ListItemIcon>
265 The password must contain at least 1 numeric character.
266 </ListItem>
267 <ListItem>
268 <ListItemIcon>
269 <GppMaybe />
270 </ListItemIcon>
271 The password must contain at least one special character.
272 </ListItem>
273 <ListItem>
274 <ListItemIcon>
275 <GppMaybe />
276 </ListItemIcon>
277 The password must be eight characters or longer for Strong strength.
278 </ListItem>
279 </List>
280 </Typography>
281 );
282};
283
284const UsernameRules = () => {
285 return (
286 <Typography variant="body1">
287 <List>
288 <ListItem>
289 <ListItemIcon>
290 <Warning />
291 </ListItemIcon>
292 The username must be 3 to 32 characters long.
293 </ListItem>
294 <ListItem>
295 <ListItemIcon>
296 <Warning />
297 </ListItemIcon>
298 The username may contain lowercase and uppercase alphabetical characters.
299 </ListItem>
300 <ListItem>
301 <ListItemIcon>
302 <Warning />
303 </ListItemIcon>
304 The username may contain hyphens {'(-)'}.
305 </ListItem>
306 <ListItem>
307 <ListItemIcon>
308 <Warning />
309 </ListItemIcon>
310 The username may contain underscores {'(_)'}.
311 </ListItem>
312 </List>
313 </Typography>
314 );
315};