2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
import type { ReactElement, Ref } from "react";
import React, { forwardRef } from "react";
import type { FieldValues, SubmitHandler, UseFormReturn } from "react-hook-form";
import { FormProvider } from "react-hook-form";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { showToast } from "../../..";
type FormProps<T extends object> = { form: UseFormReturn<T>; handleSubmit: SubmitHandler<T> } & Omit<
JSX.IntrinsicElements["form"],
"onSubmit"
>;
const PlainForm = <T extends FieldValues>(props: FormProps<T>, ref: Ref<HTMLFormElement>) => {
const { form, handleSubmit, ...passThrough } = props;
return (
<FormProvider {...form}>
<form
ref={ref}
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
form
.handleSubmit(handleSubmit)(event)
.catch((err) => {
// FIXME: Booking Pages don't have toast, so this error is never shown
showToast(`${getErrorFromUnknown(err).message}`, "error");
});
}}
{...passThrough}>
{props.children}
</form>
</FormProvider>
);
};
export const Form = forwardRef(PlainForm) as <T extends FieldValues>(
p: FormProps<T> & { ref?: Ref<HTMLFormElement> }
) => ReactElement;

View File

@@ -0,0 +1,122 @@
import type { FieldValues } from "react-hook-form";
import { useFormContext } from "react-hook-form";
import { Icon } from "../../..";
import { InputError } from "./InputError";
type hintsOrErrorsProps = {
hintErrors?: string[];
fieldName: string;
t: (key: string) => string;
};
export function HintsOrErrors<T extends FieldValues = FieldValues>({
hintErrors,
fieldName,
t,
}: hintsOrErrorsProps) {
const methods = useFormContext() as ReturnType<typeof useFormContext> | null;
/* If there's no methods it means we're using these components outside a React Hook Form context */
if (!methods) return null;
const { formState } = methods;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const fieldErrors: FieldErrors<T> | undefined = formState.errors[fieldName];
if (!hintErrors && fieldErrors && !fieldErrors.message) {
// no hints passed, field errors exist and they are custom ones
return (
<div className="text-gray text-default mt-2 flex items-center text-sm">
<ul className="ml-2">
{Object.keys(fieldErrors).map((key: string) => {
return (
<li key={key} className="text-blue-700">
{t(`${fieldName}_hint_${key}`)}
</li>
);
})}
</ul>
</div>
);
}
if (hintErrors && fieldErrors) {
// hints passed, field errors exist
return (
<div className="text-gray text-default mt-2 flex items-center text-sm">
<ul className="ml-2">
{hintErrors.map((key: string) => {
const submitted = formState.isSubmitted;
const error = fieldErrors[key] || fieldErrors.message;
return (
<li
key={key}
data-testid="hint-error"
className={error !== undefined ? (submitted ? "text-red-500" : "") : "text-green-600"}>
{error !== undefined ? (
submitted ? (
<Icon
name="x"
size="12"
strokeWidth="3"
className="-ml-1 inline-block ltr:mr-2 rtl:ml-2"
/>
) : (
<Icon
name="circle"
fill="currentColor"
size="5"
className="inline-block ltr:mr-2 rtl:ml-2"
/>
)
) : (
<Icon
name="check"
size="12"
strokeWidth="3"
className="-ml-1 inline-block ltr:mr-2 rtl:ml-2"
/>
)}
{t(`${fieldName}_hint_${key}`)}
</li>
);
})}
</ul>
</div>
);
}
// errors exist, not custom ones, just show them as is
if (fieldErrors) {
return <InputError message={fieldErrors.message} />;
}
if (!hintErrors) return null;
// hints passed, no errors exist, proceed to just show hints
return (
<div className="text-gray text-default mt-2 flex items-center text-sm">
<ul className="ml-2">
{hintErrors.map((key: string) => {
// if field was changed, as no error exist, show checked status and color
const dirty = formState.dirtyFields[fieldName];
return (
<li key={key} className={!!dirty ? "text-green-600" : ""}>
{!!dirty ? (
<Icon
name="check"
size="12"
strokeWidth="3"
className="-ml-1 inline-block ltr:mr-2 rtl:ml-2"
/>
) : (
<Icon name="circle" fill="currentColor" size="5" className="inline-block ltr:mr-2 rtl:ml-2" />
)}
{t(`${fieldName}_hint_${key}`)}
</li>
);
})}
</ul>
</div>
);
}

