Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Input password component layout to avoid password manager button overlap with visibility button #12398

Merged
merged 5 commits into from
May 2, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions airbyte-webapp/src/components/base/Input/Input.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { render } from "@testing-library/react";

import { Input } from "./Input";

describe("<Input />", () => {
test("renders text input", () => {
const value = "aribyte@example.com";
const { getByTestId, queryByTestId } = render(<Input defaultValue={value} />);

expect(getByTestId("input")).toHaveAttribute("type", "text");
expect(getByTestId("input")).toHaveValue(value);
expect(queryByTestId("toggle-password-visibility-button")).toBeFalsy();
});

test("renders password input with visibilty button", () => {
const value = "eight888";
const { getByTestId, getByRole } = render(<Input type="password" defaultValue={value} />);

expect(getByTestId("input")).toHaveAttribute("type", "password");
expect(getByTestId("input")).toHaveValue(value);
expect(getByRole("img", { hidden: true })).toHaveAttribute("data-icon", "eye");
});

test("renders visible password when visibility button is clicked", () => {
const value = "eight888";
const { getByTestId, getByRole } = render(<Input type="password" defaultValue={value} />);

getByTestId("toggle-password-visibility-button").click();

expect(getByTestId("input")).toHaveAttribute("type", "text");
expect(getByTestId("input")).toHaveValue(value);
expect(getByRole("img", { hidden: true })).toHaveAttribute("data-icon", "eye-slash");
});
});
96 changes: 59 additions & 37 deletions airbyte-webapp/src/components/base/Input/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useState } from "react";
import { useToggle } from "react-use";
import styled from "styled-components";
import { Theme } from "theme";

Expand All @@ -23,67 +24,88 @@ export type InputProps = {
light?: boolean;
} & React.InputHTMLAttributes<HTMLInputElement>;

const InputComponent = styled.input<InputProps>`
outline: none;
const InputContainer = styled.div<InputProps>`
width: 100%;
padding: 7px 18px 7px 8px;
border-radius: 4px;
font-size: 14px;
line-height: 20px;
font-weight: normal;
border: 1px solid ${(props) => (props.error ? props.theme.dangerColor : props.theme.greyColor0)};
position: relative;
background: ${(props) => getBackgroundColor(props)};
color: ${({ theme }) => theme.textColor};
caret-color: ${({ theme }) => theme.primaryColor};

&::placeholder {
color: ${({ theme }) => theme.greyColor40};
}
border: 1px solid ${(props) => (props.error ? props.theme.dangerColor : props.theme.greyColor0)};
border-radius: 4px;

&:hover {
background: ${({ theme, light }) => (light ? theme.whiteColor : theme.greyColor20)};
border-color: ${(props) => (props.error ? props.theme.dangerColor : props.theme.greyColor20)};
}

&:focus {
&.input-container--focused {
background: ${({ theme, light }) => (light ? theme.whiteColor : theme.primaryColor12)};
border-color: ${({ theme }) => theme.primaryColor};
}
`;

const InputComponent = styled.input<InputProps & { isPassword?: boolean }>`
outline: none;
width: ${({ isPassword }) => (isPassword ? "calc(100% - 22px)" : "100%")};
padding: 7px 8px 7px 8px;
font-size: 14px;
line-height: 20px;
font-weight: normal;
border: none;
background: none;
color: ${({ theme }) => theme.textColor};
caret-color: ${({ theme }) => theme.primaryColor};

&::placeholder {
color: ${({ theme }) => theme.greyColor40};
}

&:disabled {
pointer-events: none;
color: ${({ theme }) => theme.greyColor55};
}
`;

const Container = styled.div`
width: 100%;
position: relative;
`;

const VisibilityButton = styled(Button)`
position: absolute;
right: 2px;
top: 7px;
right: 0px;
top: 0;
display: flex;
height: 100%;
width: 30px;
align-items: center;
justify-content: center;
border: none;
`;

const Input: React.FC<InputProps> = (props) => {
const [isContentVisible, setIsContentVisible] = useState(false);

if (props.type === "password") {
return (
<Container>
<InputComponent {...props} type={isContentVisible ? "text" : "password"} />
{props.disabled ? null : (
<VisibilityButton iconOnly onClick={() => setIsContentVisible(!isContentVisible)} type="button">
<FontAwesomeIcon icon={isContentVisible ? faEyeSlash : faEye} />
</VisibilityButton>
)}
</Container>
);
}

return <InputComponent {...props} />;
const [focused, toggleFocused] = useToggle(false);

const isPassword = props.type === "password";
const isVisibilityButtonVisible = isPassword && !props.disabled;
const onInputFocusChange = () => toggleFocused();
krishnaglick marked this conversation as resolved.
Show resolved Hide resolved

return (
<InputContainer {...props} className={focused ? "input-container--focused" : undefined}>
<InputComponent
{...props}
type={!isPassword || isContentVisible ? "text" : "password"}
isPassword={isPassword}
onFocus={onInputFocusChange}
onBlur={onInputFocusChange}
data-testid="input"
/>
{isVisibilityButtonVisible ? (
<VisibilityButton
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is an icon only button, could we still attach an aria-label to it with "Toggle password visibility" (or the like)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Udpated

iconOnly
onClick={() => setIsContentVisible(!isContentVisible)}
type="button"
data-testid="toggle-password-visibility-button"
>
<FontAwesomeIcon icon={isContentVisible ? faEyeSlash : faEye} fixedWidth />
</VisibilityButton>
) : null}
</InputContainer>
);
};

export default Input;
Expand Down