summaryrefslogtreecommitdiffstats
path: root/src/components/atoms
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-03-31 17:57:39 +0200
committerArmand Philippot <git@armandphilippot.com>2022-03-31 17:57:39 +0200
commitb145ed4492de834f5cea9437e9772c4f7fbe90ec (patch)
tree76a0b99d5106bc30719bba9e7f13ba30f42e9d8c /src/components/atoms
parent8370602f37ad6aa02485d85e5b179b76c3f15701 (diff)
chore: add a Field component
Diffstat (limited to 'src/components/atoms')
-rw-r--r--src/components/atoms/forms/field.stories.tsx176
-rw-r--r--src/components/atoms/forms/field.test.tsx14
-rw-r--r--src/components/atoms/forms/field.tsx94
-rw-r--r--src/components/atoms/forms/forms.module.scss39
4 files changed, 323 insertions, 0 deletions
diff --git a/src/components/atoms/forms/field.stories.tsx b/src/components/atoms/forms/field.stories.tsx
new file mode 100644
index 0000000..0406f10
--- /dev/null
+++ b/src/components/atoms/forms/field.stories.tsx
@@ -0,0 +1,176 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import FieldComponent from './field';
+
+export default {
+ title: 'Atoms/Forms',
+ component: FieldComponent,
+ args: {
+ disabled: false,
+ required: false,
+ setValue: () => null,
+ type: 'text',
+ value: '',
+ },
+ argTypes: {
+ disabled: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Field state: either enabled or disabled.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field id.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ 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: 'Field name.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ placeholder: {
+ control: {
+ type: 'text',
+ },
+ description: 'A placeholder value.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ required: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the field is required.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ setValue: {
+ control: {
+ type: null,
+ },
+ description: 'Callback function to set field value.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ step: {
+ control: {
+ type: 'number',
+ },
+ description: 'Field incremental values that are valid.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ type: {
+ control: {
+ type: 'select',
+ },
+ description: 'Field type: input type or textarea.',
+ options: [
+ 'datetime-local',
+ 'email',
+ 'number',
+ 'search',
+ 'tel',
+ 'text',
+ 'textarea',
+ 'time',
+ 'url',
+ ],
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ value: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field value.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof FieldComponent>;
+
+const Template: ComponentStory<typeof FieldComponent> = (args) => (
+ <FieldComponent {...args} />
+);
+
+export const Field = Template.bind({});
+Field.args = {
+ setValue: () => null,
+ value: '',
+};
diff --git a/src/components/atoms/forms/field.test.tsx b/src/components/atoms/forms/field.test.tsx
new file mode 100644
index 0000000..5488220
--- /dev/null
+++ b/src/components/atoms/forms/field.test.tsx
@@ -0,0 +1,14 @@
+import { render, screen } from '@test-utils';
+import Field from './field';
+
+describe('Field', () => {
+ it('renders a text input', () => {
+ render(<Field type="text" value="" setValue={() => null} />);
+ expect(screen.getByRole('textbox')).toHaveAttribute('type', 'text');
+ });
+
+ it('renders a search input', () => {
+ render(<Field type="search" value="" setValue={() => null} />);
+ expect(screen.getByRole('searchbox')).toHaveAttribute('type', 'search');
+ });
+});
diff --git a/src/components/atoms/forms/field.tsx b/src/components/atoms/forms/field.tsx
new file mode 100644
index 0000000..7d1ac93
--- /dev/null
+++ b/src/components/atoms/forms/field.tsx
@@ -0,0 +1,94 @@
+import { ChangeEvent, FC, SetStateAction } from 'react';
+import styles from './forms.module.scss';
+
+type FieldType =
+ | 'datetime-local'
+ | 'email'
+ | 'number'
+ | 'search'
+ | 'tel'
+ | 'text'
+ | 'textarea'
+ | 'time'
+ | 'url';
+
+type FieldProps = {
+ /**
+ * Field state. Either enabled (false) or disabled (true).
+ */
+ disabled?: boolean;
+ /**
+ * Field id attribute.
+ */
+ id?: string;
+ /**
+ * Field maximum value.
+ */
+ max?: number | string;
+ /**
+ * Field minimum value.
+ */
+ min?: number | string;
+ /**
+ * Field name attribute.
+ */
+ name?: string;
+ /**
+ * Placeholder value.
+ */
+ placeholder?: string;
+ /**
+ * True if the field is required. Default: false.
+ */
+ required?: boolean;
+ /**
+ * Callback function to set field value.
+ */
+ setValue: (value: SetStateAction<string>) => void;
+ /**
+ * Field incremental values that are valid.
+ */
+ step?: number | string;
+ /**
+ * Field type. Default: text.
+ */
+ type: FieldType;
+ /**
+ * Field value.
+ */
+ value: string;
+};
+
+/**
+ * Field component.
+ *
+ * Render either an input or a textarea.
+ */
+const Field: FC<FieldProps> = ({ setValue, type, ...props }) => {
+ /**
+ * Update select value when an option is selected.
+ * @param e - The option change event.
+ */
+ const updateValue = (
+ e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
+ ) => {
+ setValue(e.target.value);
+ };
+
+ return type === 'textarea' ? (
+ <textarea
+ onChange={updateValue}
+ className={`${styles.field} ${styles['field--textarea']}`}
+ {...props}
+ />
+ ) : (
+ <input
+ type={type}
+ onChange={updateValue}
+ className={styles.field}
+ {...props}
+ />
+ );
+};
+
+export default Field;
diff --git a/src/components/atoms/forms/forms.module.scss b/src/components/atoms/forms/forms.module.scss
new file mode 100644
index 0000000..5347bad
--- /dev/null
+++ b/src/components/atoms/forms/forms.module.scss
@@ -0,0 +1,39 @@
+@use "@styles/abstracts/functions" as fun;
+
+.field {
+ width: 100%;
+ padding: var(--spacing-2xs) var(--spacing-xs);
+ background: var(--color-bg-tertiary);
+ border: fun.convert-px(2) solid var(--color-border);
+ box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow);
+ transition: all 0.25s linear 0s;
+
+ &--textarea {
+ min-height: fun.convert-px(200);
+ }
+
+ &:disabled {
+ background: var(--color-bg-secondary);
+ border: fun.convert-px(2) solid var(--color-border-light);
+ box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0
+ var(--color-shadow-light);
+ cursor: not-allowed;
+ }
+
+ &:not(:disabled) {
+ &:hover {
+ box-shadow: fun.convert-px(5) fun.convert-px(5) 0 fun.convert-px(1)
+ var(--color-shadow);
+ transform: translate(#{fun.convert-px(-3)}, #{fun.convert-px(-3)});
+ }
+
+ &:focus {
+ background: var(--color-bg);
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 0 var(--color-shadow);
+ transform: translate(#{fun.convert-px(3)}, #{fun.convert-px(3)});
+ outline: none;
+ transition: all 0.2s ease-in-out 0s, transform 0.3s ease-out 0s;
+ }
+ }
+}