View File

@@ -0,0 +1,200 @@
import type { ReactNode } from "react";
import React, { forwardRef, useCallback, useId, useState } from "react";
import { useFormContext } from "react-hook-form";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert, Icon, Input, InputField, Tooltip } from "../../..";
import { Label } from "./Label";
import type { InputFieldProps } from "./types";
export function InputLeading(props: JSX.IntrinsicElements["div"]) {
return (
<span className="bg-muted border-default text-subtle inline-flex flex-shrink-0 items-center rounded-l-sm border px-3 ltr:border-r-0 rtl:border-l-0 sm:text-sm sm:leading-4">
{props.children}
</span>
);
}
export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(function PasswordField(
props,
ref
) {
const { t } = useLocale();
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const toggleIsPasswordVisible = useCallback(
() => setIsPasswordVisible(!isPasswordVisible),
[isPasswordVisible, setIsPasswordVisible]
);
const textLabel = isPasswordVisible ? t("hide_password") : t("show_password");
return (
<InputField
type={isPasswordVisible ? "text" : "password"}
placeholder={props.placeholder || "•••••••••••••"}
ref={ref}
{...props}
className={classNames(
"addon-wrapper mb-0 ltr:border-r-0 ltr:pr-10 rtl:border-l-0 rtl:pl-10",
props.className
)}
addOnFilled={false}
addOnSuffix={
<Tooltip content={textLabel}>
<button
className="text-emphasis h-9"
tabIndex={-1}
type="button"
onClick={() => toggleIsPasswordVisible()}>
{isPasswordVisible ? (
<Icon name="eye-off" className="h-4 stroke-[2.5px]" />
) : (
<Icon name="eye" className="h-4 stroke-[2.5px]" />
)}
<span className="sr-only">{textLabel}</span>
</button>
</Tooltip>
}
/>
);
});
export const EmailInput = forwardRef<HTMLInputElement, InputFieldProps>(function EmailInput(props, ref) {
return (
<Input
ref={ref}
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
inputMode="email"
{...props}
/>
);
});
export const EmailField = forwardRef<HTMLInputElement, InputFieldProps>(function EmailField(props, ref) {
return (
<InputField
ref={ref}
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
inputMode="email"
{...props}
/>
);
});
type TextAreaProps = JSX.IntrinsicElements["textarea"];
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function TextAreaInput(props, ref) {
return (
<textarea
ref={ref}
{...props}
className={classNames(
"hover:border-emphasis border-default bg-default placeholder:text-muted text-emphasis disabled:hover:border-default disabled:bg-subtle focus:ring-brand-default focus:border-subtle mb-2 block w-full rounded-md border px-3 py-2 text-sm transition focus:outline-none focus:ring-2 focus:ring-offset-1 disabled:cursor-not-allowed",
props.className
)}
/>
);
});
type TextAreaFieldProps = {
label?: ReactNode;
t?: (key: string) => string;
} & React.ComponentProps<typeof TextArea> & {
name: string;
labelProps?: React.ComponentProps<typeof Label>;
};
export const TextAreaField = forwardRef<HTMLTextAreaElement, TextAreaFieldProps>(function TextField(
props,
ref
) {
const id = useId();
const { t: _t } = useLocale();
const t = props.t || _t;
const methods = useFormContext();
const {
label = t(props.name as string),
labelProps,
/** Prevents displaying untranslated placeholder keys */
placeholder = t(`${props.name}_placeholder`) !== `${props.name}_placeholder`
? `${props.name}_placeholder`
: "",
...passThrough
} = props;
return (
<div>
{!!props.name && (
<Label htmlFor={id} {...labelProps}>
{label}
</Label>
)}
<TextArea ref={ref} placeholder={placeholder} {...passThrough} />
{methods?.formState?.errors[props.name]?.message && (
<Alert
className="mt-1"
severity="error"
message={<>{methods.formState.errors[props.name]?.message}</>}
/>
)}
</div>
);
});
export function FieldsetLegend(props: JSX.IntrinsicElements["legend"]) {
return (
<legend {...props} className={classNames("text-default text-sm font-medium leading-4", props.className)}>
{props.children}
</legend>
);
}
export function InputGroupBox(props: JSX.IntrinsicElements["div"]) {
return (
<div
{...props}
className={classNames("bg-default border-default space-y-2 rounded-sm border p-2", props.className)}>
{props.children}
</div>
);
}
export const NumberInput = forwardRef<HTMLInputElement, InputFieldProps>(function NumberInput(props, ref) {
return (
<Input
ref={ref}
type="number"
autoCapitalize="none"
autoComplete="numeric"
autoCorrect="off"
inputMode="numeric"
{...props}
/>
);
});
export const FilterSearchField = forwardRef<HTMLInputElement, InputFieldProps>(function TextField(
props,
ref
) {
return (
<div
dir="ltr"
className="focus-within:ring-brand-default group relative mx-3 mb-1 mt-2.5 flex items-center rounded-md focus-within:outline-none focus-within:ring-2">
<div className="addon-wrapper border-default [input:hover_+_&]:border-emphasis [input:hover_+_&]:border-l-default [&:has(+_input:hover)]:border-emphasis [&:has(+_input:hover)]:border-r-default flex h-7 items-center justify-center rounded-l-md border border-r-0">
<Icon name="search" className="ms-3 h-4 w-4" data-testid="search-icon" />
</div>
<Input
ref={ref}
className="disabled:bg-subtle disabled:hover:border-subtle !my-0 h-7 rounded-l-none border-l-0 !ring-0 disabled:cursor-not-allowed"
{...props}
/>
</div>
);
});

