import { StoryObj, Meta } from "@storybook/react-vite"; import { within, userEvent, expect } from "storybook/test"; import React from "react"; import Input from "./inputs"; import { Checkbox as CheckboxComponent } from "./checkbox"; import "./form.scss"; const meta: Meta = { title: "FP.React Forms/Inputs", component: Input, tags: ["stable"], args: {}, parameters: { docs: { description: { component: 'Use the `` component to render an any input element -- text, email, number etc. Pass props like `name`, `value`, `placeholder` etc to control the input.', }, }, }, } as Story; export default meta; type Story = StoryObj; export const InputComponent: Story = { args: {}, play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByRole("textbox")).toBeInTheDocument(); }, }; //required input story export const RequiredInput: Story = { parameters: { docs: { description: { story: 'Displays a required input `aria-required="true"` on any input type the placeholder displays an `*` at the start of a default placeholder text to indicate it is required', }, }, }, args: { type: "text", required: true, placeholder: "This Field is required (placeholder)", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); const input = canvas.getByRole("textbox"); expect(input).toBeRequired(); await userEvent.type(input, "test"); expect(input).toBeValid(); await userEvent.clear(input); userEvent.type(input, "\n"); expect(input).toBeInvalid(); }, } as Story; export const DefaultRequired: Story = { args: { type: "text", required: true, }, } as Story; /** * Disabled input using WCAG-compliant aria-disabled pattern. * * Key accessibility features implemented by the optimized useDisabledState hook: * - Uses aria-disabled instead of native disabled attribute * - Remains keyboard focusable (in tab order) * - Prevents all interactions (typing, onChange events) * - Screen readers can discover and announce disabled state * - Automatic className merging (.is-disabled + custom classes) */ export const InputDisabled: Story = { parameters: { docs: { description: { story: ` Displays a disabled input with \`aria-disabled="true"\`. **Why aria-disabled instead of disabled?** - Keeps input in tab order for screen reader users - Allows focus for screen reader announcement - Better contrast control for WCAG AA compliance - Can show tooltips/help text even when disabled Try tabbing to the input - it receives focus! Try typing - interactions are prevented by the hook. `, }, }, }, args: { type: "text", disabled: true, placeholder: "This input is disabled", }, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); const input = canvas.getByRole("textbox"); await step("Disabled input has aria-disabled attribute", async () => { expect(input).toHaveAttribute("aria-disabled", "true"); }); await step("Disabled input remains focusable", async () => { await userEvent.tab(); expect(input).toHaveFocus(); }); await step("Disabled input has .is-disabled class", async () => { expect(input).toHaveClass("is-disabled"); }); await step("Disabled input prevents typing interactions", async () => { const initialValue = input.getAttribute("value") || ""; await userEvent.type(input, "test"); // Value should remain unchanged due to disabled state expect(input).toHaveValue(initialValue); }); }, } as Story; /** * Disabled input with custom classes. * * Demonstrates the hook's automatic className merging feature. * The .is-disabled class is automatically combined with custom classes. */ export const DisabledWithCustomClass: Story = { args: { type: "text", disabled: true, classes: "custom-input highlight-disabled", placeholder: "Disabled with custom classes", }, parameters: { docs: { description: { story: "Shows how the optimized hook automatically merges `.is-disabled` with custom classes (`custom-input highlight-disabled`).", }, }, }, } as Story; export const EmailInput: Story = { args: { type: "email", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); const input = canvas.getByRole("textbox"); expect(input).toHaveAttribute("type", "email"); await userEvent.type(input, "test@example.com"); expect(input).toHaveValue("test@example.com"); }, } as Story; export const PasswordInput: Story = { args: { type: "password", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); const input = canvas.getByPlaceholderText(/password/i); expect(input).toHaveAttribute("type", "password"); await userEvent.type(input, "password"); expect(input).toHaveValue("password"); }, } as Story; export const SearchInput: Story = { args: { type: "search", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); const input = canvas.getByRole("searchbox"); expect(input).toHaveAttribute("type", "search"); await userEvent.type(input, "search term"); expect(input).toHaveValue("search term"); }, } as Story; export const TelInput: Story = { args: { type: "tel", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); const input = canvas.getByRole("textbox"); expect(input).toHaveAttribute("type", "tel"); await userEvent.type(input, "1234567890"); expect(input).toHaveValue("1234567890"); }, } as Story; // URL text input story export const UrlInput: Story = { args: { type: "url", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); const input = canvas.getByRole("textbox"); expect(input).toHaveAttribute("type", "url"); await userEvent.type(input, "https://example.com"); expect(input).toHaveValue("https://example.com"); }, } as Story; export const Checkbox: Story = { args: { type: "checkbox", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); const input = canvas.getByRole("checkbox"); expect(input).toHaveAttribute("type", "checkbox"); await userEvent.click(input); expect(input).toBeChecked(); await userEvent.click(input); expect(input).not.toBeChecked(); }, } as Story; // ============================================================================ // Checkbox Wrapper Component Stories // ============================================================================ /** * CheckboxWrapper - Basic checkbox with label * * Demonstrates the Checkbox wrapper component with simplified API. * Features automatic label association, boolean onChange, and keyboard support. */ export const CheckboxWrapper: Story = { render: () => ( ), play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); const checkbox = canvas.getByRole("checkbox"); await step("Checkbox renders unchecked", async () => { expect(checkbox).toBeInTheDocument(); expect(checkbox).not.toBeChecked(); }); await step("Checkbox can be checked by clicking", async () => { await userEvent.click(checkbox); expect(checkbox).toBeChecked(); }); await step("Label can be clicked to toggle", async () => { const label = canvas.getByText("I accept the terms and conditions"); await userEvent.click(label); expect(checkbox).not.toBeChecked(); }); await step("Space key toggles checkbox", async () => { checkbox.focus(); await userEvent.keyboard(" "); expect(checkbox).toBeChecked(); }); }, }; /** * CheckboxControlled - Controlled checkbox with state management * * Demonstrates controlled mode with React state and boolean onChange API. */ const CheckboxControlledExample = () => { const [checked, setChecked] = React.useState(false); return (

