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}
}