View File

@@ -0,0 +1,14 @@
import { Icon } from "../../..";
type InputErrorProp = {
message: string;
};
export const InputError = ({ message }: InputErrorProp) => (
<div data-testid="field-error" className="text-gray mt-2 flex items-center gap-x-2 text-sm text-red-700">
<div>
<Icon name="info" className="h-3 w-3" />
</div>
<p>{message}</p>
</div>
);

View File

@@ -0,0 +1,19 @@
import React, { forwardRef } from "react";
import { InputField, UnstyledSelect } from "../../..";
import type { InputFieldProps } from "./types";
export const InputFieldWithSelect = forwardRef<
HTMLInputElement,
InputFieldProps & { selectProps: typeof UnstyledSelect }
>(function EmailField(props, ref) {
return (
<InputField
ref={ref}
{...props}
inputIsFullWidth={false}
addOnClassname="!px-0"
addOnSuffix={<UnstyledSelect {...props.selectProps} />}
/>
);
});

View File

@@ -0,0 +1,15 @@
import { classNames } from "@calcom/lib";
export function Label(props: JSX.IntrinsicElements["label"]) {
const { className, ...restProps } = props;
return (
<label
className={classNames(
"text-default text-emphasis mb-2 block text-sm font-medium leading-none",
className
)}
{...restProps}>
{props.children}
</label>
);
}

View File

