Text area component with storybook and testing

This commit is contained in:
adilallo
2025-10-10 12:37:52 -06:00
parent 9c72afdc52
commit b71f0a7dea
8 changed files with 1126 additions and 110 deletions
+7 -8
View File
@@ -59,7 +59,7 @@ const Input = forwardRef(
return {
input:
"bg-[var(--color-content-default-secondary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] cursor-not-allowed",
label: "text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
@@ -67,7 +67,7 @@ const Input = forwardRef(
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-utility-negative)]",
label: "text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
@@ -76,25 +76,25 @@ const Input = forwardRef(
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
case "hover":
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
case "focus":
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-utility-info)] shadow-[0_0_5px_3px_#3281F8]",
label: "text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
default:
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
};
@@ -153,8 +153,7 @@ const Input = forwardRef(
{label && (
<label
htmlFor={inputId}
className={`${labelClasses} font-inter font-medium`}
style={{ color: "var(--color-content-default-secondary)" }}
className={`${labelClasses} font-inter font-medium text-[var(--color-content-default-secondary)]`}
>
{label}
</label>
+10 -6
View File
@@ -133,14 +133,14 @@ const Select = forwardRef(
return {
select:
"bg-[var(--color-content-default-secondary)] border-[var(--color-border-default-tertiary)] cursor-not-allowed opacity-40",
label: "text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
if (error) {
return {
select: "border-[var(--color-border-default-utility-negative)]",
label: "text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
@@ -149,18 +149,18 @@ const Select = forwardRef(
return {
select:
"border-[var(--color-border-default-tertiary)] shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
case "focus":
return {
select:
"border-[var(--color-border-default-utility-info)] shadow-[0_0_5px_3px_#3281F8]",
label: "text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
default:
return {
select: "border-[var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-primary)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
};
@@ -244,7 +244,11 @@ const Select = forwardRef(
return (
<div className={containerClasses}>
{label && (
<label id={labelId} htmlFor={selectId} className={labelClasses}>
<label
id={labelId}
htmlFor={selectId}
className={`${labelClasses} text-[var(--color-content-default-secondary)]`}
>
{label}
</label>
)}
+190
View File
@@ -0,0 +1,190 @@
"use client";
import React, { memo, useCallback, forwardRef, useId } from "react";
const TextArea = forwardRef(
(
{
size = "medium",
labelVariant = "default",
state = "default",
disabled = false,
error = false,
label,
placeholder,
value,
onChange,
onFocus,
onBlur,
id,
name,
className = "",
rows,
...props
},
ref
) => {
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const textareaId = id || `textarea-${generatedId}`;
// Size variants with specific heights and radius for TextArea
const sizeStyles = {
small: {
textarea:
labelVariant === "horizontal"
? "h-[60px] px-[12px] py-[8px] text-[10px]"
: "h-[60px] px-[12px] py-[8px] text-[10px]",
label: "text-[12px] leading-[14px] font-medium",
container: "gap-[4px]",
radius: "var(--measures-radius-xsmall)",
},
medium: {
textarea:
labelVariant === "horizontal"
? "h-[110px] px-[12px] py-[8px] text-[14px] leading-[20px]"
: "h-[100px] px-[12px] py-[8px] text-[14px] leading-[20px]",
label: "text-[14px] leading-[16px] font-medium",
container: "gap-[8px]",
radius: "var(--measures-radius-xsmall)",
},
large: {
textarea: "h-[150px] px-[12px] py-[8px] text-[16px] leading-[24px]",
label: "text-[16px] leading-[20px] font-medium",
container: "gap-[12px]",
radius: "var(--measures-radius-small)",
},
};
// State styles
const getStateStyles = () => {
if (disabled) {
return {
textarea:
"bg-[var(--color-content-default-secondary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] cursor-not-allowed",
label: "text-[var(--color-content-default-secondary)]",
};
}
if (error) {
return {
textarea:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-utility-negative)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
switch (state) {
case "active":
return {
textarea:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-secondary)]",
};
case "hover":
return {
textarea:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-secondary)]",
};
case "focus":
return {
textarea:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-utility-info)] shadow-[0_0_5px_3px_#3281F8]",
label: "text-[var(--color-content-default-secondary)]",
};
default:
return {
textarea:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
};
const stateStyles = getStateStyles();
const currentSize = sizeStyles[size];
// Container classes based on label variant
const containerClasses =
labelVariant === "horizontal"
? `flex items-center gap-[12px]`
: `flex flex-col ${currentSize.container}`;
const labelClasses =
labelVariant === "horizontal"
? `${currentSize.label} font-inter min-w-fit`
: `${currentSize.label} font-inter`;
const textareaClasses = `
w-full border transition-all duration-200 ease-in-out
focus:outline-none focus:ring-0 resize-none
${currentSize.textarea}
${stateStyles.textarea}
${className}
`.trim();
const handleChange = useCallback(
(e) => {
if (!disabled && onChange) {
onChange(e);
}
},
[disabled, onChange]
);
const handleFocus = useCallback(
(e) => {
if (!disabled && onFocus) {
onFocus(e);
}
},
[disabled, onFocus]
);
const handleBlur = useCallback(
(e) => {
if (!disabled && onBlur) {
onBlur(e);
}
},
[disabled, onBlur]
);
return (
<div className={containerClasses}>
{label && (
<label
htmlFor={textareaId}
className={`${labelClasses} font-inter font-medium text-[var(--color-content-default-secondary)]`}
>
{label}
</label>
)}
<div className={disabled ? "opacity-40" : ""}>
<textarea
ref={ref}
id={textareaId}
name={name}
value={value}
placeholder={placeholder}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
rows={rows}
className={textareaClasses}
style={{ borderRadius: currentSize.radius }}
aria-disabled={disabled}
aria-invalid={error}
{...props}
/>
</div>
</div>
);
}
);
TextArea.displayName = "TextArea";
export default memo(TextArea);
+29 -96
View File
@@ -1,11 +1,7 @@
"use client";
import React, { useState } from "react";
import Select from "../components/Select";
import ContextMenu from "../components/ContextMenu";
import ContextMenuItem from "../components/ContextMenuItem";
import ContextMenuSection from "../components/ContextMenuSection";
import ContextMenuDivider from "../components/ContextMenuDivider";
import TextArea from "../components/TextArea";
export default function FormsPlayground() {
const [smallValue, setSmallValue] = useState("");
@@ -23,157 +19,94 @@ export default function FormsPlayground() {
<h1 className="font-bricolage text-[24px]">Forms Playground</h1>
<section className="space-y-[12px]">
<h2 className="font-space text-[18px]">Select Examples</h2>
<h2 className="font-space text-[18px]">TextArea Examples</h2>
<div className="max-w-[520px] space-y-[16px]">
<div>
<h3 className="font-space text-[14px] mb-[8px]">Sizes</h3>
<div className="space-y-[12px]">
<Select
<TextArea
label="Small"
size="small"
value={smallValue}
onChange={(e) => setSmallValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
<Select
placeholder="Enter text..."
/>
<TextArea
label="Medium"
size="medium"
value={mediumValue}
onChange={(e) => setMediumValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
<Select
placeholder="Enter text..."
/>
<TextArea
label="Large"
size="large"
value={largeValue}
onChange={(e) => setLargeValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
placeholder="Enter text..."
/>
</div>
</div>
<div>
<h3 className="font-space text-[14px] mb-[8px]">Label Variants</h3>
<div className="space-y-[12px]">
<Select
<TextArea
label="Default (Top Label)"
labelVariant="default"
size="medium"
value={defaultLabelValue}
onChange={(e) => setDefaultLabelValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
<Select
placeholder="Enter text..."
/>
<TextArea
label="Small Default"
labelVariant="default"
size="small"
value={smallDefaultValue}
onChange={(e) => setSmallDefaultValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
<Select
placeholder="Enter text..."
/>
<TextArea
label="Horizontal (Left Label)"
labelVariant="horizontal"
size="medium"
value={horizontalLabelValue}
onChange={(e) => setHorizontalLabelValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
<Select
placeholder="Enter text..."
/>
<TextArea
label="Small Horizontal"
labelVariant="horizontal"
size="small"
value={smallHorizontalValue}
onChange={(e) => setSmallHorizontalValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
placeholder="Enter text..."
/>
</div>
</div>
<div>
<h3 className="font-space text-[14px] mb-[8px]">States</h3>
<div className="space-y-[12px]">
<Select
<TextArea
label="Error"
size="medium"
state="default"
error={true}
value={errorStateValue}
onChange={(e) => setErrorStateValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
<Select
placeholder="Enter text..."
/>
<TextArea
label="Disabled"
size="medium"
state="default"
disabled={true}
value={disabledStateValue}
onChange={(e) => setDisabledStateValue(e.target.value)}
placeholder="Select"
>
<option value="item1">Context Menu Item 1</option>
<option value="item2">Context Menu Item 2</option>
<option value="item3">Context Menu Item 3</option>
</Select>
</div>
</div>
</div>
</section>
<section className="space-y-[12px]">
<h2 className="font-space text-[18px]">Context Menu Examples</h2>
<div className="max-w-[520px] space-y-[16px]">
<div>
<h3 className="font-space text-[14px] mb-[8px]">
Context Menu Demo
</h3>
<div className="space-y-[12px]">
<ContextMenu>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
<ContextMenuItem hasSubmenu>Context Menu Item</ContextMenuItem>
<ContextMenuItem hasSubmenu>Context Menu Item</ContextMenuItem>
<ContextMenuDivider />
<ContextMenuItem selected>Context Menu Item</ContextMenuItem>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
<ContextMenuDivider />
<ContextMenuSection title="Section Title">
<ContextMenuItem>Context Menu Item</ContextMenuItem>
<ContextMenuItem>Context Menu Item</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>
placeholder="Enter text..."
/>
</div>
</div>
</div>
+286
View File
@@ -0,0 +1,286 @@
import React from "react";
import TextArea from "../app/components/TextArea";
export default {
title: "Forms/TextArea",
component: TextArea,
parameters: {
layout: "centered",
},
argTypes: {
size: {
control: { type: "select" },
options: ["small", "medium", "large"],
},
labelVariant: {
control: { type: "select" },
options: ["default", "horizontal"],
},
state: {
control: { type: "select" },
options: ["default", "active", "hover", "focus", "error"],
},
disabled: {
control: { type: "boolean" },
},
error: {
control: { type: "boolean" },
},
},
};
const Template = (args) => <TextArea {...args} />;
export const Default = Template.bind({});
Default.args = {
label: "Text Area",
placeholder: "Enter text...",
value: "",
};
export const WithValue = Template.bind({});
WithValue.args = {
label: "Text Area",
placeholder: "Enter text...",
value:
"This is some sample text content that demonstrates how the text area looks with content.",
};
export const Small = Template.bind({});
Small.args = {
size: "small",
label: "Small Text Area",
placeholder: "Enter text...",
value: "",
};
export const Medium = Template.bind({});
Medium.args = {
size: "medium",
label: "Medium Text Area",
placeholder: "Enter text...",
value: "",
};
export const Large = Template.bind({});
Large.args = {
size: "large",
label: "Large Text Area",
placeholder: "Enter text...",
value: "",
};
export const HorizontalLabel = Template.bind({});
HorizontalLabel.args = {
labelVariant: "horizontal",
label: "Horizontal Label",
placeholder: "Enter text...",
value: "",
};
export const AllSizes = () => (
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold">Default Label Variant</h3>
<div className="space-y-4">
<TextArea
size="small"
label="Small Text Area"
placeholder="Enter text..."
value=""
/>
<TextArea
size="medium"
label="Medium Text Area"
placeholder="Enter text..."
value=""
/>
<TextArea
size="large"
label="Large Text Area"
placeholder="Enter text..."
value=""
/>
</div>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold">Horizontal Label Variant</h3>
<div className="space-y-4">
<TextArea
size="small"
labelVariant="horizontal"
label="Small Text Area"
placeholder="Enter text..."
value=""
/>
<TextArea
size="medium"
labelVariant="horizontal"
label="Medium Text Area"
placeholder="Enter text..."
value=""
/>
<TextArea
size="large"
labelVariant="horizontal"
label="Large Text Area"
placeholder="Enter text..."
value=""
/>
</div>
</div>
</div>
);
export const States = () => (
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold">Default Label Variant</h3>
<div className="space-y-4">
<TextArea label="Default State" placeholder="Enter text..." value="" />
<TextArea
label="Active State"
placeholder="Enter text..."
value=""
state="active"
/>
<TextArea
label="Hover State"
placeholder="Enter text..."
value=""
state="hover"
/>
<TextArea
label="Focus State"
placeholder="Enter text..."
value=""
state="focus"
/>
<TextArea
label="Error State"
placeholder="Enter text..."
value=""
error
/>
<TextArea
label="Disabled State"
placeholder="Enter text..."
value=""
disabled
/>
</div>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold">Horizontal Label Variant</h3>
<div className="space-y-4">
<TextArea
labelVariant="horizontal"
label="Default State"
placeholder="Enter text..."
value=""
/>
<TextArea
labelVariant="horizontal"
label="Active State"
placeholder="Enter text..."
value=""
state="active"
/>
<TextArea
labelVariant="horizontal"
label="Hover State"
placeholder="Enter text..."
value=""
state="hover"
/>
<TextArea
labelVariant="horizontal"
label="Focus State"
placeholder="Enter text..."
value=""
state="focus"
/>
<TextArea
labelVariant="horizontal"
label="Error State"
placeholder="Enter text..."
value=""
error
/>
<TextArea
labelVariant="horizontal"
label="Disabled State"
placeholder="Enter text..."
value=""
disabled
/>
</div>
</div>
</div>
);
export const Interactive = () => {
const [value, setValue] = React.useState("");
const [state, setState] = React.useState("default");
const [disabled, setDisabled] = React.useState(false);
const [error, setError] = React.useState(false);
return (
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold">Interactive TextArea</h3>
<div className="space-y-4">
<TextArea
label="Interactive Text Area"
placeholder="Type something..."
value={value}
onChange={(e) => setValue(e.target.value)}
state={state}
disabled={disabled}
error={error}
/>
</div>
</div>
<div className="space-y-4">
<h4 className="text-md font-semibold">Controls</h4>
<div className="space-y-2">
<div>
<label className="block text-sm font-medium mb-1">State:</label>
<select
value={state}
onChange={(e) => setState(e.target.value)}
className="px-3 py-1 border border-gray-300 rounded"
>
<option value="default">Default</option>
<option value="active">Active</option>
<option value="hover">Hover</option>
<option value="focus">Focus</option>
</select>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="disabled"
checked={disabled}
onChange={(e) => setDisabled(e.target.checked)}
/>
<label htmlFor="disabled" className="text-sm">
Disabled
</label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="error"
checked={error}
onChange={(e) => setError(e.target.checked)}
/>
<label htmlFor="error" className="text-sm">
Error
</label>
</div>
</div>
</div>
</div>
);
};
+121
View File
@@ -0,0 +1,121 @@
import { expect, test, describe, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { axe, toHaveNoViolations } from "jest-axe";
import TextArea from "../../app/components/TextArea";
expect.extend(toHaveNoViolations);
describe("TextArea Accessibility", () => {
test("renders without accessibility violations", async () => {
const { container } = render(<TextArea label="Test TextArea" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has proper label association", () => {
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Test Label");
expect(textarea).toHaveAttribute("id");
expect(label).toHaveAttribute("for", textarea.id);
});
test("has proper ARIA attributes", () => {
render(<TextArea label="Test Label" name="test-textarea" />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveAttribute("id");
expect(textarea).toHaveAttribute("name", "test-textarea");
});
test("supports keyboard navigation", async () => {
const user = userEvent.setup();
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
await user.tab();
expect(textarea).toHaveFocus();
});
test("announces changes to screen readers", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<TextArea label="Test Label" onChange={handleChange} />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "test");
expect(textarea).toHaveValue("test");
});
test("handles disabled state accessibility", () => {
render(<TextArea label="Test Label" disabled />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeDisabled();
expect(textarea).toHaveAttribute("aria-disabled", "true");
});
test("handles error state accessibility", () => {
render(<TextArea label="Test Label" error />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveAttribute("aria-invalid", "true");
});
test("maintains focus management", async () => {
const user = userEvent.setup();
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
expect(textarea).toHaveFocus();
});
test("supports horizontal label layout", () => {
render(<TextArea labelVariant="horizontal" label="Horizontal Label" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Horizontal Label");
expect(textarea).toBeInTheDocument();
expect(label).toBeInTheDocument();
});
test("handles different sizes accessibility", () => {
const { rerender } = render(<TextArea size="small" label="Small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
rerender(<TextArea size="medium" label="Medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
rerender(<TextArea size="large" label="Large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
});
test("maintains proper contrast ratios", () => {
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Test Label");
expect(textarea).toHaveClass("text-[var(--color-content-default-primary)]");
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
});
test("supports screen reader announcements for state changes", async () => {
const user = userEvent.setup();
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
await user.type(textarea, "Hello");
expect(textarea).toHaveValue("Hello");
});
});
@@ -0,0 +1,280 @@
import React from "react";
import { expect, test, describe, it, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TextArea from "../../app/components/TextArea";
// Test form component for integration testing
const TestForm = () => {
const [formData, setFormData] = React.useState({
textarea1: "",
textarea2: "",
});
const handleSubmit = (e) => {
e.preventDefault();
};
return (
<form onSubmit={handleSubmit}>
<TextArea
label="First TextArea"
name="textarea1"
value={formData.textarea1}
onChange={(e) =>
setFormData((prev) => ({ ...prev, textarea1: e.target.value }))
}
placeholder="Enter first text..."
/>
<TextArea
label="Second TextArea"
name="textarea2"
value={formData.textarea2}
onChange={(e) =>
setFormData((prev) => ({ ...prev, textarea2: e.target.value }))
}
placeholder="Enter second text..."
/>
<button type="submit">Submit</button>
</form>
);
};
// Dynamic TextArea component for prop changes testing
const DynamicTextArea = ({ size, labelVariant, state, disabled, error }) => {
const [value, setValue] = React.useState("");
return (
<TextArea
label="Dynamic TextArea"
value={value}
onChange={(e) => setValue(e.target.value)}
size={size}
labelVariant={labelVariant}
state={state}
disabled={disabled}
error={error}
placeholder="Enter text..."
/>
);
};
describe("TextArea Integration Tests", () => {
test("handles form submission with multiple textareas", async () => {
const user = userEvent.setup();
render(<TestForm />);
const firstTextarea = screen.getByPlaceholderText("Enter first text...");
const secondTextarea = screen.getByPlaceholderText("Enter second text...");
const submitButton = screen.getByRole("button", { name: /Submit/ });
await user.type(firstTextarea, "First content");
await user.type(secondTextarea, "Second content");
expect(firstTextarea).toHaveValue("First content");
expect(secondTextarea).toHaveValue("Second content");
await user.click(submitButton);
// Form submission should not cause errors
});
test("handles keyboard navigation between textareas", async () => {
const user = userEvent.setup();
render(<TestForm />);
const firstTextarea = screen.getByPlaceholderText("Enter first text...");
const secondTextarea = screen.getByPlaceholderText("Enter second text...");
await user.click(firstTextarea);
expect(firstTextarea).toHaveFocus();
await user.tab();
expect(secondTextarea).toHaveFocus();
});
test("handles dynamic prop changes", () => {
const { rerender } = render(<DynamicTextArea size="small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[60px]");
rerender(<DynamicTextArea size="medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[100px]");
rerender(<DynamicTextArea size="large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[150px]");
});
test("handles state changes", () => {
const { rerender } = render(<DynamicTextArea state="default" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]"
);
rerender(<DynamicTextArea state="hover" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]"
);
rerender(<DynamicTextArea state="focus" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
"shadow-[0_0_5px_3px_#3281F8]"
);
});
test("handles disabled state changes", () => {
const { rerender } = render(<DynamicTextArea disabled={false} />);
let textarea = screen.getByRole("textbox");
expect(textarea).not.toBeDisabled();
rerender(<DynamicTextArea disabled={true} />);
textarea = screen.getByRole("textbox");
expect(textarea).toBeDisabled();
});
test("handles error state changes", () => {
const { rerender } = render(<DynamicTextArea error={false} />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]"
);
rerender(<DynamicTextArea error={true} />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-negative)]"
);
});
test("handles label variant changes", () => {
const { rerender } = render(<DynamicTextArea labelVariant="default" />);
let container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "flex-col");
rerender(<DynamicTextArea labelVariant="horizontal" />);
container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "items-center", "gap-[12px]");
});
test("handles text input and changes", async () => {
const user = userEvent.setup();
render(<DynamicTextArea />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "Hello World");
expect(textarea).toHaveValue("Hello World");
});
test("handles focus and blur events", async () => {
const user = userEvent.setup();
const handleFocus = vi.fn();
const handleBlur = vi.fn();
render(
<TextArea
label="Test TextArea"
onFocus={handleFocus}
onBlur={handleBlur}
/>
);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
expect(handleFocus).toHaveBeenCalled();
await user.tab();
expect(handleBlur).toHaveBeenCalled();
});
test("handles multiple textareas with different configurations", () => {
render(
<div>
<TextArea
size="small"
label="Small TextArea"
placeholder="Small placeholder"
/>
<TextArea
size="medium"
labelVariant="horizontal"
label="Medium Horizontal"
placeholder="Medium placeholder"
/>
<TextArea
size="large"
label="Large TextArea"
placeholder="Large placeholder"
/>
</div>
);
expect(
screen.getByPlaceholderText("Small placeholder")
).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Medium placeholder")
).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Large placeholder")
).toBeInTheDocument();
});
test("handles form validation with error states", () => {
render(
<div>
<TextArea label="Valid TextArea" placeholder="Valid input" />
<TextArea label="Invalid TextArea" placeholder="Invalid input" error />
<TextArea
label="Disabled TextArea"
placeholder="Disabled input"
disabled
/>
</div>
);
const validTextarea = screen.getByPlaceholderText("Valid input");
const invalidTextarea = screen.getByPlaceholderText("Invalid input");
const disabledTextarea = screen.getByPlaceholderText("Disabled input");
expect(validTextarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]"
);
expect(invalidTextarea).toHaveClass(
"border-[var(--color-border-default-utility-negative)]"
);
expect(disabledTextarea).toBeDisabled();
});
test("handles performance with multiple re-renders", () => {
const { rerender } = render(<DynamicTextArea />);
// Simulate multiple re-renders
for (let i = 0; i < 10; i++) {
rerender(<DynamicTextArea size={i % 2 === 0 ? "small" : "large"} />);
}
const textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
});
test("handles accessibility with screen readers", async () => {
const user = userEvent.setup();
render(<TextArea label="Accessible TextArea" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Accessible TextArea");
expect(textarea).toHaveAttribute("id");
expect(label).toHaveAttribute("for", textarea.id);
await user.click(textarea);
expect(textarea).toHaveFocus();
});
});
+203
View File
@@ -0,0 +1,203 @@
import { expect, test, describe, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TextArea from "../../app/components/TextArea";
describe("TextArea", () => {
test("renders with default props", () => {
render(<TextArea />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
});
test("renders with label", () => {
render(<TextArea label="Test Label" />);
expect(screen.getByText("Test Label")).toBeInTheDocument();
expect(screen.getByLabelText("Test Label")).toBeInTheDocument();
});
test("renders with placeholder", () => {
render(<TextArea placeholder="Enter text..." />);
expect(screen.getByPlaceholderText("Enter text...")).toBeInTheDocument();
});
test("renders with value", () => {
render(<TextArea value="Test value" />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveValue("Test value");
});
test("renders with different sizes", () => {
const { rerender } = render(<TextArea size="small" label="Small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[60px]");
rerender(<TextArea size="medium" label="Medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[100px]");
rerender(<TextArea size="large" label="Large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[150px]");
});
test("renders with horizontal label variant", () => {
render(<TextArea labelVariant="horizontal" label="Horizontal Label" />);
const container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "items-center", "gap-[12px]");
});
test("renders with default label variant", () => {
render(<TextArea labelVariant="default" label="Default Label" />);
const container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "flex-col");
});
test("applies disabled state", () => {
render(<TextArea disabled />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeDisabled();
});
test("applies error state", () => {
render(<TextArea error />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-negative)]"
);
});
test("applies different states", () => {
const { rerender } = render(<TextArea state="active" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]"
);
rerender(<TextArea state="hover" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]"
);
rerender(<TextArea state="focus" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
"shadow-[0_0_5px_3px_#3281F8]"
);
});
test("calls onChange when text changes", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<TextArea onChange={handleChange} />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "test");
expect(handleChange).toHaveBeenCalledTimes(4);
});
test("calls onFocus when focused", async () => {
const user = userEvent.setup();
const handleFocus = vi.fn();
render(<TextArea onFocus={handleFocus} />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
expect(handleFocus).toHaveBeenCalled();
});
test("calls onBlur when blurred", async () => {
const user = userEvent.setup();
const handleBlur = vi.fn();
render(<TextArea onBlur={handleBlur} />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
await user.tab();
expect(handleBlur).toHaveBeenCalled();
});
test("does not call onChange when disabled", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<TextArea disabled onChange={handleChange} />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "test");
expect(handleChange).not.toHaveBeenCalled();
});
test("applies custom className", () => {
render(<TextArea className="custom-class" />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("custom-class");
});
test("forwards ref", () => {
const ref = vi.fn();
render(<TextArea ref={ref} />);
expect(ref).toHaveBeenCalled();
});
test("applies correct height for small horizontal label", () => {
render(
<TextArea
size="small"
labelVariant="horizontal"
label="Small Horizontal"
/>
);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[60px]");
});
test("applies correct height for medium horizontal label", () => {
render(
<TextArea
size="medium"
labelVariant="horizontal"
label="Medium Horizontal"
/>
);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[110px]");
});
test("applies correct border radius for different sizes", () => {
const { rerender } = render(<TextArea size="small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveStyle({
borderRadius: "var(--measures-radius-xsmall)",
});
rerender(<TextArea size="medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveStyle({
borderRadius: "var(--measures-radius-xsmall)",
});
rerender(<TextArea size="large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveStyle({
borderRadius: "var(--measures-radius-small)",
});
});
test("applies correct text color", () => {
render(<TextArea />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("text-[var(--color-content-default-primary)]");
});
test("applies correct label color", () => {
render(<TextArea label="Test Label" />);
const label = screen.getByText("Test Label");
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
});
});