Add click to reveal password (#624)
This commit is contained in:
@ -1,10 +1,12 @@
|
|||||||
import { ChangeEvent, useRef } from 'react';
|
import { ChangeEvent, useRef, useState } from 'react';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { usePreviousHotkeysScope } from '@/hotkeys/hooks/internal/usePreviousHotkeysScope';
|
import { usePreviousHotkeysScope } from '@/hotkeys/hooks/internal/usePreviousHotkeysScope';
|
||||||
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
|
||||||
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||||
|
import { IconEye, IconEyeOff } from '@/ui/icons/index';
|
||||||
|
|
||||||
type OwnProps = Omit<
|
type OwnProps = Omit<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
@ -50,11 +52,26 @@ const StyledInput = styled.input<{ fullWidth: boolean }>`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledIconContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIcon = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
export function TextInput({
|
export function TextInput({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
fullWidth,
|
fullWidth,
|
||||||
|
type,
|
||||||
...props
|
...props
|
||||||
}: OwnProps): JSX.Element {
|
}: OwnProps): JSX.Element {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@ -80,23 +97,46 @@ export function TextInput({
|
|||||||
InternalHotkeysScope.TextInput,
|
InternalHotkeysScope.TextInput,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||||
|
|
||||||
|
const handleTogglePasswordVisibility = () => {
|
||||||
|
setPasswordVisible(!passwordVisible);
|
||||||
|
};
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
{label && <StyledLabel>{label}</StyledLabel>}
|
{label && <StyledLabel>{label}</StyledLabel>}
|
||||||
<StyledInput
|
<StyledIconContainer>
|
||||||
ref={inputRef}
|
<StyledInput
|
||||||
tabIndex={props.tabIndex ?? 0}
|
ref={inputRef}
|
||||||
onFocus={handleFocus}
|
tabIndex={props.tabIndex ?? 0}
|
||||||
onBlur={handleBlur}
|
onFocus={handleFocus}
|
||||||
fullWidth={fullWidth ?? false}
|
onBlur={handleBlur}
|
||||||
value={value}
|
fullWidth={fullWidth ?? false}
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
value={value}
|
||||||
if (onChange) {
|
type={passwordVisible ? 'text' : type}
|
||||||
onChange(event.target.value);
|
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
}
|
if (onChange) {
|
||||||
}}
|
onChange(event.target.value);
|
||||||
{...props}
|
}
|
||||||
/>
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{type === 'password' && ( // only show the icon for password inputs
|
||||||
|
<StyledIcon
|
||||||
|
onClick={handleTogglePasswordVisibility}
|
||||||
|
data-testid="reveal-password-button"
|
||||||
|
>
|
||||||
|
{passwordVisible ? (
|
||||||
|
<IconEyeOff size={theme.icon.size.md} />
|
||||||
|
) : (
|
||||||
|
<IconEye size={theme.icon.size.md} />
|
||||||
|
)}
|
||||||
|
</StyledIcon>
|
||||||
|
)}
|
||||||
|
</StyledIconContainer>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,3 +63,24 @@ export const FullWidth: Story = {
|
|||||||
/>,
|
/>,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PasswordInput: Story = {
|
||||||
|
render: getRenderWrapperForComponent(
|
||||||
|
<TextInput
|
||||||
|
onChange={changeJestFn}
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
/>,
|
||||||
|
),
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const input = canvas.getByPlaceholderText('Password');
|
||||||
|
await userEvent.type(input, 'pa$$w0rd');
|
||||||
|
|
||||||
|
const revealButton = canvas.getByTestId('reveal-password-button');
|
||||||
|
await userEvent.click(revealButton);
|
||||||
|
|
||||||
|
expect(input).toHaveAttribute('type', 'text');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -35,3 +35,5 @@ export { IconNotes } from '@tabler/icons-react';
|
|||||||
export { IconCirclePlus } from '@tabler/icons-react';
|
export { IconCirclePlus } from '@tabler/icons-react';
|
||||||
export { IconCheckbox } from '@tabler/icons-react';
|
export { IconCheckbox } from '@tabler/icons-react';
|
||||||
export { IconTimelineEvent } from '@tabler/icons-react';
|
export { IconTimelineEvent } from '@tabler/icons-react';
|
||||||
|
export { IconEye } from '@tabler/icons-react';
|
||||||
|
export { IconEyeOff } from '@tabler/icons-react';
|
||||||
|
|||||||
Reference in New Issue
Block a user