@@ -0,0 +1,193 @@
import React, { forwardRef, useId, useState } from "react";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon, Skeleton } from "../../..";
import { HintsOrErrors } from "./HintOrErrors";
import { Label } from "./Label";
import type { InputFieldProps, InputProps } from "./types";
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ isFullWidth = true, ...props },
ref
) {
return (
<input
{...props}
ref={ref}
className={classNames(
"hover:border-emphasis dark:focus:border-emphasis border-default bg-default placeholder:text-muted text-emphasis disabled:hover:border-default disabled:bg-subtle focus:ring-brand-default focus:border-subtle mb-2 block h-9 rounded-md border px-3 py-2 text-sm leading-4 transition focus:outline-none focus:ring-2 disabled:cursor-not-allowed",
isFullWidth && "w-full",
props.className
)}
/>
);
});
type AddonProps = {
children: React.ReactNode;
isFilled?: boolean;
className?: string;
error?: boolean;
onClickAddon?: () => void;
};
const Addon = ({ isFilled, children, className, error, onClickAddon }: AddonProps) => (
<div
onClick={onClickAddon && onClickAddon}
className={classNames(
"addon-wrapper border-default [input:hover_+_&]:border-emphasis [input:hover_+_&]:border-l-default [&:has(+_input:hover)]:border-emphasis [&:has(+_input:hover)]:border-r-default h-9 border px-3",
isFilled && "bg-subtle",
onClickAddon && "cursor-pointer disabled:hover:cursor-not-allowed",
className
)}>
<div
className={classNames(
"min-h-9 flex flex-col justify-center text-sm leading-7",
error ? "text-error" : "text-default"
)}>
<span
className="flex max-w-2xl overflow-y-auto whitespace-nowrap"
style={{
WebkitOverflowScrolling: "touch",
scrollbarWidth: "none",
overflow: "-ms-scroll-chaining",
msOverflowStyle: "-ms-autohiding-scrollbar",
}}>
{children}
</span>
</div>
</div>
);
export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputField(props, ref) {
const id = useId();
const { t: _t, isLocaleReady, i18n } = useLocale();
const t = props.t || _t;
const name = props.name || "";
const {
label = t(name),
labelProps,
labelClassName,
disabled,
LockedIcon,
placeholder = isLocaleReady && i18n.exists(`${name}_placeholder`) ? t(`${name}_placeholder`) : "",
className,
addOnLeading,
addOnSuffix,
addOnFilled = true,
addOnClassname,
inputIsFullWidth,
hint,
type,
hintErrors,
labelSrOnly,
containerClassName,
readOnly,
showAsteriskIndicator,
onClickAddon,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
t: __t,
dataTestid,
...passThrough
} = props;
const [inputValue, setInputValue] = useState<string>("");
return (
<div className={classNames(containerClassName)}>
{!!name && (
<Skeleton
as={Label}
htmlFor={id}
loadingClassName="w-16"
{...labelProps}
className={classNames(labelClassName, labelSrOnly && "sr-only", props.error && "text-error")}>
{label}
{showAsteriskIndicator && !readOnly && passThrough.required ? (
<span className="text-default ml-1 font-medium">*</span>
) : null}
{LockedIcon}
</Skeleton>
)}
{addOnLeading || addOnSuffix ? (
<div
dir="ltr"
className="focus-within:ring-brand-default group relative mb-1 flex items-center rounded-md transition focus-within:outline-none focus-within:ring-2">
{addOnLeading && (
<Addon
isFilled={addOnFilled}
className={classNames("ltr:rounded-l-md rtl:rounded-r-md", addOnClassname)}>
{addOnLeading}
</Addon>
)}
<Input
data-testid={`${dataTestid}-input` ?? "input-field"}
id={id}
type={type}
placeholder={placeholder}
isFullWidth={inputIsFullWidth}
className={classNames(
className,
"disabled:bg-subtle disabled:hover:border-subtle disabled:cursor-not-allowed",
addOnLeading && "rounded-l-none border-l-0",
addOnSuffix && "rounded-r-none border-r-0",
type === "search" && "pr-8",
"!my-0 !ring-0"
)}
{...passThrough}
{...(type == "search" && {
onChange: (e) => {
setInputValue(e.target.value);
props.onChange && props.onChange(e);
},
value: inputValue,
})}
disabled={readOnly || disabled}
ref={ref}
/>
{addOnSuffix && (
<Addon
onClickAddon={onClickAddon}
isFilled={addOnFilled}
className={classNames("ltr:rounded-r-md rtl:rounded-l-md", addOnClassname)}>
{addOnSuffix}
</Addon>
)}
{type === "search" && inputValue?.toString().length > 0 && (
<Icon
name="x"
className="text-subtle absolute top-2.5 h-4 w-4 cursor-pointer ltr:right-2 rtl:left-2"
onClick={(e) => {
setInputValue("");
props.onChange && props.onChange(e as unknown as React.ChangeEvent<HTMLInputElement>);
}}
/>
)}
</div>
) : (
<Input
id={id}
type={type}
placeholder={placeholder}
className={classNames(
className,
"disabled:bg-subtle disabled:hover:border-subtle disabled:cursor-not-allowed"
)}
{...passThrough}
readOnly={readOnly}
ref={ref}
isFullWidth={inputIsFullWidth}
disabled={readOnly || disabled}
/>
)}
<HintsOrErrors hintErrors={hintErrors} fieldName={name} t={t} />
{hint && <div className="text-default mt-2 flex items-center text-sm">{hint}</div>}
</div>
);
});
export const TextField = forwardRef<HTMLInputElement, InputFieldProps>(function TextField(props, ref) {
return <InputField ref={ref} {...props} />;
});

