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

Created PasswordInput component with visibility button (eye icon) #750

Merged
merged 49 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
6ffd9f7
added show button to the WiFi password input
balsa-asanovic Sep 13, 2023
c297a64
added description to changes file
balsa-asanovic Sep 13, 2023
7c3dd11
added icon-size-15 class to utilities.scss
balsa-asanovic Sep 14, 2023
e4e1cf4
imported visibility icons to Icon component
balsa-asanovic Sep 14, 2023
772a34e
switched from text to icon inside the button
balsa-asanovic Sep 14, 2023
948782b
updated changes file
balsa-asanovic Sep 14, 2023
30ccb0e
updated changes file
balsa-asanovic Sep 14, 2023
2aacf8f
added desc to changes file
balsa-asanovic Sep 14, 2023
1424e61
updated changes file to resolve conflict
balsa-asanovic Sep 14, 2023
05ee9f1
Merge branch 'openSUSE:master' into wifi-password-show-button
balsa-asanovic Sep 14, 2023
4266493
added desc to changes file
balsa-asanovic Sep 14, 2023
61e0764
moved visibility lines above warning in Icon comp
balsa-asanovic Sep 15, 2023
569c823
removed label attribute from TextInput
balsa-asanovic Sep 15, 2023
e57a463
removed label property from Button
balsa-asanovic Sep 15, 2023
580d410
added variable to track type of visibility icon
balsa-asanovic Sep 15, 2023
1d7491d
centering eye icon of the visibility button
balsa-asanovic Sep 15, 2023
09a58a7
added newline at the end of file
balsa-asanovic Sep 15, 2023
2213fbf
created PasswordInput component
balsa-asanovic Sep 18, 2023
d7c5e6a
added export for PasswordInput in index.js
balsa-asanovic Sep 18, 2023
36da61f
switched in WifiConnectionForm to use PasswordInput
balsa-asanovic Sep 18, 2023
468b3fa
Merge branch 'openSUSE:master' into wifi-password-show-button
balsa-asanovic Sep 18, 2023
88e7e67
Merge branch 'wifi-password-show-button' of https://github.com/balsa-…
balsa-asanovic Sep 18, 2023
0317fc9
added validated and isDisabled props
balsa-asanovic Sep 18, 2023
a51464b
switched to using PasswordInput component
balsa-asanovic Sep 18, 2023
3dfac77
added autoFocus prop to PasswordInput
balsa-asanovic Sep 18, 2023
1c007f4
switched to using PasswordInput component
balsa-asanovic Sep 18, 2023
2c282e0
added onBlur prop to PasswordInput
balsa-asanovic Sep 18, 2023
bff2400
switched to using PasswordInput component
balsa-asanovic Sep 18, 2023
e375121
added FormGroup node to PasswordInput comp
balsa-asanovic Sep 18, 2023
f4bd01a
removed FormGroup from password input part
balsa-asanovic Sep 18, 2023
5af26c3
added unit tests for PasswordInput component
balsa-asanovic Sep 18, 2023
97f55fa
updated changes file
balsa-asanovic Sep 18, 2023
7aba261
adjusted description in changes file
balsa-asanovic Sep 19, 2023
a4a356d
updated changelog
balsa-asanovic Sep 19, 2023
35cfb05
Merge branch 'openSUSE:master' into wifi-password-show-button
balsa-asanovic Sep 19, 2023
328f83c
removed FormGroup, simplifed props
balsa-asanovic Sep 19, 2023
2d74e8b
removed split prop and div wrapper for it
balsa-asanovic Sep 19, 2023
b17730f
reverted to using FormGroup
balsa-asanovic Sep 19, 2023
107e20f
adjusted test considering simplifed component
balsa-asanovic Sep 19, 2023
7c401ff
adjusted description of css comment
balsa-asanovic Sep 19, 2023
17217c7
changelog
balsa-asanovic Sep 19, 2023
233226a
Merge branch 'wifi-password-show-button' of https://github.com/balsa-…
balsa-asanovic Sep 19, 2023
370d52e
missing semicolon
balsa-asanovic Sep 19, 2023
0346c08
added className to visibility button
balsa-asanovic Sep 19, 2023
2341ca5
changed CSS override to target class instead of id
balsa-asanovic Sep 19, 2023
06e6197
year typo correction
balsa-asanovic Sep 19, 2023
578be45
added unit test for onChange callback
balsa-asanovic Sep 19, 2023
7e97287
corrected eslinit issues in test file
balsa-asanovic Sep 20, 2023
6f12cf0
added documention data to component
balsa-asanovic Sep 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions web/package/cockpit-agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
-------------------------------------------------------------------
Tue Sep 19 19:11:12 UTC 2023 - Balsa Asanovic <[email protected]>

