diff --git a/packages/documentation/src/Form/TextInput.stories.tsx b/packages/documentation/src/Form/TextInput.stories.tsx index fb31879b..b6620bc8 100644 --- a/packages/documentation/src/Form/TextInput.stories.tsx +++ b/packages/documentation/src/Form/TextInput.stories.tsx @@ -20,6 +20,7 @@ export default { className: "", mode: "system", labelHidden: false, + size: "md", }, argTypes: { mode: { @@ -30,6 +31,10 @@ export default { options: ["dark", "light", "system", "alt-system"], control: { type: "radio" }, }, + size: { + options: ["xs", "sm", "md", "lg", "xl"], + control: { type: "radio" }, + }, }, }; diff --git a/packages/ui-textinput/src/components/TextInput/TextInput.tsx b/packages/ui-textinput/src/components/TextInput/TextInput.tsx index b8a8182d..43b6a66c 100644 --- a/packages/ui-textinput/src/components/TextInput/TextInput.tsx +++ b/packages/ui-textinput/src/components/TextInput/TextInput.tsx @@ -30,6 +30,7 @@ export const TextInput = React.forwardRef( rightElement, spacing, + size = "md", ...extraProps }, @@ -49,6 +50,7 @@ export const TextInput = React.forwardRef( noBorder, spacing, mode, + size, }); /* c8 ignore start - ResizeObserver is tough to test... */ diff --git a/packages/ui-textinput/src/components/TextInput/TextInputTypes.d.ts b/packages/ui-textinput/src/components/TextInput/TextInputTypes.d.ts index 928034f6..c6b2a70a 100644 --- a/packages/ui-textinput/src/components/TextInput/TextInputTypes.d.ts +++ b/packages/ui-textinput/src/components/TextInput/TextInputTypes.d.ts @@ -1,5 +1,7 @@ import type { SpacingProps } from "@versini/ui-private/dist/utilities"; +export type Size = "xs" | "sm" | "md" | "lg" | "xl"; + export type CommonTextInputProps = { /** * The label of the TextInput. @@ -49,8 +51,12 @@ export type CommonTextInputProps = { * @default false */ raw?: boolean; + /** + * Controls input height and horizontal padding, 'md' by default + */ + size?: Size; } & SpacingProps & - React.InputHTMLAttributes; + Omit, "size">; export type TextInputProps = { /** @@ -59,8 +65,7 @@ export type TextInputProps = { * elements, such a Button. */ rightElement?: React.ReactElement; -} & CommonTextInputProps & - React.InputHTMLAttributes; +} & CommonTextInputProps; export type TextInputMaskProps = { /** @@ -78,5 +83,4 @@ export type TextInputMaskProps = { * Callback fired when the user blurs out of the TextInputMask. */ onTextInputMaskBlur?: () => void; -} & CommonTextInputProps & - React.InputHTMLAttributes; +} & CommonTextInputProps; diff --git a/packages/ui-textinput/src/components/TextInput/__tests__/TextInput.test.tsx b/packages/ui-textinput/src/components/TextInput/__tests__/TextInput.test.tsx index ae9bc1e8..13364b53 100644 --- a/packages/ui-textinput/src/components/TextInput/__tests__/TextInput.test.tsx +++ b/packages/ui-textinput/src/components/TextInput/__tests__/TextInput.test.tsx @@ -10,6 +10,74 @@ describe("TextInput (exceptions)", () => { }); }); +describe("TextInput sizes", () => { + it.each` + size | description + ${"xs"} | ${"extra small"} + ${"sm"} | ${"small"} + ${"md"} | ${"medium"} + ${"lg"} | ${"large"} + ${"xl"} | ${"extra large"} + `("should render a text input with size $description", async ({ size }) => { + render(); + const label = await screen.findAllByText("hello world"); + const input = await screen.findByRole("textbox"); + + expect(label[0]?.className).toContain("sr-only"); + expectToHaveClasses(label[1], [ + "absolute", + "cursor-text", + "font-medium", + "text-copy-dark", + ]); + let heightClass = "", + paddingClass = ""; + switch (size) { + case "xs": + heightClass = "h-8"; + paddingClass = "px-2"; + break; + case "sm": + heightClass = "h-10"; + paddingClass = "px-3"; + break; + case "md": + heightClass = "h-12"; + paddingClass = "px-4"; + break; + case "lg": + heightClass = "h-14"; + paddingClass = "px-4"; + break; + case "xl": + heightClass = "h-16"; + paddingClass = "px-4"; + break; + + default: + heightClass = "h-12"; + paddingClass = "px-4"; + break; + } + expectToHaveClasses(input, [ + TEXT_INPUT_CLASSNAME, + "bg-surface-lighter", + "border-2", + "border-border-dark", + "caret-copy-dark", + "focus:outline-2", + "focus:outline-focus-dark", + "focus:outline-offset-2", + "focus:outline", + heightClass, + paddingClass, + "rounded-md", + "text-base", + "text-copy-dark", + ]); + }); +}); + describe("TextInput modifiers", () => { it("should render a dark or light (system) text input", async () => { render(); @@ -137,7 +205,6 @@ describe("TextInput modifiers", () => { />, ); const label = await screen.findAllByText("hello world"); - screen.debug(label); expect(label.length).toBe(1); }); diff --git a/packages/ui-textinput/src/components/TextInput/utilities.ts b/packages/ui-textinput/src/components/TextInput/utilities.ts index df4b4c9f..ae3ab322 100644 --- a/packages/ui-textinput/src/components/TextInput/utilities.ts +++ b/packages/ui-textinput/src/components/TextInput/utilities.ts @@ -7,6 +7,7 @@ import { TEXT_INPUT_HELPER_TEXT_CLASSNAME, TEXT_INPUT_WRAPPER_CLASSNAME, } from "../../common/constants"; +import type { Size } from "./TextInputTypes"; type getTextInputClassesProps = { disabled: boolean; @@ -15,6 +16,7 @@ type getTextInputClassesProps = { mode: "dark" | "light" | "system" | "alt-system"; noBorder: boolean; raw: boolean; + size: Size; className?: string; inputClassName?: string; @@ -151,6 +153,7 @@ export const getTextInputClasses = ({ spacing, mode, focusMode, + size, }: getTextInputClassesProps) => { const wrapper = raw ? className @@ -161,12 +164,33 @@ export const getTextInputClasses = ({ getSpacing(spacing), ); + let sizeClass = ""; + switch (size) { + case "xs": + sizeClass = "h-8 px-2"; + break; + case "sm": + sizeClass = "h-10 px-3"; + break; + case "lg": + sizeClass = "h-14 px-4"; + break; + case "xl": + sizeClass = "h-16 px-4"; + break; + + default: + sizeClass = "h-12 px-4"; + break; + } + const input = raw ? clsx(inputClassName) : clsx( TEXT_INPUT_CLASSNAME, inputClassName, - "h-12 rounded-md px-4 text-base", + sizeClass, + "rounded-md text-base", getTextInputColorClasses({ mode }), getTextInputFocusClasses({ focusMode }), getTextInputBorderClasses({ noBorder, error }),