View File

@@ -0,0 +1,182 @@
/* eslint-disable playwright/missing-playwright-await */
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { render, fireEvent } from "@testing-library/react";
import { vi } from "vitest";
import type { UnstyledSelect } from "../../../form/Select";
import { EmailField, TextAreaField, PasswordField, NumberInput, FilterSearchField } from "./Input";
import { InputFieldWithSelect } from "./InputFieldWithSelect";
import { InputField } from "./TextField";
const onChangeMock = vi.fn();
describe("Tests for InputField Component", () => {
test("Should render correctly with label and placeholder", () => {
const { getByLabelText, getByPlaceholderText } = render(
<InputField name="testInput" label="Test Label" placeholder="Test Placeholder" />
);
expect(getByLabelText("Test Label")).toBeInTheDocument();
expect(getByPlaceholderText("Test Placeholder")).toBeInTheDocument();
});
test("Should handle input correctly", () => {
const { getByRole } = render(<InputField name="testInput" onChange={onChangeMock} />);
const inputElement = getByRole("textbox") as HTMLInputElement;
fireEvent.change(inputElement, { target: { value: "Hello" } });
expect(onChangeMock).toHaveBeenCalledTimes(1);
expect(inputElement.value).toBe("Hello");
});
it("should render with addOnLeading prop", () => {
const { getByText } = render(<InputField addOnLeading={<span>Leading</span>} />);
const addOnLeadingElement = getByText("Leading");
expect(addOnLeadingElement).toBeInTheDocument();
});
it("should render with addOnSuffix prop", () => {
const { getByText } = render(<InputField addOnSuffix={<span>Suffix</span>} />);
const addOnSuffixElement = getByText("Suffix");
expect(addOnSuffixElement).toBeInTheDocument();
});
it("should display both addOnLeading and addOnSuffix", () => {
const { getByText } = render(
<InputField addOnLeading={<span>Leading</span>} addOnSuffix={<span>Suffix</span>} />
);
const addOnLeadingElement = getByText("Leading");
const addOnSuffixElement = getByText("Suffix");
expect(addOnLeadingElement).toBeInTheDocument();
expect(addOnSuffixElement).toBeInTheDocument();
});
it("Should display error message when error prop is provided", () => {
const errorMessage = "This field is required";
const { getByRole } = render(<InputField error={errorMessage} />);
const errorElement = getByRole("textbox");
expect(errorElement).toHaveAttribute("error", errorMessage);
});
});
describe("Tests for PasswordField Component", () => {
test("Should toggle password visibility correctly", () => {
const { getByLabelText, getByText } = render(
<TooltipProvider>
<PasswordField name="password" />
</TooltipProvider>
);
const passwordInput = getByLabelText("password") as HTMLInputElement;
const toggleButton = getByText("show_password");
expect(passwordInput.type).toBe("password");
fireEvent.click(toggleButton);
expect(passwordInput.type).toBe("text");
fireEvent.click(toggleButton);
expect(passwordInput.type).toBe("password");
});
});
describe("Tests for EmailField Component", () => {
test("Should render correctly with email-related attributes", () => {
const { getByRole } = render(<EmailField name="email" />);
const emailInput = getByRole("textbox");
expect(emailInput).toHaveAttribute("type", "email");
expect(emailInput).toHaveAttribute("autoCapitalize", "none");
expect(emailInput).toHaveAttribute("autoComplete", "email");
expect(emailInput).toHaveAttribute("autoCorrect", "off");
expect(emailInput).toHaveAttribute("inputMode", "email");
});
});
describe("Tests for TextAreaField Component", () => {
test("Should render correctly with label and placeholder", () => {
const { getByText, getByPlaceholderText, getByRole } = render(
<TextAreaField name="testTextArea" label="Test Label" placeholder="Test Placeholder" />
);
expect(getByText("Test Label")).toBeInTheDocument();
expect(getByPlaceholderText("Test Placeholder")).toBeInTheDocument();
expect(getByRole("textbox")).toBeInTheDocument();
});
test("Should handle input correctly", () => {
const { getByRole } = render(<TextAreaField name="testTextArea" onChange={onChangeMock} />);
const textareaElement = getByRole("textbox") as HTMLInputElement;
fireEvent.change(textareaElement, { target: { value: "Hello" } });
expect(onChangeMock).toHaveBeenCalled();
expect(textareaElement.value).toBe("Hello");
});
});
describe("Tests for InputFieldWithSelect Component", () => {
test("Should render correctly with InputField and UnstyledSelect", () => {
const onChangeMock = vi.fn();
const selectProps = {
value: null,
onChange: onChangeMock,
name: "testSelect",
options: [
{ value: "Option 1", label: "Option 1" },
{ value: "Option 2", label: "Option 2" },
{ value: "Option 3", label: "Option 3" },
],
} as unknown as typeof UnstyledSelect;
const { getByText } = render(<InputFieldWithSelect selectProps={selectProps} label="testSelect" />);
const inputElement = getByText("Select...");
fireEvent.mouseDown(inputElement);
const optionElement = getByText("Option 1");
expect(optionElement).toBeInTheDocument();
});
});
describe("Tests for NumberInput Component", () => {
test("Should render correctly with input type number", () => {
const { getByRole } = render(<NumberInput name="numberInput" />);
const numberInput = getByRole("spinbutton");
expect(numberInput).toBeInTheDocument();
expect(numberInput).toHaveAttribute("type", "number");
});
test("Should handle input correctly", () => {
const { getByRole } = render(<NumberInput name="numberInput" onChange={onChangeMock} />);
const numberInput = getByRole("spinbutton") as HTMLInputElement;
fireEvent.change(numberInput, { target: { value: "42" } });
expect(onChangeMock).toHaveBeenCalled();
expect(numberInput.value).toBe("42");
});
});
describe("Tests for FilterSearchField Component", () => {
test("Should render correctly with Search icon and input", async () => {
const { getByRole, findByTestId } = render(<FilterSearchField name="searchField" />);
const searchInput = getByRole("textbox");
const searchIcon = await findByTestId("search-icon");
expect(searchInput).toBeInTheDocument();
expect(searchIcon).toBeInTheDocument();
});
test("Should handle input correctly", () => {
const { getByRole } = render(<FilterSearchField name="searchField" onChange={onChangeMock} />);
const searchInput = getByRole("textbox") as HTMLInputElement;
fireEvent.change(searchInput, { target: { value: "Test search" } });
expect(onChangeMock).toHaveBeenCalled();
expect(searchInput.value).toBe("Test search");
});
});

