Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OPHJOD-294: Add input field component #31

Merged
merged 1 commit into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions lib/components/InputField/InputField.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof InputField>;

export default meta;

type Story = StoryObj<typeof meta>;

const render = (args: Story['args']) => {
const { value, onChange, ...rest } = args;
const [textValue, setTextValue] = useState(value);
return (
<InputField
value={textValue}
onChange={(newValue) => {
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) => (
<div className="max-w-[415px]">
<Story />
</div>
),
],
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) => (
<div className="max-w-[415px]">
<Story />
</div>
),
],
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) => (
<div className="max-w-[415px]">
<Story />
</div>
),
],
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,
},
};
50 changes: 50 additions & 0 deletions lib/components/InputField/InputField.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<InputField value={value} onChange={onChange} label={label} />);
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(
<InputField value={value} onChange={onChange} label={label} placeholder={placeholder} />,
);
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(<InputField value={value} onChange={onChange} label={label} help={helpText} />);
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(<InputField value={value} onChange={onChange} label={label} />);
const inputElement = screen.getByRole('textbox');
fireEvent.change(inputElement, { target: { value: 'new value' } });
expect(onChange).toHaveBeenCalledWith('new value');
expect(container.firstChild).toMatchSnapshot();
});
});
36 changes: 36 additions & 0 deletions lib/components/InputField/InputField.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<label htmlFor={inputId} className="mb-4 inline-block align-top text-form-label text-primary-gray">
{label}
</label>
<input
id={inputId}
type="text"
value={value}
onChange={(event) => 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 && (
<div id={helpId} className="mt-2 block text-help text-secondary-3">
{help}
</div>
)}
</>
);
};
37 changes: 37 additions & 0 deletions lib/components/InputField/__snapshots__/InputField.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`InputField > renders help text correctly 1`] = `
<label
class="mb-4 inline-block align-top text-form-label text-primary-gray"
for=":r4:"
>
Username
</label>
`;

exports[`InputField > renders input correctly 1`] = `
<label
class="mb-4 inline-block align-top text-form-label text-primary-gray"
for=":r2:"
>
Username
</label>
`;

exports[`InputField > renders label correctly 1`] = `
<label
class="mb-4 inline-block align-top text-form-label text-primary-gray"
for=":r0:"
>
Username
</label>
`;

exports[`InputField > updates input value correctly 1`] = `
<label
class="mb-4 inline-block align-top text-form-label text-primary-gray"
for=":r6:"
>
Username
</label>
`;
5 changes: 4 additions & 1 deletion lib/main.ts
Original file line number Diff line number Diff line change
@@ -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';