diff options
Diffstat (limited to 'src/components/atoms')
| -rw-r--r-- | src/components/atoms/forms/field.stories.tsx | 45 | ||||
| -rw-r--r-- | src/components/atoms/forms/field.tsx | 21 | ||||
| -rw-r--r-- | src/components/atoms/forms/form.test.tsx | 9 | ||||
| -rw-r--r-- | src/components/atoms/forms/form.tsx | 73 | ||||
| -rw-r--r-- | src/components/atoms/forms/forms.module.scss | 23 | ||||
| -rw-r--r-- | src/components/atoms/forms/label.module.scss | 17 | ||||
| -rw-r--r-- | src/components/atoms/forms/label.stories.tsx | 42 | ||||
| -rw-r--r-- | src/components/atoms/forms/label.test.tsx | 2 | ||||
| -rw-r--r-- | src/components/atoms/forms/label.tsx | 32 | ||||
| -rw-r--r-- | src/components/atoms/forms/select.stories.tsx | 44 | ||||
| -rw-r--r-- | src/components/atoms/forms/select.tsx | 25 | ||||
| -rw-r--r-- | src/components/atoms/forms/toggle.module.scss | 2 | ||||
| -rw-r--r-- | src/components/atoms/forms/toggle.tsx | 7 | 
13 files changed, 287 insertions, 55 deletions
| diff --git a/src/components/atoms/forms/field.stories.tsx b/src/components/atoms/forms/field.stories.tsx index 02681e7..ec81922 100644 --- a/src/components/atoms/forms/field.stories.tsx +++ b/src/components/atoms/forms/field.stories.tsx @@ -1,4 +1,5 @@  import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { useState } from 'react';  import FieldComponent from './field';  export default { @@ -7,11 +8,35 @@ export default {    args: {      disabled: false,      required: false, -    setValue: () => null,      type: 'text', -    value: '',    },    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, +      }, +    },      disabled: {        control: {          type: 'boolean', @@ -148,7 +173,7 @@ export default {      },      value: {        control: { -        type: 'text', +        type: null,        },        description: 'Field value.',        type: { @@ -159,14 +184,18 @@ export default {    },  } as ComponentMeta<typeof FieldComponent>; -const Template: ComponentStory<typeof FieldComponent> = (args) => ( -  <FieldComponent {...args} /> -); +const Template: ComponentStory<typeof FieldComponent> = ({ +  value: _value, +  setValue: _setValue, +  ...args +}) => { +  const [value, setValue] = useState<string>(''); + +  return <FieldComponent value={value} setValue={setValue} {...args} />; +};  export const Field = Template.bind({});  Field.args = {    id: 'field-storybook',    name: 'field-storybook', -  setValue: () => null, -  value: '',  }; diff --git a/src/components/atoms/forms/field.tsx b/src/components/atoms/forms/field.tsx index 513d2ba..2e75d0f 100644 --- a/src/components/atoms/forms/field.tsx +++ b/src/components/atoms/forms/field.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, FC, SetStateAction } from 'react'; +import { ChangeEvent, SetStateAction, VFC } from 'react';  import styles from './forms.module.scss';  export type FieldType = @@ -14,6 +14,14 @@ export type FieldType =  export type FieldProps = {    /** +   * One or more ids that refers to the field name. +   */ +  'aria-labelledby'?: string; +  /** +   * Add classnames to the field. +   */ +  className?: string; +  /**     * Field state. Either enabled (false) or disabled (true).     */    disabled?: boolean; @@ -64,7 +72,12 @@ export type FieldProps = {   *   * Render either an input or a textarea.   */ -const Field: FC<FieldProps> = ({ setValue, type, ...props }) => { +const Field: VFC<FieldProps> = ({ +  className = '', +  setValue, +  type, +  ...props +}) => {    /**     * Update select value when an option is selected.     * @param e - The option change event. @@ -78,14 +91,14 @@ const Field: FC<FieldProps> = ({ setValue, type, ...props }) => {    return type === 'textarea' ? (      <textarea        onChange={updateValue} -      className={`${styles.field} ${styles['field--textarea']}`} +      className={`${styles.field} ${styles['field--textarea']} ${className}`}        {...props}      />    ) : (      <input        type={type}        onChange={updateValue} -      className={styles.field} +      className={`${styles.field} ${className}`}        {...props}      />    ); diff --git a/src/components/atoms/forms/form.test.tsx b/src/components/atoms/forms/form.test.tsx new file mode 100644 index 0000000..9cd3c58 --- /dev/null +++ b/src/components/atoms/forms/form.test.tsx @@ -0,0 +1,9 @@ +import { render, screen } from '@test-utils'; +import Form from './form'; + +describe('Form', () => { +  it('renders a form', () => { +    render(<Form aria-label="Jest form" onSubmit={() => null}></Form>); +    expect(screen.getByRole('form', { name: 'Jest form' })).toBeInTheDocument(); +  }); +}); diff --git a/src/components/atoms/forms/form.tsx b/src/components/atoms/forms/form.tsx new file mode 100644 index 0000000..8e80930 --- /dev/null +++ b/src/components/atoms/forms/form.tsx @@ -0,0 +1,73 @@ +import { Children, FC, FormEvent, Fragment } from 'react'; +import styles from './forms.module.scss'; + +export type FormProps = { +  /** +   * An accessible name. +   */ +  'aria-label'?: string; +  /** +   * One or more ids that refers to the form name. +   */ +  'aria-labelledby'?: string; +  /** +   * Set additional classnames to the form wrapper. +   */ +  className?: string; +  /** +   * Wrap each items with a div. Default: true. +   */ +  grouped?: boolean; +  /** +   * A callback function to execute on submit. +   */ +  onSubmit: () => void; +}; + +/** + * Form component. + * + * Render children wrapped in a form element. + */ +const Form: FC<FormProps> = ({ +  children, +  className = '', +  grouped = true, +  onSubmit, +  ...props +}) => { +  const arrayChildren = Children.toArray(children); + +  /** +   * Get the form items. +   * @returns {JSX.Element[]} An array of child elements wrapped in a div. +   */ +  const getFormItems = (): JSX.Element[] => { +    return arrayChildren.map((child, index) => +      grouped ? ( +        <div key={`item-${index}`} className={styles.item}> +          {child} +        </div> +      ) : ( +        <Fragment key={`item-${index}`}>{child}</Fragment> +      ) +    ); +  }; + +  /** +   * Handle form submit. +   * @param {FormEvent} e - The form event. +   */ +  const handleSubmit = (e: FormEvent) => { +    e.preventDefault(); +    onSubmit(); +  }; + +  return ( +    <form onSubmit={handleSubmit} className={className} {...props}> +      {getFormItems()} +    </form> +  ); +}; + +export default Form; diff --git a/src/components/atoms/forms/forms.module.scss b/src/components/atoms/forms/forms.module.scss index 689a318..279c185 100644 --- a/src/components/atoms/forms/forms.module.scss +++ b/src/components/atoms/forms/forms.module.scss @@ -1,7 +1,12 @@  @use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +.item { +  margin: var(--spacing-xs) 0; +  max-width: 45ch; +}  .field { -  width: 100%;    padding: var(--spacing-2xs) var(--spacing-xs);    background: var(--color-bg-tertiary);    border: fun.convert-px(2) solid var(--color-border); @@ -10,6 +15,10 @@    &--select {      cursor: pointer; + +    @include mix.pointer("fine") { +      padding: fun.convert-px(3) var(--spacing-xs); +    }    }    &--textarea { @@ -41,15 +50,3 @@      }    }  } - -.label { -  display: block; -  color: var(--color-primary-darker); -  font-size: var(--font-size-sm); -  font-variant: small-caps; -  font-weight: 600; -} - -.required { -  color: var(--color-secondary); -} diff --git a/src/components/atoms/forms/label.module.scss b/src/components/atoms/forms/label.module.scss new file mode 100644 index 0000000..f900925 --- /dev/null +++ b/src/components/atoms/forms/label.module.scss @@ -0,0 +1,17 @@ +.label { +  color: var(--color-primary-darker); +  font-weight: 600; + +  &--small { +    font-size: var(--font-size-sm); +    font-variant: small-caps; +  } + +  &--medium { +    font-size: var(--font-size-md); +  } +} + +.required { +  color: var(--color-secondary); +} diff --git a/src/components/atoms/forms/label.stories.tsx b/src/components/atoms/forms/label.stories.tsx index 06e8eb9..463e8ac 100644 --- a/src/components/atoms/forms/label.stories.tsx +++ b/src/components/atoms/forms/label.stories.tsx @@ -4,7 +4,24 @@ import LabelComponent from './label';  export default {    title: 'Atoms/Forms',    component: LabelComponent, +  args: { +    required: false, +    size: 'small', +  },    argTypes: { +    className: { +      control: { +        type: 'text', +      }, +      description: 'Add classnames to the label.', +      table: { +        category: 'Styles', +      }, +      type: { +        name: 'string', +        required: false, +      }, +    },      children: {        control: {          type: 'text', @@ -32,22 +49,37 @@ export default {        description: 'Set to true if the field is required.',        table: {          category: 'Options', +        defaultValue: { summary: false },        },        type: {          name: 'boolean',          required: false,        },      }, +    size: { +      control: { +        type: 'select', +      }, +      description: 'The label size.', +      options: ['medium', 'small'], +      table: { +        category: 'Options', +        defaultValue: { summary: 'small' }, +      }, +      type: { +        name: 'string', +        required: false, +      }, +    },    },  } as ComponentMeta<typeof LabelComponent>; -const Template: ComponentStory<typeof LabelComponent> = (args) => { -  const { children, ...props } = args; -  return <LabelComponent {...props}>{children}</LabelComponent>; -}; +const Template: ComponentStory<typeof LabelComponent> = ({ +  children, +  ...args +}) => <LabelComponent {...args}>{children}</LabelComponent>;  export const Label = Template.bind({});  Label.args = {    children: 'A label', -  htmlFor: 'a-field-id',  }; diff --git a/src/components/atoms/forms/label.test.tsx b/src/components/atoms/forms/label.test.tsx index fcf1731..14257c3 100644 --- a/src/components/atoms/forms/label.test.tsx +++ b/src/components/atoms/forms/label.test.tsx @@ -3,7 +3,7 @@ import Label from './label';  describe('Label', () => {    it('renders a field label', () => { -    render(<Label htmlFor="a-field-id">A label</Label>); +    render(<Label>A label</Label>);      expect(screen.getByText('A label')).toBeDefined();    });  }); diff --git a/src/components/atoms/forms/label.tsx b/src/components/atoms/forms/label.tsx index 860cd73..8d57ee2 100644 --- a/src/components/atoms/forms/label.tsx +++ b/src/components/atoms/forms/label.tsx @@ -1,9 +1,23 @@  import { FC } from 'react'; -import styles from './forms.module.scss'; +import styles from './label.module.scss'; -type LabelProps = { -  htmlFor: string; +export type LabelProps = { +  /** +   * Add classnames to the label. +   */ +  className?: string; +  /** +   * The field id. +   */ +  htmlFor?: string; +  /** +   * Is the field required? Default: false. +   */    required?: boolean; +  /** +   * The label size. Default: small. +   */ +  size?: 'medium' | 'small';  };  /** @@ -11,9 +25,17 @@ type LabelProps = {   *   * Render a HTML label element.   */ -const Label: FC<LabelProps> = ({ children, required = false, ...props }) => { +const Label: FC<LabelProps> = ({ +  children, +  className = '', +  required = false, +  size = 'small', +  ...props +}) => { +  const sizeClass = styles[`label--${size}`]; +    return ( -    <label className={styles.label} {...props}> +    <label className={`${styles.label} ${sizeClass} ${className}`} {...props}>        {children}        {required && <span className={styles.required}> *</span>}      </label> diff --git a/src/components/atoms/forms/select.stories.tsx b/src/components/atoms/forms/select.stories.tsx index c7bb253..c2fb8c6 100644 --- a/src/components/atoms/forms/select.stories.tsx +++ b/src/components/atoms/forms/select.stories.tsx @@ -1,4 +1,5 @@  import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { useState } from 'react';  import SelectComponent from './select';  const selectOptions = [ @@ -10,14 +11,31 @@ const selectOptions = [  export default {    title: 'Atoms/Forms',    component: SelectComponent, +  args: { +    disabled: false, +    required: false, +  },    argTypes: { -    classes: { +    'aria-labelledby': {        control: {          type: 'text',        }, -      description: 'Set additional classes', +      description: 'One or more ids that refers to the select field name.',        table: { -        category: 'Options', +        category: 'Accessibility', +      }, +      type: { +        name: 'string', +        required: false, +      }, +    }, +    className: { +      control: { +        type: 'text', +      }, +      description: 'Add classnames to the select field.', +      table: { +        category: 'Styles',        },        type: {          name: 'string', @@ -59,9 +77,6 @@ export default {        },      },      options: { -      control: { -        type: null, -      },        description: 'Select options.',        type: {          name: 'array', @@ -100,7 +115,7 @@ export default {      },      value: {        control: { -        type: 'text', +        type: null,        },        description: 'Field value.',        type: { @@ -111,13 +126,20 @@ export default {    },  } as ComponentMeta<typeof SelectComponent>; -const Template: ComponentStory<typeof SelectComponent> = (args) => ( -  <SelectComponent {...args} /> -); +const Template: ComponentStory<typeof SelectComponent> = ({ +  value, +  setValue: _setValue, +  ...args +}) => { +  const [selected, setSelected] = useState<string>(value); + +  return <SelectComponent value={selected} setValue={setSelected} {...args} />; +};  export const Select = Template.bind({});  Select.args = { +  id: 'storybook-select', +  name: 'storybook-select',    options: selectOptions, -  setValue: () => null,    value: 'option2',  }; diff --git a/src/components/atoms/forms/select.tsx b/src/components/atoms/forms/select.tsx index a42dbda..25e86e0 100644 --- a/src/components/atoms/forms/select.tsx +++ b/src/components/atoms/forms/select.tsx @@ -1,17 +1,30 @@ -import { ChangeEvent, FC, SetStateAction } from 'react'; +import { ChangeEvent, SetStateAction, VFC } from 'react';  import styles from './forms.module.scss';  export type SelectOptions = { +  /** +   * The option id. +   */    id: string; +  /** +   * The option name. +   */    name: string; +  /** +   * The option value. +   */    value: string;  };  export type SelectProps = {    /** -   * Set additional classes. +   * One or more ids that refers to the select field name. +   */ +  'aria-labelledby'?: string; +  /** +   * Add classnames to the select field.     */ -  classes?: string; +  className?: string;    /**     * Field state. Either enabled (false) or disabled (true).     */ @@ -47,8 +60,8 @@ export type SelectProps = {   *   * Render a HTML select element.   */ -const Select: FC<SelectProps> = ({ -  classes = '', +const Select: VFC<SelectProps> = ({ +  className = '',    options,    setValue,    ...props @@ -74,7 +87,7 @@ const Select: FC<SelectProps> = ({    return (      <select -      className={`${styles.field} ${styles['field--select']} ${classes}`} +      className={`${styles.field} ${styles['field--select']} ${className}`}        onChange={updateValue}        {...props}      > diff --git a/src/components/atoms/forms/toggle.module.scss b/src/components/atoms/forms/toggle.module.scss index 24b867e..2e8a49f 100644 --- a/src/components/atoms/forms/toggle.module.scss +++ b/src/components/atoms/forms/toggle.module.scss @@ -10,7 +10,7 @@  }  .title { -  margin-right: auto; +  margin-right: var(--spacing-2xs);  }  .toggle { diff --git a/src/components/atoms/forms/toggle.tsx b/src/components/atoms/forms/toggle.tsx index 7ef40ed..c3bc09d 100644 --- a/src/components/atoms/forms/toggle.tsx +++ b/src/components/atoms/forms/toggle.tsx @@ -27,6 +27,10 @@ export type ToggleProps = {     */    label: string;    /** +   * Set additional classnames to the label. +   */ +  labelClassName?: string; +  /**     * The label size.     */    labelSize?: LabelProps['size']; @@ -53,6 +57,7 @@ const Toggle: VFC<ToggleProps> = ({    choices,    id,    label, +  labelClassName = '',    labelSize,    name,    setValue, @@ -69,7 +74,7 @@ const Toggle: VFC<ToggleProps> = ({          className={styles.checkbox}        />        <Label size={labelSize} htmlFor={id} className={styles.label}> -        <span className={styles.title}>{label}</span> +        <span className={`${styles.title} ${labelClassName}`}>{label}</span>          {choices.left}          <span className={styles.toggle}></span>          {choices.right} | 
