diff --git a/packages/react/src/components/Forms/Input/_index.scss b/packages/react/src/components/Forms/Input/_index.scss
index afb19cd..d9490c9 100644
--- a/packages/react/src/components/Forms/Input/_index.scss
+++ b/packages/react/src/components/Forms/Input/_index.scss
@@ -1,3 +1,14 @@
+.c__input__label {
+ display: block;
+ font-size: var(--c--components--forms-labelledbox--classic-label-font-size);
+ color: var(--c--components--forms-labelledbox--label-color--small);
+ margin-bottom: var(--c--components--forms-labelledbox--classic-label-margin-bottom);
+
+ &--disabled {
+ color: var(--c--components--forms-labelledbox--label-color--small--disabled);
+ }
+}
+
.c__input__wrapper {
border-radius: var(--c--components--forms-input--border-radius);
border-width: var(--c--components--forms-input--border-width);
@@ -123,6 +134,11 @@
&--error {
border-color: var(--c--contextuals--border--semantic--error--primary);
}
+
+ &--classic {
+ align-items: center;
+ height: 2.75rem;
+ }
}
.c__input--password {
diff --git a/packages/react/src/components/Forms/Input/index.spec.tsx b/packages/react/src/components/Forms/Input/index.spec.tsx
index 4e222cb..3fb452c 100644
--- a/packages/react/src/components/Forms/Input/index.spec.tsx
+++ b/packages/react/src/components/Forms/Input/index.spec.tsx
@@ -273,4 +273,102 @@ describe("", () => {
await user.click(button);
expect(input.type).toEqual("password");
});
+
+ describe("classic variant", () => {
+ it("renders with classic variant", () => {
+ render();
+ // In classic mode, label is rendered outside the wrapper with its own class
+ expect(document.querySelector(".c__input__label")).toBeInTheDocument();
+ expect(screen.getByText("First name")).toBeInTheDocument();
+ });
+
+ it("label is always static in classic variant", async () => {
+ const user = userEvent.setup();
+ render(
+
+
+
+
,
+ );
+
+ const input: HTMLInputElement = screen.getByRole("textbox", {
+ name: "First name",
+ });
+ const label = screen.getByText("First name");
+
+ // In classic variant, label is outside the wrapper and has c__input__label class
+ expect(label.classList.contains("c__input__label")).toBe(true);
+
+ // Focusing should not change anything
+ await user.click(input);
+ expect(label.classList.contains("c__input__label")).toBe(true);
+
+ // Typing should not change anything
+ await user.type(input, "John");
+ expect(label.classList.contains("c__input__label")).toBe(true);
+ });
+
+ it("shows placeholder in classic variant", () => {
+ render(
+ ,
+ );
+ const input: HTMLInputElement = screen.getByRole("textbox", {
+ name: "First name",
+ });
+ expect(input.placeholder).toEqual("Enter your first name");
+ });
+
+ it("ignores placeholder in floating variant", () => {
+ render(
+ ,
+ );
+ const input: HTMLInputElement = screen.getByRole("textbox", {
+ name: "First name",
+ });
+ expect(input.placeholder).toEqual("");
+ });
+
+ it("defaults to floating variant (placeholder ignored)", () => {
+ render();
+ const input: HTMLInputElement = screen.getByRole("textbox", {
+ name: "First name",
+ });
+ expect(input.placeholder).toEqual("");
+ expect(
+ document.querySelector(".c__input__label"),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe("hideLabel", () => {
+ it("hides label visually but keeps it accessible in floating variant", () => {
+ render();
+ const input = screen.getByRole("textbox", { name: "First name" });
+ expect(input).toBeInTheDocument();
+ // Label should be visually hidden via LabelledBox
+ const label = screen.getByText("First name");
+ expect(label.closest("label")).toHaveClass("c__offscreen");
+ });
+
+ it("hides label visually but keeps it accessible in classic variant", () => {
+ render();
+ const input = screen.getByRole("textbox", { name: "First name" });
+ expect(input).toBeInTheDocument();
+ // Label should be visually hidden with c__offscreen class
+ const label = screen.getByText("First name");
+ expect(label).toHaveClass("c__offscreen");
+ // The visible label class should not be present
+ expect(
+ document.querySelector(".c__input__label"),
+ ).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/packages/react/src/components/Forms/Input/index.stories.tsx b/packages/react/src/components/Forms/Input/index.stories.tsx
index a22d75f..a7fa577 100644
--- a/packages/react/src/components/Forms/Input/index.stories.tsx
+++ b/packages/react/src/components/Forms/Input/index.stories.tsx
@@ -22,6 +22,10 @@ export default {
control: "select",
options: ["default", "success", "error"],
},
+ variant: {
+ control: "select",
+ options: ["floating", "classic"],
+ },
},
} as Meta;
@@ -253,6 +257,71 @@ export const FormExample = () => {
);
};
+export const ClassicVariant = {
+ args: {
+ label: "Your name",
+ variant: "classic",
+ placeholder: "Enter your name",
+ },
+};
+
+export const ClassicVariantFilled = {
+ args: {
+ label: "Your name",
+ variant: "classic",
+ placeholder: "Enter your name",
+ defaultValue: "John Doe",
+ },
+};
+
+export const ClassicVariantWithIcon = {
+ args: {
+ label: "Your email",
+ variant: "classic",
+ placeholder: "Enter your email address",
+ icon: email,
+ },
+};
+
+export const ClassicVariantDisabled = {
+ args: {
+ label: "Your name",
+ variant: "classic",
+ placeholder: "Enter your name",
+ disabled: true,
+ },
+};
+
+export const ClassicVariantError = {
+ args: {
+ label: "Your email",
+ variant: "classic",
+ placeholder: "Enter your email",
+ defaultValue: "invalid-email",
+ state: "error",
+ text: "Please enter a valid email address",
+ },
+};
+
+export const HiddenLabel = {
+ args: {
+ label: "Search",
+ hideLabel: true,
+ placeholder: "Search...",
+ icon: search,
+ },
+};
+
+export const HiddenLabelClassic = {
+ args: {
+ label: "Search",
+ variant: "classic",
+ hideLabel: true,
+ placeholder: "Search...",
+ icon: search,
+ },
+};
+
export const ReactHookForm = () => {
interface InputExampleFormValues {
email: string;
diff --git a/packages/react/src/components/Forms/Input/index.tsx b/packages/react/src/components/Forms/Input/index.tsx
index 4178a36..546b90a 100644
--- a/packages/react/src/components/Forms/Input/index.tsx
+++ b/packages/react/src/components/Forms/Input/index.tsx
@@ -10,9 +10,13 @@ import classNames from "classnames";
import { randomString } from ":/utils";
import { Field, FieldProps } from ":/components/Forms/Field";
import { LabelledBox } from ":/components/Forms/LabelledBox";
+import { ClassicLabel } from ":/components/Forms/ClassicLabel";
+import type { FieldVariant } from ":/components/Forms/types";
export type InputOnlyProps = {
label?: string;
+ variant?: FieldVariant;
+ hideLabel?: boolean;
icon?: ReactNode;
rightIcon?: ReactNode;
charCounter?: boolean;
@@ -28,6 +32,8 @@ export const Input = ({
className,
defaultValue,
label,
+ variant = "floating",
+ hideLabel,
id,
icon,
rightIcon,
@@ -36,6 +42,7 @@ export const Input = ({
ref,
...props
}: InputProps) => {
+ const isClassic = variant === "classic";
const classes = ["c__input"];
const inputRef = useRef(null);
const [inputFocus, setInputFocus] = useState(false);
@@ -46,16 +53,12 @@ export const Input = ({
? `${value.toString().length}/${charCounterMax}`
: props.rightText;
- const updateLabel = () => {
+ useEffect(() => {
if (inputFocus) {
setLabelAsPlaceholder(false);
return;
}
setLabelAsPlaceholder(!value);
- };
-
- useEffect(() => {
- updateLabel();
}, [inputFocus, value]);
// If the input is used as a controlled component, we need to update the local value.
@@ -76,8 +79,51 @@ export const Input = ({
...inputProps
} = props;
+ const inputElement = (
+ {
+ setInputFocus(true);
+ props.onFocus?.(e);
+ }}
+ onBlur={(e) => {
+ setInputFocus(false);
+ props.onBlur?.(e);
+ }}
+ onChange={(e) => {
+ setValue(e.target.value);
+ props.onChange?.(e);
+ }}
+ ref={(inputTextRef) => {
+ if (ref) {
+ if (typeof ref === "function") {
+ ref(inputTextRef);
+ } else {
+ ref.current = inputTextRef;
+ }
+ }
+ inputRef.current = inputTextRef;
+ }}
+ />
+ );
+
return (
+ {isClassic && (
+
+ )}
{/* We disabled linting for this specific line because we consider that the onClick props is only used for */}
{/* mouse users, so this do not engender any issue for accessibility. */}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
@@ -87,6 +133,7 @@ export const Input = ({
props.state && "c__input__wrapper--" + props.state,
{
"c__input__wrapper--disabled": props.disabled,
+ "c__input__wrapper--classic": isClassic,
},
)}
onClick={() => {
@@ -94,42 +141,20 @@ export const Input = ({
}}
>
{!!icon && {icon}
}
-
- {
- setInputFocus(true);
- props.onFocus?.(e);
- }}
- onBlur={(e) => {
- setInputFocus(false);
- props.onBlur?.(e);
- }}
- onChange={(e) => {
- setValue(e.target.value);
- props.onChange?.(e);
- }}
- ref={(inputTextRef) => {
- if (ref) {
- if (typeof ref === "function") {
- ref(inputTextRef);
- } else {
- ref.current = inputTextRef;
- }
- }
- inputRef.current = inputTextRef;
- }}
- />
-
+ {isClassic ? (
+ inputElement
+ ) : (
+
+ {inputElement}
+
+ )}
{!!rightIcon && {rightIcon}
}