Status: {checked ? "✓ Subscribed" : "Not subscribed"}

); }; export const CheckboxControlled: Story = { render: () => , }; /** * CheckboxRequired - Required checkbox with asterisk indicator * * Shows required field indicator and aria-required attribute. */ export const CheckboxRequired: Story = { render: () => ( ), play: async ({ canvasElement }) => { const canvas = within(canvasElement); const checkbox = canvas.getByRole("checkbox"); expect(checkbox).toHaveAttribute("aria-required", "true"); expect(canvas.getByText("*")).toBeInTheDocument(); }, }; /** * CheckboxDisabled - Disabled checkbox (WCAG compliant) * * Demonstrates aria-disabled pattern that remains focusable for screen readers. */ export const CheckboxDisabled: Story = { render: () => ( ), play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); const checkbox = canvas.getByRole("checkbox"); await step("Disabled checkbox has aria-disabled", async () => { expect(checkbox).toHaveAttribute("aria-disabled", "true"); }); await step("Disabled checkbox remains focusable", async () => { await userEvent.tab(); expect(checkbox).toHaveFocus(); }); await step("Disabled checkbox prevents interaction", async () => { const wasChecked = checkbox.checked; await userEvent.click(checkbox); // Value should remain unchanged due to disabled state expect(checkbox.checked).toBe(wasChecked); }); }, }; /** * CheckboxValidation - Checkbox with validation error * * Shows error state with aria-invalid and error message. */ export const CheckboxValidation: Story = { render: () => ( ), play: async ({ canvasElement }) => { const canvas = within(canvasElement); const checkbox = canvas.getByRole("checkbox"); expect(checkbox).toHaveAttribute("aria-invalid", "true"); expect(checkbox).toHaveAttribute("aria-describedby"); expect(canvas.getByText("You must accept the terms to continue")).toBeInTheDocument(); }, }; /** * CheckboxWithHint - Checkbox with hint text * * Demonstrates hint text for additional context. */ export const CheckboxWithHint: Story = { render: () => ( ), }; /** * CheckboxCustomSize - Predefined size variants using size prop * * Demonstrates semantic size prop for common size variants. * For custom sizes beyond presets, see CheckboxCustomSizeCSSOverride story. */ export const CheckboxCustomSize: Story = { render: () => (
), play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step("XS checkbox has correct data attribute", async () => { const checkbox = canvas.getByRole("checkbox", { name: /Extra Small/ }); const wrapper = checkbox.closest('div'); expect(wrapper).toHaveAttribute("data-checkbox-size", "xs"); }); await step("LG checkbox has correct data attribute", async () => { const checkbox = canvas.getByRole("checkbox", { name: /Large/ }); const wrapper = checkbox.closest('div'); expect(wrapper).toHaveAttribute("data-checkbox-size", "lg"); }); await step("All checkboxes are functional", async () => { const xsCheckbox = canvas.getByRole("checkbox", { name: /Extra Small/ }); await userEvent.click(xsCheckbox); expect(xsCheckbox).toBeChecked(); }); }, }; /** * CheckboxCustomSizeCSSOverride - Custom sizes via CSS variables * * Demonstrates that CSS variable overrides still work for custom sizes * beyond the predefined xs/sm/md/lg variants. */ export const CheckboxCustomSizeCSSOverride: Story = { render: () => (
), parameters: { docs: { description: { story: ` For sizes outside the predefined variants, use the \`styles\` prop to override CSS variables directly. This provides maximum flexibility while keeping the API clean for common cases. `, }, }, }, }; /** * CheckboxGroup - Multiple checkboxes in a fieldset * * Demonstrates grouping related checkboxes with semantic HTML. */ export const CheckboxGroup: Story = { render: () => (
Notification Preferences
), }; /** * CSS Variable Customization * * Demonstrates how to customize input appearance using the new standardized * CSS custom property naming convention. * * New variable naming patterns: * - Logical properties: `--input-padding-inline`, `--input-padding-block` * - Full property names: `--input-width`, `--input-radius`, `--input-border` * - Focus state variables: `--input-focus-outline`, `--input-focus-outline-offset` * - Disabled state variables: `--input-disabled-bg`, `--input-disabled-opacity` * - Approved abbreviations: `--input-fs` (font-size) */ export const Customization: Story = { render: () => (
{/* Custom brand styling */}

Custom Brand Styling

{/* Custom focus indicator (WCAG compliant) */}

Custom Focus Indicator (WCAG AA Compliant)

{/* Custom disabled state */}

Custom Disabled State

{/* Compact input */}

Compact Input (Logical Properties)

{/* Spacious input */}

Spacious Input

{/* Dark theme example */}

Dark Theme Example

), parameters: { docs: { description: { story: ` ## Available CSS Variables ### Base Properties - \`--input-padding-inline\`: Horizontal padding (logical property) - \`--input-padding-block\`: Vertical padding (logical property) - \`--input-width\`: Input width (default: clamp(200px, 100%, 500px)) - \`--input-border\`: Border style - \`--input-radius\`: Border radius - \`--input-bg\`: Background color - \`--input-outline\`: Default outline ### Typography (Approved Abbreviation) - \`--input-fs\`: Font size ### Focus State Variables (NEW) - \`--input-focus-outline\`: Outline when focused - \`--input-focus-outline-offset\`: Outline offset (for WCAG compliance) ### Disabled State Variables (NEW) - \`--input-disabled-bg\`: Background when disabled - \`--input-disabled-opacity\`: Opacity when disabled - \`--input-disabled-cursor\`: Cursor style when disabled ### Placeholder Variables - \`--placeholder-color\`: Placeholder text color - \`--placeholder-fs\`: Placeholder font size - \`--placeholder-style\`: Placeholder font style (italic) ### Migration from Old Names - ❌ \`--input-px\` → ✅ \`--input-padding-inline\` - ❌ \`--input-py\` → ✅ \`--input-padding-block\` - ❌ \`--input-w\` → ✅ \`--input-width\` `, }, }, }, } as Story;