From 2fd2cd01b372d23a54b083152e5aedb96959dbe8 Mon Sep 17 00:00:00 2001 From: Sauli Anto Date: Wed, 10 Apr 2024 15:49:32 +0300 Subject: [PATCH] OPHJOD-294: Add input field component --- .../InputField/InputField.stories.tsx | 116 ++++++++++++++++++ lib/components/InputField/InputField.test.tsx | 50 ++++++++ lib/components/InputField/InputField.tsx | 36 ++++++ .../__snapshots__/InputField.test.tsx.snap | 37 ++++++ lib/main.ts | 5 +- 5 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 lib/components/InputField/InputField.stories.tsx create mode 100644 lib/components/InputField/InputField.test.tsx create mode 100644 lib/components/InputField/InputField.tsx create mode 100644 lib/components/InputField/__snapshots__/InputField.test.tsx.snap diff --git a/lib/components/InputField/InputField.stories.tsx b/lib/components/InputField/InputField.stories.tsx new file mode 100644 index 0000000..049f7ce --- /dev/null +++ b/lib/components/InputField/InputField.stories.tsx @@ -0,0 +1,116 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { useState } from 'react'; + +import { InputField } from './InputField'; + +const meta = { + title: 'Forms/InputField', + component: InputField, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const render = (args: Story['args']) => { + const { value, onChange, ...rest } = args; + const [textValue, setTextValue] = useState(value); + return ( + { + setTextValue(newValue); + onChange(newValue); + }} + {...rest} + /> + ); +}; +const url = 'https://www.figma.com/file/6M2LrpSCcB0thlFDaQAI2J/cx_jod_client?node-id=542%3A7550'; +const label = 'Label text'; +const loremIpsum = 'Lorem ipsum dolor sit amet'; + +export const Default: Story = { + render, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + parameters: { + design: { + type: 'figma', + url, + }, + docs: { + description: { + story: 'This is a input field component for displaying and editing a value with a label text.', + }, + }, + }, + args: { + value: loremIpsum, + onChange: fn(), + label, + }, +}; + +export const HelpText: Story = { + render, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + parameters: { + design: { + type: 'figma', + url, + }, + docs: { + description: { + story: 'This is a input field component for displaying and editing a value with a label and help text.', + }, + }, + }, + args: { + value: loremIpsum, + onChange: fn(), + label, + help: 'Help text', + }, +}; + +export const Placeholder: Story = { + render, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + parameters: { + design: { + type: 'figma', + url, + }, + docs: { + description: { + story: 'This is a input field component for displaying and editing a value with placeholder text.', + }, + }, + }, + args: { + value: '', + onChange: fn(), + label, + placeholder: loremIpsum, + }, +}; diff --git a/lib/components/InputField/InputField.test.tsx b/lib/components/InputField/InputField.test.tsx new file mode 100644 index 0000000..2f54c2c --- /dev/null +++ b/lib/components/InputField/InputField.test.tsx @@ -0,0 +1,50 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +import { InputField } from './InputField'; + +describe('InputField', () => { + const label = 'Username'; + + it('renders label correctly', () => { + const value = 'john'; + const onChange = vi.fn(); + const { container } = render(); + const labelElement = screen.getByText(label); + expect(labelElement).toBeInTheDocument(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('renders input correctly', () => { + const placeholder = 'Enter your username'; + const value = 'john'; + const onChange = vi.fn(); + const { container } = render( + , + ); + const inputElement = screen.getByPlaceholderText(`(${placeholder})`); + expect(inputElement).toBeInTheDocument(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('renders help text correctly', () => { + const helpText = 'Please enter your username'; + const value = 'john'; + const onChange = vi.fn(); + const { container } = render(); + const helpElement = screen.getByText(helpText); + expect(helpElement).toBeInTheDocument(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('updates input value correctly', () => { + const onChange = vi.fn(); + const value = 'test value'; + const { container } = render(); + const inputElement = screen.getByRole('textbox'); + fireEvent.change(inputElement, { target: { value: 'new value' } }); + expect(onChange).toHaveBeenCalledWith('new value'); + expect(container.firstChild).toMatchSnapshot(); + }); +}); diff --git a/lib/components/InputField/InputField.tsx b/lib/components/InputField/InputField.tsx new file mode 100644 index 0000000..fe31532 --- /dev/null +++ b/lib/components/InputField/InputField.tsx @@ -0,0 +1,36 @@ +import { useId } from 'react'; + +export interface InputFieldProps { + value: string; + onChange: (newValue: string) => void; + placeholder?: string; + label: string; + help?: string; +} + +export const InputField = ({ value, onChange, placeholder, label, help }: InputFieldProps) => { + const inputId = useId(); + const helpId = useId(); + return ( + <> + + onChange(event.target.value)} + placeholder={placeholder ? `(${placeholder})` : undefined} + autoComplete="off" + aria-describedby={help ? helpId : undefined} + className="block w-full rounded-[10px] border-[5px] border-border-gray bg-white p-[11px] text-primary-gray outline-none placeholder:text-secondary-gray" + /> + {help && ( +
+ {help} +
+ )} + + ); +}; diff --git a/lib/components/InputField/__snapshots__/InputField.test.tsx.snap b/lib/components/InputField/__snapshots__/InputField.test.tsx.snap new file mode 100644 index 0000000..091c235 --- /dev/null +++ b/lib/components/InputField/__snapshots__/InputField.test.tsx.snap @@ -0,0 +1,37 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`InputField > renders help text correctly 1`] = ` + +`; + +exports[`InputField > renders input correctly 1`] = ` + +`; + +exports[`InputField > renders label correctly 1`] = ` + +`; + +exports[`InputField > updates input value correctly 1`] = ` + +`; diff --git a/lib/main.ts b/lib/main.ts index 71ee2be..c06d8d8 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -1,12 +1,15 @@ import './index.css'; export { Button } from './components/Button/Button'; -export { HeroCard } from './components/HeroCard/HeroCard'; export { DropdownMenu } from './components/DropdownMenu/DropdownMenu'; export { Expander } from './components/Expander/Expander'; +export { HeroCard } from './components/HeroCard/HeroCard'; +export { InputField } from './components/InputField/InputField'; export { NavigationBar } from './components/NavigationBar/NavigationBar'; +export { Note } from './components/Note/Note'; export { RadioButton } from './components/RadioButton/RadioButton'; export { RadioButtonGroup } from './components/RadioButton/RadioButtonGroup'; export { ResultsCard } from './components/ResultsCard/ResultsCard'; export { RoundButton } from './components/RoundButton/RoundButton'; export { RoundLinkButton } from './components/RoundLinkButton/RoundLinkButton'; +export { Toast } from './components/Toast/Toast';