aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/forms/fields/input
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-09-22 19:34:01 +0200
committerArmand Philippot <git@armandphilippot.com>2023-10-24 12:23:48 +0200
commita6ff5eee45215effb3344cb5d631a27a7c0369aa (patch)
tree5051747acf72318b4fc5c18d603e3757fbefdfdb /src/components/atoms/forms/fields/input
parent651ea4fc992e77d2f36b3c68f8e7a70644246067 (diff)
refactor(components): rewrite form components
Diffstat (limited to 'src/components/atoms/forms/fields/input')
-rw-r--r--src/components/atoms/forms/fields/input/index.ts1
-rw-r--r--src/components/atoms/forms/fields/input/input.stories.tsx237
-rw-r--r--src/components/atoms/forms/fields/input/input.test.tsx34
-rw-r--r--src/components/atoms/forms/fields/input/input.tsx72
4 files changed, 344 insertions, 0 deletions
diff --git a/src/components/atoms/forms/fields/input/index.ts b/src/components/atoms/forms/fields/input/index.ts
new file mode 100644
index 0000000..e3365cb
--- /dev/null
+++ b/src/components/atoms/forms/fields/input/index.ts
@@ -0,0 +1 @@
+export * from './input';
diff --git a/src/components/atoms/forms/fields/input/input.stories.tsx b/src/components/atoms/forms/fields/input/input.stories.tsx
new file mode 100644
index 0000000..35657f8
--- /dev/null
+++ b/src/components/atoms/forms/fields/input/input.stories.tsx
@@ -0,0 +1,237 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { ChangeEvent, useCallback, useState } from 'react';
+import { Input } from './input';
+
+/**
+ * Input - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Forms/Fields',
+ component: Input,
+ args: {
+ isDisabled: false,
+ isRequired: false,
+ },
+ argTypes: {
+ 'aria-labelledby': {
+ control: {
+ type: 'text',
+ },
+ description: 'One or more ids that refers to the field name.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Add classnames to the field.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ description: 'Input id.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ isDisabled: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Input state: either enabled or disabled.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ isRequired: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the field is required.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ max: {
+ control: {
+ type: 'number',
+ },
+ description: 'Maximum value.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ min: {
+ control: {
+ type: 'number',
+ },
+ description: 'Minimum value.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ name: {
+ control: {
+ type: 'text',
+ },
+ description: 'Input name.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ placeholder: {
+ control: {
+ type: 'text',
+ },
+ description: 'A placeholder value.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ step: {
+ control: {
+ type: 'number',
+ },
+ description: 'Input incremental values that are valid.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ type: {
+ control: {
+ type: 'select',
+ },
+ description: 'Input type: input type or textarea.',
+ options: [
+ 'datetime-local',
+ 'email',
+ 'number',
+ 'search',
+ 'tel',
+ 'text',
+ 'textarea',
+ 'time',
+ 'url',
+ ],
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ value: {
+ control: {
+ type: null,
+ },
+ description: 'Input value.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof Input>;
+
+const Template: ComponentStory<typeof Input> = ({
+ value: initialValue,
+ onChange: _onChange,
+ ...args
+}) => {
+ const [value, setValue] = useState(initialValue);
+ const updateValue = useCallback((e: ChangeEvent<HTMLInputElement>) => {
+ setValue(e.target.value);
+ }, []);
+
+ return <Input value={value} onChange={updateValue} {...args} />;
+};
+
+/**
+ * Input Story - DateTime
+ */
+export const DateTime = Template.bind({});
+DateTime.args = {
+ id: 'field-storybook',
+ name: 'field-storybook',
+ type: 'datetime-local',
+};
+
+/**
+ * Input Story - Email
+ */
+export const Email = Template.bind({});
+Email.args = {
+ id: 'field-storybook',
+ name: 'field-storybook',
+ type: 'email',
+};
+
+/**
+ * Input Story - Numeric
+ */
+export const Numeric = Template.bind({});
+Numeric.args = {
+ id: 'field-storybook',
+ name: 'field-storybook',
+ type: 'number',
+};
+
+/**
+ * Input Story - Text
+ */
+export const Text = Template.bind({});
+Text.args = {
+ id: 'field-storybook',
+ name: 'field-storybook',
+ type: 'text',
+};
+
+/**
+ * Input Story - Time
+ */
+export const Time = Template.bind({});
+Time.args = {
+ id: 'field-storybook',
+ name: 'field-storybook',
+ type: 'time',
+};
diff --git a/src/components/atoms/forms/fields/input/input.test.tsx b/src/components/atoms/forms/fields/input/input.test.tsx
new file mode 100644
index 0000000..1692c9e
--- /dev/null
+++ b/src/components/atoms/forms/fields/input/input.test.tsx
@@ -0,0 +1,34 @@
+import { render, screen } from '../../../../../../tests/utils';
+import { Input } from './input';
+
+const doNothing = () => {
+ // do nothing
+};
+
+describe('Input', () => {
+ it('renders a text input', () => {
+ render(
+ <Input
+ id="text-field"
+ name="text-field"
+ onChange={doNothing}
+ type="text"
+ value=""
+ />
+ );
+ expect(screen.getByRole('textbox')).toHaveAttribute('type', 'text');
+ });
+
+ it('renders a search input', () => {
+ render(
+ <Input
+ id="search-field"
+ name="search-field"
+ onChange={doNothing}
+ type="search"
+ value=""
+ />
+ );
+ expect(screen.getByRole('searchbox')).toHaveAttribute('type', 'search');
+ });
+});
diff --git a/src/components/atoms/forms/fields/input/input.tsx b/src/components/atoms/forms/fields/input/input.tsx
new file mode 100644
index 0000000..0f0736c
--- /dev/null
+++ b/src/components/atoms/forms/fields/input/input.tsx
@@ -0,0 +1,72 @@
+import {
+ type ForwardedRef,
+ forwardRef,
+ type InputHTMLAttributes,
+ type HTMLInputTypeAttribute,
+} from 'react';
+import styles from '../fields.module.scss';
+
+export type InputProps = Omit<
+ InputHTMLAttributes<HTMLInputElement>,
+ 'disabled' | 'hidden' | 'readonly' | 'required' | 'type'
+> &
+ Required<Pick<InputHTMLAttributes<HTMLInputElement>, 'id' | 'name'>> & {
+ /**
+ * Should the field be disabled?
+ *
+ * @default false
+ */
+ isDisabled?: boolean;
+ /**
+ * Should the field be hidden?
+ *
+ * @default false
+ */
+ isHidden?: boolean;
+ /**
+ * Should the field be readonly?
+ *
+ * @default false
+ */
+ isReadOnly?: boolean;
+ /**
+ * Should the field be required?
+ *
+ * @default false
+ */
+ isRequired?: boolean;
+ /**
+ * The input type.
+ */
+ type: Exclude<HTMLInputTypeAttribute, 'checkbox' | 'radio' | 'range'>;
+ };
+
+const InputWithRef = (
+ {
+ className = '',
+ isDisabled = false,
+ isHidden = false,
+ isReadOnly = false,
+ isRequired = false,
+ ...props
+ }: InputProps,
+ ref: ForwardedRef<HTMLInputElement>
+) => {
+ const fieldClassName = `${styles.field} ${className}`;
+
+ return (
+ <input
+ {...props}
+ className={fieldClassName}
+ disabled={isDisabled}
+ readOnly={isReadOnly}
+ ref={ref}
+ required={isRequired}
+ />
+ );
+};
+
+/**
+ * Input component.
+ */
+export const Input = forwardRef(InputWithRef);