✨(react) add InputPassword
We had the need to have a built-in password input able to show or hide the password. Closes #301
This commit is contained in:
5
.changeset/flat-guests-matter.md
Normal file
5
.changeset/flat-guests-matter.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@openfun/cunningham-react": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
add InputPassword
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Meta } from "@storybook/react";
|
||||||
|
import { InputPassword } from ":/components/Forms/Input/InputPassword";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Forms/Input",
|
||||||
|
component: InputPassword,
|
||||||
|
} as Meta<typeof InputPassword>;
|
||||||
|
|
||||||
|
export const Password = {
|
||||||
|
args: {
|
||||||
|
label: "Your most secret password",
|
||||||
|
},
|
||||||
|
};
|
||||||
41
packages/react/src/components/Forms/Input/InputPassword.tsx
Normal file
41
packages/react/src/components/Forms/Input/InputPassword.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React, { forwardRef } from "react";
|
||||||
|
import { Input, InputProps } from ":/components/Forms/Input/index";
|
||||||
|
import { Button } from ":/components/Button";
|
||||||
|
import { useCunningham } from ":/components/Provider";
|
||||||
|
|
||||||
|
export const InputPassword = forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
Omit<InputProps, "rightIcon">
|
||||||
|
>((props: InputProps, ref) => {
|
||||||
|
const [showPassword, setShowPassword] = React.useState(false);
|
||||||
|
const { className, ...otherProps } = props;
|
||||||
|
const customClassName = "c__input--password";
|
||||||
|
const { t } = useCunningham();
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
{...otherProps}
|
||||||
|
ref={ref}
|
||||||
|
className={className + " " + customClassName}
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
rightIcon={
|
||||||
|
showPassword ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowPassword(false)}
|
||||||
|
icon={<span className="material-icons">visibility_off</span>}
|
||||||
|
color="tertiary-text"
|
||||||
|
size="small"
|
||||||
|
aria-label={t("components.forms.input.password.hide_password")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowPassword(true)}
|
||||||
|
icon={<span className="material-icons">visibility</span>}
|
||||||
|
color="tertiary-text"
|
||||||
|
size="small"
|
||||||
|
aria-label={t("components.forms.input.password.show_password")}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -133,3 +133,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c__input--password {
|
||||||
|
.c__input__icon-right .material-icons {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Canvas, Meta, Story, Source, ArgTypes } from '@storybook/blocks';
|
import { Canvas, Meta, Story, Source, ArgTypes } from '@storybook/blocks';
|
||||||
import { Input } from "./index";
|
import { Input } from "./index";
|
||||||
import * as Stories from './index.stories';
|
import * as Stories from './index.stories';
|
||||||
|
import * as InputPasswordStories from './InputPassword.stories';
|
||||||
|
|
||||||
<Meta of={Stories}/>
|
<Meta of={Stories}/>
|
||||||
|
|
||||||
@@ -65,6 +66,12 @@ in mind to also define `charCounterMax`.
|
|||||||
|
|
||||||
<Canvas of={Stories.CharCounter} sourceState="shown"/>
|
<Canvas of={Stories.CharCounter} sourceState="shown"/>
|
||||||
|
|
||||||
|
## Password
|
||||||
|
|
||||||
|
You can also use a built-in password input that includes a button to show or hide the password.
|
||||||
|
|
||||||
|
<Canvas of={InputPasswordStories.Password} sourceState="shown"/>
|
||||||
|
|
||||||
## Controlled / Non Controlled
|
## Controlled / Non Controlled
|
||||||
|
|
||||||
Like a native input, you can use the Input component in a controlled or non controlled way. You can see the example below
|
Like a native input, you can use the Input component in a controlled or non controlled way. You can see the example below
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import userEvent from "@testing-library/user-event";
|
|||||||
import { expect } from "vitest";
|
import { expect } from "vitest";
|
||||||
import { Input, InputOnlyProps } from ":/components/Forms/Input/index";
|
import { Input, InputOnlyProps } from ":/components/Forms/Input/index";
|
||||||
import { Button } from ":/components/Button";
|
import { Button } from ":/components/Button";
|
||||||
|
import { InputPassword } from ":/components/Forms/Input/InputPassword";
|
||||||
|
import { CunninghamProvider } from ":/components/Provider";
|
||||||
import { FieldProps } from "../Field";
|
import { FieldProps } from "../Field";
|
||||||
|
|
||||||
const spyError = vi.spyOn(global.console, "error");
|
const spyError = vi.spyOn(global.console, "error");
|
||||||
@@ -242,4 +244,32 @@ describe("<Input/>", () => {
|
|||||||
document.querySelector(".c__field.my-custom-class"),
|
document.querySelector(".c__field.my-custom-class"),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders with className", async () => {
|
||||||
|
render(<Input label="First name" className="my-custom-class" />);
|
||||||
|
expect(
|
||||||
|
document.querySelector(".c__field.my-custom-class"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows to show/hide password", async () => {
|
||||||
|
render(
|
||||||
|
<CunninghamProvider>
|
||||||
|
<InputPassword label="Password" />
|
||||||
|
</CunninghamProvider>,
|
||||||
|
);
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const input: HTMLInputElement = screen.getByLabelText("Password");
|
||||||
|
|
||||||
|
await user.type(input, "azerty");
|
||||||
|
expect(input.type).toEqual("password");
|
||||||
|
|
||||||
|
let button = screen.getByRole("button", { name: "Show password" });
|
||||||
|
await user.click(button);
|
||||||
|
expect(input.type).toEqual("text");
|
||||||
|
|
||||||
|
button = screen.getByRole("button", { name: "Hide password" });
|
||||||
|
await user.click(button);
|
||||||
|
expect(input.type).toEqual("password");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export * from "./components/Forms/DatePicker";
|
|||||||
export * from "./components/Forms/Field";
|
export * from "./components/Forms/Field";
|
||||||
export * from "./components/Forms/FileUploader";
|
export * from "./components/Forms/FileUploader";
|
||||||
export * from "./components/Forms/Input";
|
export * from "./components/Forms/Input";
|
||||||
|
export * from "./components/Forms/Input/InputPassword";
|
||||||
export * from "./components/Forms/Radio";
|
export * from "./components/Forms/Radio";
|
||||||
export * from "./components/Forms/Select";
|
export * from "./components/Forms/Select";
|
||||||
export * from "./components/Forms/Switch";
|
export * from "./components/Forms/Switch";
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
"test": "This is a test: {name}"
|
"test": "This is a test: {name}"
|
||||||
},
|
},
|
||||||
"forms": {
|
"forms": {
|
||||||
|
"input": {
|
||||||
|
"password": {
|
||||||
|
"show_password": "Show password",
|
||||||
|
"hide_password": "Hide password"
|
||||||
|
}
|
||||||
|
},
|
||||||
"select": {
|
"select": {
|
||||||
"toggle_button_aria_label": "Toggle dropdown",
|
"toggle_button_aria_label": "Toggle dropdown",
|
||||||
"clear_button_aria_label": "Clear selection",
|
"clear_button_aria_label": "Clear selection",
|
||||||
|
|||||||
Reference in New Issue
Block a user