View File

@@ -0,0 +1,100 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs";
import {
Examples,
Example,
Title,
CustomArgsTable,
VariantRow,
VariantsTable,
} from "@calcom/storybook/components";
import { InputFieldWithSelect } from "./InputFieldWithSelect";
import { InputField } from "./TextField";
<Meta title="UI/Form/Input Field" component={InputField} />
<Title title="Inputs" suffix="Brief" subtitle="Version 2.0 — Last Update: 24 Aug 2023" />
## Definition
Text fields allow users to input and edit text into the product. Usually appear in forms and modals. Various options can be shown with the field to communicate the input requirements.## Structure
## Structure
<CustomArgsTable of={InputField} />
<Examples
title="Inputs"
footnote={
<ul>
<li>The width is flexible but the height is fixed for both desktop and mobile. </li>
</ul>
}>
<Example title="Default">
<InputField placeholder="Default" />
</Example>
</Examples>
<Examples title="Input Types">
<Example title="Default">
<InputField placeholder="Default" />
</Example>
<Example title="Prefix">
<InputField placeholder="Prefix" addOnLeading={<>Prefix</>} />
</Example>
<Example title="Suffix">
<InputField placeholder="Suffic" addOnSuffix={<>Suffix</>} />
</Example>
<Example title="Suffix With Select">
<InputFieldWithSelect
placeholder="Suffix"
selectProps={{ options: [{ value: "TEXT", label: "Text" }] }}
/>
</Example>
<Example title="Focused">
<InputField placeholder="Focused" className="sb-pseudo--focus" />
</Example>
<Example title="Hovered">
<InputField placeholder="Hovered" className="sb-pseudo--hover" />
</Example>
<Example title="Error">
<InputField placeholder="Error" error="Error" />
</Example>
</Examples>
<Examples title="Input Text">
<Example title="Default">
<InputField />
</Example>
<Example title="Placeholder">
<InputField placeholder="Placeholder" />
</Example>
<Example title="Filled">
<InputField value="Filled" />
</Example>
</Examples>
<Title offset title="Input" suffix="Variants" />
<Canvas>
<Story name="All Variants">
<VariantsTable titles={["Default", "Focused", "Hovered"]} columnMinWidth={150}>
<VariantRow variant="Default">
<InputField placeholder="Default" />
<InputField placeholder="Focused" className="sb-pseudo--focus" />
<InputField placeholder="Hovered" className="sb-pseudo--hover" />
</VariantRow>
<VariantRow variant="Prefixed">
<InputField placeholder="Default" addOnLeading={<>Prefix</>} />
<InputField placeholder="Focused" className="sb-pseudo--focus" addOnLeading={<>Prefix</>} />
<InputField placeholder="Hovered" className="sb-pseudo--hover" addOnLeading={<>Prefix</>} />
</VariantRow>
<VariantRow variant="Suffix">
<InputField placeholder="Default" addOnSuffix={<>Prefix</>} />
<InputField placeholder="Focused" className="sb-pseudo--focus" addOnSuffix={<>Prefix</>} />
<InputField placeholder="Hovered" className="sb-pseudo--hover" addOnSuffix={<>Prefix</>} />
</VariantRow>
</VariantsTable>
</Story>
</Canvas>

View File

@@ -0,0 +1,27 @@
import type { ReactNode } from "react";
import type { Input } from "./TextField";
export type InputFieldProps = {
label?: ReactNode;
LockedIcon?: React.ReactNode;
hint?: ReactNode;
hintErrors?: string[];
addOnLeading?: ReactNode;
addOnSuffix?: ReactNode;
inputIsFullWidth?: boolean;
addOnFilled?: boolean;
addOnClassname?: string;
error?: string;
labelSrOnly?: boolean;
containerClassName?: string;
showAsteriskIndicator?: boolean;
t?: (key: string) => string;
dataTestid?: string;
onClickAddon?: () => void;
} & React.ComponentProps<typeof Input> & {
labelProps?: React.ComponentProps<typeof Label>;
labelClassName?: string;
};
export type InputProps = JSX.IntrinsicElements["input"] & { isFullWidth?: boolean };