Skip to content

Commit bd0b460

Browse files
feat: add a button to hide and show password fields
1 parent 1c47cd7 commit bd0b460

File tree

3 files changed

+191
-4
lines changed

3 files changed

+191
-4
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { PasswordInput as ChakraPasswordInput } from '@/components/ui/password-input'
2+
import { forwardRef } from 'react'
3+
import { FiEye, FiEyeOff } from 'react-icons/fi'
4+
5+
interface PasswordInputProps {
6+
placeholder: string
7+
value?: string
8+
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
9+
name?: string
10+
ref?: React.Ref<HTMLInputElement>
11+
}
12+
13+
const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>((props, ref) => {
14+
return (
15+
<ChakraPasswordInput
16+
ref={ref}
17+
placeholder={props.placeholder}
18+
value={props.value}
19+
onChange={props.onChange}
20+
name={props.name}
21+
borderRadius="sm"
22+
color="fg.DEFAULT"
23+
bg="bg.input"
24+
width="100%"
25+
visibilityIcon={{ on: <FiEye />, off: <FiEyeOff /> }}
26+
css={{
27+
'&:focus': {
28+
outline: 'none',
29+
borderColor: 'bg.50',
30+
bg: 'bg.100',
31+
},
32+
'&::selection': {
33+
backgroundColor: 'bg.50',
34+
color: 'accent.blue',
35+
},
36+
}}
37+
/>
38+
)
39+
})
40+
41+
export default PasswordInput
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"use client"
2+
3+
import type {
4+
ButtonProps,
5+
GroupProps,
6+
InputProps,
7+
StackProps,
8+
} from "@chakra-ui/react"
9+
import {
10+
Box,
11+
HStack,
12+
IconButton,
13+
Input,
14+
InputGroup,
15+
Stack,
16+
mergeRefs,
17+
useControllableState,
18+
} from "@chakra-ui/react"
19+
import * as React from "react"
20+
import { LuEye, LuEyeOff } from "react-icons/lu"
21+
22+
export interface PasswordVisibilityProps {
23+
defaultVisible?: boolean
24+
visible?: boolean
25+
onVisibleChange?: (visible: boolean) => void
26+
visibilityIcon?: { on: React.ReactNode; off: React.ReactNode }
27+
}
28+
29+
export interface PasswordInputProps
30+
extends InputProps,
31+
PasswordVisibilityProps {
32+
rootProps?: GroupProps
33+
}
34+
35+
export const PasswordInput = React.forwardRef<
36+
HTMLInputElement,
37+
PasswordInputProps
38+
>(function PasswordInput(props, ref) {
39+
const {
40+
rootProps,
41+
defaultVisible,
42+
visible: visibleProp,
43+
onVisibleChange,
44+
visibilityIcon = { on: <LuEye />, off: <LuEyeOff /> },
45+
...rest
46+
} = props
47+
48+
const [visible, setVisible] = useControllableState({
49+
value: visibleProp,
50+
defaultValue: defaultVisible || false,
51+
onChange: onVisibleChange,
52+
})
53+
54+
const inputRef = React.useRef<HTMLInputElement>(null)
55+
56+
return (
57+
<InputGroup
58+
endElement={
59+
<VisibilityTrigger
60+
disabled={rest.disabled}
61+
onPointerDown={(e) => {
62+
if (rest.disabled) return
63+
if (e.button !== 0) return
64+
e.preventDefault()
65+
setVisible(!visible)
66+
}}
67+
>
68+
{visible ? visibilityIcon.off : visibilityIcon.on}
69+
</VisibilityTrigger>
70+
}
71+
{...rootProps}
72+
>
73+
<Input
74+
{...rest}
75+
ref={mergeRefs(ref, inputRef)}
76+
type={visible ? "text" : "password"}
77+
/>
78+
</InputGroup>
79+
)
80+
})
81+
82+
const VisibilityTrigger = React.forwardRef<HTMLButtonElement, ButtonProps>(
83+
function VisibilityTrigger(props, ref) {
84+
return (
85+
<IconButton
86+
tabIndex={-1}
87+
ref={ref}
88+
me="-2"
89+
aspectRatio="square"
90+
size="sm"
91+
variant="ghost"
92+
height="calc(100% - {spacing.2})"
93+
aria-label="Toggle password visibility"
94+
{...props}
95+
/>
96+
)
97+
},
98+
)
99+
100+
interface PasswordStrengthMeterProps extends StackProps {
101+
max?: number
102+
value: number
103+
}
104+
105+
export const PasswordStrengthMeter = React.forwardRef<
106+
HTMLDivElement,
107+
PasswordStrengthMeterProps
108+
>(function PasswordStrengthMeter(props, ref) {
109+
const { max = 4, value, ...rest } = props
110+
111+
const percent = (value / max) * 100
112+
const { label, colorPalette } = getColorPalette(percent)
113+
114+
return (
115+
<Stack align="flex-end" gap="1" ref={ref} {...rest}>
116+
<HStack width="full" ref={ref} {...rest}>
117+
{Array.from({ length: max }).map((_, index) => (
118+
<Box
119+
key={index}
120+
height="1"
121+
flex="1"
122+
rounded="sm"
123+
data-selected={index < value ? "" : undefined}
124+
layerStyle="fill.subtle"
125+
colorPalette="gray"
126+
_selected={{
127+
colorPalette,
128+
layerStyle: "fill.solid",
129+
}}
130+
/>
131+
))}
132+
</HStack>
133+
{label && <HStack textStyle="xs">{label}</HStack>}
134+
</Stack>
135+
)
136+
})
137+
138+
function getColorPalette(percent: number) {
139+
switch (true) {
140+
case percent < 33:
141+
return { label: "Low", colorPalette: "red" }
142+
case percent < 66:
143+
return { label: "Medium", colorPalette: "orange" }
144+
default:
145+
return { label: "High", colorPalette: "green" }
146+
}
147+
}

frontend/src/routes/_publicLayout/signup.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { type SubmitHandler, useForm } from 'react-hook-form'
66
import { useTranslation } from 'react-i18next'
77
import type { UserRegister } from '../../client'
88
import { DefaultInput } from '../../components/commonUI/Input'
9+
import PasswordInput from '../../components/commonUI/PasswordInput'
910
import { confirmPasswordRules, emailPattern, passwordRules } from '../../utils'
1011

1112
export const Route = createFileRoute('/_publicLayout/signup')({
@@ -88,9 +89,8 @@ function SignUp() {
8889

8990
<Field.Root>
9091
<Field.Label>{t('general.words.password')}</Field.Label>
91-
<DefaultInput
92+
<PasswordInput
9293
placeholder={t('general.words.password')}
93-
type="password"
9494
{...register('password', passwordRules())}
9595
/>
9696
{errors.password && (
@@ -102,9 +102,8 @@ function SignUp() {
102102

103103
<Field.Root>
104104
<Field.Label>{t('general.actions.confirmPassword')}</Field.Label>
105-
<DefaultInput
105+
<PasswordInput
106106
placeholder={t('general.actions.repeatPassword')}
107-
type="password"
108107
{...register('confirm_password', confirmPasswordRules(getValues))}
109108
/>
110109
{errors.confirm_password && (

0 commit comments

Comments
 (0)