diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-03-31 17:57:39 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-03-31 17:57:39 +0200 |
| commit | b145ed4492de834f5cea9437e9772c4f7fbe90ec (patch) | |
| tree | 76a0b99d5106bc30719bba9e7f13ba30f42e9d8c | |
| parent | 8370602f37ad6aa02485d85e5b179b76c3f15701 (diff) | |
chore: add a Field component
| -rw-r--r-- | src/components/atoms/forms/field.stories.tsx | 176 | ||||
| -rw-r--r-- | src/components/atoms/forms/field.test.tsx | 14 | ||||
| -rw-r--r-- | src/components/atoms/forms/field.tsx | 94 | ||||
| -rw-r--r-- | src/components/atoms/forms/forms.module.scss | 39 |
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; + } + } +} |