- Allow users to show password values (gh#openSUSE/agama#750).

-------------------------------------------------------------------
Tue Sep 19 11:18:05 UTC 2023 - José Iván López González <[email protected]>

Expand Down
5 changes: 5 additions & 0 deletions web/src/assets/styles/patternfly-overrides.scss
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,8 @@ table td > .pf-c-empty-state {
outline: none;
box-shadow: 0 0 0 1px var(--focus-color);
}

// Center icon in the visibility button of password input form fields
.password-toggler span.pf-c-button__icon {
display: flex;
}
5 changes: 5 additions & 0 deletions web/src/assets/styles/utilities.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
height: 14px;
}

.icon-size-15 {
width: 15px;
height: 15px;
}

.icon-size-16 {
width: 16px;
height: 16px;
Expand Down
18 changes: 7 additions & 11 deletions web/src/components/core/PasswordAndConfirmationInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,11 @@
*/

import React, { useState } from "react";
import {
FormGroup,
TextInput
} from "@patternfly/react-core";
import { FormGroup } from "@patternfly/react-core";
import { PasswordInput } from "~/components/core";
import { _ } from "~/i18n";

const PasswordAndConfirmationInput = ({ value, onChange, onValidation, isDisabled, split = false }) => {
const PasswordAndConfirmationInput = ({ value, onChange, onValidation, isDisabled }) => {
const [confirmation, setConfirmation] = useState(value || "");
const [error, setError] = useState("");

Expand Down Expand Up @@ -54,12 +52,11 @@ const PasswordAndConfirmationInput = ({ value, onChange, onValidation, isDisable
};

return (
<div className={split ? "split" : "stack"}>
<>
<FormGroup fieldId="password" label={_("Password")}>
<TextInput
<PasswordInput
id="password"
name="password"
type="password"
aria-label={_("User password")}
value={value}
isDisabled={isDisabled}
Expand All @@ -73,10 +70,9 @@ const PasswordAndConfirmationInput = ({ value, onChange, onValidation, isDisable
helperTextInvalid={error}
validated={error === "" ? "default" : "error"}
>
<TextInput
<PasswordInput
id="passwordConfirmation"
name="passwordConfirmation"
type="password"
aria-label={_("User password confirmation")}
value={confirmation}
isDisabled={isDisabled}
Expand All @@ -85,7 +81,7 @@ const PasswordAndConfirmationInput = ({ value, onChange, onValidation, isDisable
validated={error === "" ? "default" : "error"}
/>
</FormGroup>
</div>
</>
);
};

Expand Down
67 changes: 67 additions & 0 deletions web/src/components/core/PasswordInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) [2023] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of version 2 of the GNU General Public License as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, contact SUSE LLC.
*
* To contact SUSE LLC about this file by physical or electronic mail, you may
* find current contact information at www.suse.com.
*/

/**
* Renders a password input field and a toggle button that can be used to reveal
* and hide the password
* @component
*
* @param {string} id - the identifier for the field.
* @param {Object} props - props matching the {@link https://www.patternfly.org/components/forms/text-input PF/TextInput},
* except `type` that will be ignored.
*/
import React, { useState } from "react";
import {
Button,
InputGroup,
TextInput
} from "@patternfly/react-core";
import { Icon } from "~/components/layout";
import { _ } from "~/i18n";

export default function PasswordInput({ id, ...props }) {
const [showPassword, setShowPassword] = useState(false);
const visibilityIconName = showPassword ? "visibility_off" : "visibility";
dgdavid marked this conversation as resolved.
Show resolved Hide resolved

if (!id) {
const field = props.label || props["aria-label"] || props.name;
console.error(`The PasswordInput component must have an 'id' but it was not given for '${field}'`);
}

return (
<InputGroup>
<TextInput
{...props}
dgdavid marked this conversation as resolved.
Show resolved Hide resolved
id={id}
type={showPassword ? 'text' : 'password'}
/>
<Button
id={`toggle-${id}-visibility`}
className="password-toggler"
aria-label={_("Password visibility button")}
variant="control"
onClick={() => setShowPassword((prev) => !prev)}
icon={<Icon name={visibilityIconName} size="15" />}
isDisabled={props.isDisabled}
/>
</InputGroup>
);
}
97 changes: 97 additions & 0 deletions web/src/components/core/PasswordInput.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright (c) [2023] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of version 2 of the GNU General Public License as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, contact SUSE LLC.
*
* To contact SUSE LLC about this file by physical or electronic mail, you may
* find current contact information at www.suse.com.
*/

import React, { useState } from "react";
import { screen } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import userEvent from "@testing-library/user-event";
import PasswordInput from "./PasswordInput";
import { _ } from "~/i18n";

describe("PasswordInput Component", () => {
it("renders a password input", () => {
plainRender(
<PasswordInput
id="password"
name="password"
aria-label={_("User password")}
/>
);

const inputField = screen.getByLabelText("User password");
expect(inputField).toHaveAttribute("type", "password");
});

it("allows revealing the password", async () => {
plainRender(
<PasswordInput
id="password"
name="password"
aria-label={_("User password")}
/>
);

const passwordInput = screen.getByLabelText("User password");
const button = screen.getByRole("button");

expect(passwordInput).toHaveAttribute("type", "password");
await userEvent.click(button);
expect(passwordInput).toHaveAttribute("type", "text");
});

balsa-asanovic marked this conversation as resolved.
Show resolved Hide resolved
it("applies autoFocus behavior correctly", () => {
plainRender(
<PasswordInput
autoFocus
id="password"
name="password"
aria-label={_("User password")}
/>
);

const inputField = screen.getByLabelText("User password");
expect(document.activeElement).toBe(inputField);
});

// Using a controlled component for testing the rendered result instead of testing if
// the given onChange callback is called. The former is more aligned with the
// React Testing Library principles, https://testing-library.com/docs/guiding-principles/
const PasswordInputTest = (props) => {
const [password, setPassword] = useState(null);

return (
<>
<PasswordInput {...props} onChange={setPassword} />
{password && <p>Password value updated!</p>}
</>
);
};

it("triggers onChange callback", async () => {
const { user } = plainRender(<PasswordInputTest id="test-password" aria-label="Test password" />);
const passwordInput = screen.getByLabelText("Test password");

expect(screen.queryByText("Password value updated!")).toBeNull();
await user.type(passwordInput, "secret");

screen.getByText("Password value updated!");
});
});
1 change: 1 addition & 0 deletions web/src/components/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@ export { default as Tip } from "./Tip";
export { default as ShowTerminalButton } from "./ShowTerminalButton";
export { default as NotificationMark } from "./NotificationMark";
export { default as NumericTextInput } from "./NumericTextInput";
export { default as PasswordInput } from "./PasswordInput";
4 changes: 4 additions & 0 deletions web/src/components/layout/Icon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ import Terminal from "@icons/terminal.svg?component";
import Translate from "@icons/translate.svg?component";
import Tune from "@icons/tune.svg?component";
import Warning from "@icons/warning.svg?component";
import Visibility from "@icons/visibility.svg?component";
import VisibilityOff from "@icons/visibility_off.svg?component";
import Wifi from "@icons/wifi.svg?component";
import WifiFind from "@icons/wifi_find.svg?component";

Expand Down Expand Up @@ -117,6 +119,8 @@ const icons = {
terminal: Terminal,
translate: Translate,
tune: Tune,
visibility: Visibility,
visibility_off: VisibilityOff,
dgdavid marked this conversation as resolved.
Show resolved Hide resolved
warning: Warning,
wifi: Wifi,
wifi_find: WifiFind,
Expand Down
5 changes: 2 additions & 3 deletions web/src/components/network/WifiConnectionForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from "@patternfly/react-core";
import { useInstallerClient } from "~/context/installer";
import { _ } from "~/i18n";
import { PasswordInput } from "~/components/core";

/*
* FIXME: it should be moved to the SecurityProtocols enum that already exists or to a class based
Expand Down Expand Up @@ -118,13 +119,11 @@ export default function WifiConnectionForm({ network, onCancel, onSubmitCallback
{ security === "wpa-psk" &&
// TRANSLATORS: WiFi password
<FormGroup fieldId="password" label={_("WPA Password")}>
<TextInput
<PasswordInput
id="password"
name="password"
aria-label={_("Password")}
value={password}
label={_("Password")}
type="password"
onChange={setPassword}
/>
</FormGroup> }
Expand Down
9 changes: 4 additions & 5 deletions web/src/components/questions/LuksActivationQuestion.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
*/

import React, { useState } from "react";
import { Alert, Form, FormGroup, Text, TextInput } from "@patternfly/react-core";
import { Alert, Form, FormGroup, Text } from "@patternfly/react-core";
import { Icon } from "~/components/layout";
import { Popup } from "~/components/core";
import { PasswordInput, Popup } from "~/components/core";
import { QuestionActions } from "~/components/questions";
import { _ } from "~/i18n";

Expand Down Expand Up @@ -65,13 +65,12 @@ export default function LuksActivationQuestion({ question, answerCallback }) {
{ question.text }
</Text>
<Form onSubmit={triggerDefaultAction}>
{/* TRANSLATORS: field label */}
{ /* TRANSLATORS: field label */ }
<FormGroup label={_("Encryption Password")} fieldId="luks-password">
<TextInput
<PasswordInput
autoFocus
id="luks-password"
value={password}
type="password"
onChange={setPassword}
/>
</FormGroup>
Expand Down
10 changes: 3 additions & 7 deletions web/src/components/storage/iscsi/AuthFields.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import React, { useEffect } from "react";
import { FormGroup, TextInput } from "@patternfly/react-core";

import { Fieldset } from "~/components/core";
import { Fieldset, PasswordInput } from "~/components/core";
import { Icon } from "~/components/layout";
import { _ } from "~/i18n";

Expand Down Expand Up @@ -97,13 +97,11 @@ export default function AuthFields({ data, onChange, onValidate }) {
helperTextInvalid={_("Incorrect password")}
validated={showPasswordError() ? "error" : "default"}
>
<TextInput
<PasswordInput
id="password"
name="password"
type="password"
aria-label={_("Password")}
value={data.password || ""}
label={_("Password")}
onChange={onPasswordChange}
validated={showPasswordError() ? "error" : "default"}
/>
Expand Down Expand Up @@ -134,13 +132,11 @@ export default function AuthFields({ data, onChange, onValidate }) {
helperTextInvalid={_("Incorrect password")}
validated={showReversePasswordError() ? "error" : "default"}
>
<TextInput
<PasswordInput
id="reversePassword"
name="reversePassword"
type="password"
aria-label={_("Target Password")}
value={data.reversePassword || ""}
label={_("Password")}
isDisabled={!isValidAuth()}
onChange={onReversePasswordChange}
validated={showReversePasswordError() ? "error" : "default"}
Expand Down
Loading