aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/atoms')
-rw-r--r--src/components/atoms/forms/field.stories.tsx45
-rw-r--r--src/components/atoms/forms/field.tsx21
-rw-r--r--src/components/atoms/forms/form.test.tsx9
-rw-r--r--src/components/atoms/forms/form.tsx73
-rw-r--r--src/components/atoms/forms/forms.module.scss23
-rw-r--r--src/components/atoms/forms/label.module.scss17
-rw-r--r--src/components/atoms/forms/label.stories.tsx42
-rw-r--r--src/components/atoms/forms/label.test.tsx2
-rw-r--r--src/components/atoms/forms/label.tsx32
-rw-r--r--src/components/atoms/forms/select.stories.tsx44
-rw-r--r--src/components/atoms/forms/select.tsx25
-rw-r--r--src/components/atoms/forms/toggle.module.scss2
-rw-r--r--src/components/atoms/forms/toggle.tsx7
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}