diff options
Diffstat (limited to 'src/components')
| -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; +    } +  } +} | 
