aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-04-08 22:36:24 +0200
committerArmand Philippot <git@armandphilippot.com>2022-04-08 23:31:58 +0200
commit0b3146f7278929c4d1b33dd8f94f34e351e5e5a9 (patch)
tree6a784b197a283a7da07c2e1df80a29fee8b3790a /src
parent61278678ea8a8febee0574cd0f6006492d7b15cb (diff)
chore: add a Settings modal component
Diffstat (limited to 'src')
-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
-rw-r--r--src/components/molecules/forms/labelled-field.module.scss9
-rw-r--r--src/components/molecules/forms/labelled-field.stories.tsx33
-rw-r--r--src/components/molecules/forms/labelled-field.tsx34
-rw-r--r--src/components/molecules/forms/labelled-select.module.scss9
-rw-r--r--src/components/molecules/forms/labelled-select.stories.tsx80
-rw-r--r--src/components/molecules/forms/labelled-select.tsx51
-rw-r--r--src/components/molecules/forms/motion-toggle.stories.tsx15
-rw-r--r--src/components/molecules/forms/motion-toggle.tsx8
-rw-r--r--src/components/molecules/forms/prism-theme-toggle.stories.tsx15
-rw-r--r--src/components/molecules/forms/prism-theme-toggle.tsx11
-rw-r--r--src/components/molecules/forms/select-with-tooltip.module.scss10
-rw-r--r--src/components/molecules/forms/select-with-tooltip.stories.tsx77
-rw-r--r--src/components/molecules/forms/select-with-tooltip.tsx34
-rw-r--r--src/components/molecules/forms/theme-toggle.stories.tsx15
-rw-r--r--src/components/molecules/forms/theme-toggle.tsx8
-rw-r--r--src/components/organisms/modals/settings-modal.module.scss14
-rw-r--r--src/components/organisms/modals/settings-modal.stories.tsx31
-rw-r--r--src/components/organisms/modals/settings-modal.test.tsx34
-rw-r--r--src/components/organisms/modals/settings-modal.tsx51
32 files changed, 761 insertions, 120 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}
diff --git a/src/components/molecules/forms/labelled-field.module.scss b/src/components/molecules/forms/labelled-field.module.scss
new file mode 100644
index 0000000..64ef3d0
--- /dev/null
+++ b/src/components/molecules/forms/labelled-field.module.scss
@@ -0,0 +1,9 @@
+.label {
+ &--left {
+ margin-right: var(--spacing-2xs);
+ }
+
+ &--top {
+ display: block;
+ }
+}
diff --git a/src/components/molecules/forms/labelled-field.stories.tsx b/src/components/molecules/forms/labelled-field.stories.tsx
index eb7f8b5..b77d71e 100644
--- a/src/components/molecules/forms/labelled-field.stories.tsx
+++ b/src/components/molecules/forms/labelled-field.stories.tsx
@@ -1,4 +1,5 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
import LabelledFieldComponent from './labelled-field';
export default {
@@ -6,6 +7,7 @@ export default {
component: LabelledFieldComponent,
args: {
disabled: false,
+ labelPosition: 'top',
required: false,
},
argTypes: {
@@ -43,6 +45,21 @@ export default {
required: true,
},
},
+ labelPosition: {
+ control: {
+ type: 'select',
+ },
+ description: 'The label position.',
+ options: ['left', 'top'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'top' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
max: {
control: {
type: 'number',
@@ -155,7 +172,7 @@ export default {
},
value: {
control: {
- type: 'text',
+ type: null,
},
description: 'Field value.',
type: {
@@ -166,15 +183,19 @@ export default {
},
} as ComponentMeta<typeof LabelledFieldComponent>;
-const Template: ComponentStory<typeof LabelledFieldComponent> = (args) => (
- <LabelledFieldComponent {...args} />
-);
+const Template: ComponentStory<typeof LabelledFieldComponent> = ({
+ value: _value,
+ setValue: _setValue,
+ ...args
+}) => {
+ const [value, setValue] = useState<string>('');
+
+ return <LabelledFieldComponent value={value} setValue={setValue} {...args} />;
+};
export const LabelledField = Template.bind({});
LabelledField.args = {
id: 'labelled-field-storybook',
label: 'Labelled field',
name: 'labelled-field-storybook',
- setValue: () => null,
- value: '',
};
diff --git a/src/components/molecules/forms/labelled-field.tsx b/src/components/molecules/forms/labelled-field.tsx
index 7f81e23..08d0126 100644
--- a/src/components/molecules/forms/labelled-field.tsx
+++ b/src/components/molecules/forms/labelled-field.tsx
@@ -1,20 +1,46 @@
import Field, { type FieldProps } from '@components/atoms/forms/field';
import Label from '@components/atoms/forms/label';
-import { FC } from 'react';
+import { VFC } from 'react';
+import styles from './labelled-field.module.scss';
-type LabelledFieldProps = FieldProps & {
+export type LabelledFieldProps = FieldProps & {
+ /**
+ * Visually hide the field label. Default: false.
+ */
+ hideLabel?: boolean;
+ /**
+ * The field label.
+ */
label: string;
+ /**
+ * The label position. Default: top.
+ */
+ labelPosition?: 'left' | 'top';
};
-const LabelledField: FC<LabelledFieldProps> = ({
+/**
+ * LabelledField component
+ *
+ * Render a field tied to a label.
+ */
+const LabelledField: VFC<LabelledFieldProps> = ({
+ hideLabel = false,
id,
label,
+ labelPosition = 'top',
required,
...props
}) => {
+ const positionModifier = `label--${labelPosition}`;
+ const visibilityClass = hideLabel ? 'screen-reader-text' : '';
+
return (
<>
- <Label htmlFor={id} required={required}>
+ <Label
+ htmlFor={id}
+ required={required}
+ className={`${visibilityClass} ${styles[positionModifier]}`}
+ >
{label}
</Label>
<Field id={id} required={required} {...props} />
diff --git a/src/components/molecules/forms/labelled-select.module.scss b/src/components/molecules/forms/labelled-select.module.scss
new file mode 100644
index 0000000..64ef3d0
--- /dev/null
+++ b/src/components/molecules/forms/labelled-select.module.scss
@@ -0,0 +1,9 @@
+.label {
+ &--left {
+ margin-right: var(--spacing-2xs);
+ }
+
+ &--top {
+ display: block;
+ }
+}
diff --git a/src/components/molecules/forms/labelled-select.stories.tsx b/src/components/molecules/forms/labelled-select.stories.tsx
index 0966e13..0c569f5 100644
--- a/src/components/molecules/forms/labelled-select.stories.tsx
+++ b/src/components/molecules/forms/labelled-select.stories.tsx
@@ -1,4 +1,5 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
import LabelledSelectComponent from './labelled-select';
const selectOptions = [
@@ -12,6 +13,7 @@ export default {
component: LabelledSelectComponent,
args: {
disabled: false,
+ labelPosition: 'top',
required: false,
},
argTypes: {
@@ -49,6 +51,48 @@ export default {
required: true,
},
},
+ labelClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the label.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ labelPosition: {
+ control: {
+ type: 'select',
+ },
+ description: 'The label position.',
+ options: ['left', 'top'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'top' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ labelSize: {
+ control: {
+ type: 'select',
+ },
+ description: 'The label size.',
+ options: ['medium', 'small'],
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
name: {
control: {
type: 'text',
@@ -86,6 +130,19 @@ export default {
required: false,
},
},
+ selectClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the select field.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
setValue: {
control: {
type: null,
@@ -101,7 +158,7 @@ export default {
},
value: {
control: {
- type: 'text',
+ type: null,
},
description: 'Field value.',
type: {
@@ -112,9 +169,21 @@ export default {
},
} as ComponentMeta<typeof LabelledSelectComponent>;
-const Template: ComponentStory<typeof LabelledSelectComponent> = (args) => (
- <LabelledSelectComponent {...args} />
-);
+const Template: ComponentStory<typeof LabelledSelectComponent> = ({
+ value,
+ setValue: _setValue,
+ ...args
+}) => {
+ const [selected, setSelected] = useState<string>(value);
+
+ return (
+ <LabelledSelectComponent
+ value={selected}
+ setValue={setSelected}
+ {...args}
+ />
+ );
+};
export const LabelledSelect = Template.bind({});
LabelledSelect.args = {
@@ -122,6 +191,5 @@ LabelledSelect.args = {
label: 'Labelled select',
name: 'labelled-select-storybook',
options: selectOptions,
- setValue: () => null,
- value: '',
+ value: 'option1',
};
diff --git a/src/components/molecules/forms/labelled-select.tsx b/src/components/molecules/forms/labelled-select.tsx
index 442e91a..7d4237a 100644
--- a/src/components/molecules/forms/labelled-select.tsx
+++ b/src/components/molecules/forms/labelled-select.tsx
@@ -1,23 +1,62 @@
-import Label from '@components/atoms/forms/label';
+import Label, { LabelProps } from '@components/atoms/forms/label';
import Select, { type SelectProps } from '@components/atoms/forms/select';
-import { FC } from 'react';
+import { VFC } from 'react';
+import styles from './labelled-select.module.scss';
-type LabelledSelectProps = SelectProps & {
+export type LabelledSelectProps = Omit<
+ SelectProps,
+ 'aria-labelledby' | 'className'
+> & {
+ /**
+ * The field label.
+ */
label: string;
+ /**
+ * Set additional classnames to the label.
+ */
+ labelClassName?: string;
+ /**
+ * The label position. Default: top.
+ */
+ labelPosition?: 'left' | 'top';
+ /**
+ * The label size.
+ */
+ labelSize?: LabelProps['size'];
+ /**
+ * Set additional classnames to the select field.
+ */
+ selectClassName?: string;
};
-const LabelledSelect: FC<LabelledSelectProps> = ({
+const LabelledSelect: VFC<LabelledSelectProps> = ({
id,
label,
+ labelClassName = '',
+ labelPosition = 'top',
+ labelSize,
required,
+ selectClassName = '',
...props
}) => {
+ const positionModifier = `label--${labelPosition}`;
+
return (
<>
- <Label htmlFor={id} required={required}>
+ <Label
+ htmlFor={id}
+ required={required}
+ size={labelSize}
+ className={`${styles[positionModifier]} ${labelClassName}`}
+ >
{label}
</Label>
- <Select id={id} required={required} {...props} />
+ <Select
+ id={id}
+ required={required}
+ {...props}
+ className={selectClassName}
+ />
</>
);
};
diff --git a/src/components/molecules/forms/motion-toggle.stories.tsx b/src/components/molecules/forms/motion-toggle.stories.tsx
index 4fc199a..dc4d2a9 100644
--- a/src/components/molecules/forms/motion-toggle.stories.tsx
+++ b/src/components/molecules/forms/motion-toggle.stories.tsx
@@ -5,6 +5,18 @@ import MotionToggleComponent from './motion-toggle';
export default {
title: 'Molecules/Forms',
component: MotionToggleComponent,
+ argTypes: {
+ value: {
+ control: {
+ type: null,
+ },
+ description: 'The reduce motion value.',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ },
} as ComponentMeta<typeof MotionToggleComponent>;
const Template: ComponentStory<typeof MotionToggleComponent> = (args) => (
@@ -14,3 +26,6 @@ const Template: ComponentStory<typeof MotionToggleComponent> = (args) => (
);
export const MotionToggle = Template.bind({});
+MotionToggle.args = {
+ value: false,
+};
diff --git a/src/components/molecules/forms/motion-toggle.tsx b/src/components/molecules/forms/motion-toggle.tsx
index 77291ed..d4f7d11 100644
--- a/src/components/molecules/forms/motion-toggle.tsx
+++ b/src/components/molecules/forms/motion-toggle.tsx
@@ -2,17 +2,17 @@ import Toggle, {
ToggleChoices,
ToggleProps,
} from '@components/atoms/forms/toggle';
-import { FC, useState } from 'react';
+import { useState, VFC } from 'react';
import { useIntl } from 'react-intl';
-export type MotionToggleProps = Pick<ToggleProps, 'value'>;
+export type MotionToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
/**
* MotionToggle component
*
* Render a Toggle component to set reduce motion.
*/
-const MotionToggle: FC<MotionToggleProps> = ({ value }) => {
+const MotionToggle: VFC<MotionToggleProps> = ({ value, ...props }) => {
const intl = useIntl();
const [isDeactivated, setIsDeactivated] = useState<boolean>(value);
const reduceMotionLabel = intl.formatMessage({
@@ -40,9 +40,11 @@ const MotionToggle: FC<MotionToggleProps> = ({ value }) => {
id="reduce-motion-settings"
name="reduce-motion-settings"
label={reduceMotionLabel}
+ labelSize="medium"
choices={reduceMotionChoices}
value={isDeactivated}
setValue={setIsDeactivated}
+ {...props}
/>
);
};
diff --git a/src/components/molecules/forms/prism-theme-toggle.stories.tsx b/src/components/molecules/forms/prism-theme-toggle.stories.tsx
index fee54ef..dc9090b 100644
--- a/src/components/molecules/forms/prism-theme-toggle.stories.tsx
+++ b/src/components/molecules/forms/prism-theme-toggle.stories.tsx
@@ -5,6 +5,18 @@ import PrismThemeToggleComponent from './prism-theme-toggle';
export default {
title: 'Molecules/Forms',
component: PrismThemeToggleComponent,
+ argTypes: {
+ value: {
+ control: {
+ type: null,
+ },
+ description: 'The prism theme value.',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ },
} as ComponentMeta<typeof PrismThemeToggleComponent>;
const Template: ComponentStory<typeof PrismThemeToggleComponent> = (args) => (
@@ -14,3 +26,6 @@ const Template: ComponentStory<typeof PrismThemeToggleComponent> = (args) => (
);
export const PrismThemeToggle = Template.bind({});
+PrismThemeToggle.args = {
+ value: false,
+};
diff --git a/src/components/molecules/forms/prism-theme-toggle.tsx b/src/components/molecules/forms/prism-theme-toggle.tsx
index cedb71a..81a211b 100644
--- a/src/components/molecules/forms/prism-theme-toggle.tsx
+++ b/src/components/molecules/forms/prism-theme-toggle.tsx
@@ -4,17 +4,20 @@ import Toggle, {
} from '@components/atoms/forms/toggle';
import Moon from '@components/atoms/icons/moon';
import Sun from '@components/atoms/icons/sun';
-import { FC, useState } from 'react';
+import { useState, VFC } from 'react';
import { useIntl } from 'react-intl';
-export type PrismThemeToggleProps = Pick<ToggleProps, 'value'>;
+export type PrismThemeToggleProps = Pick<
+ ToggleProps,
+ 'labelClassName' | 'value'
+>;
/**
* PrismThemeToggle component
*
* Render a Toggle component to set code blocks theme.
*/
-const PrismThemeToggle: FC<PrismThemeToggleProps> = ({ value }) => {
+const PrismThemeToggle: VFC<PrismThemeToggleProps> = ({ value, ...props }) => {
const intl = useIntl();
const [isDarkTheme, setIsDarkTheme] = useState<boolean>(value);
const themeLabel = intl.formatMessage({
@@ -42,9 +45,11 @@ const PrismThemeToggle: FC<PrismThemeToggleProps> = ({ value }) => {
id="prism-theme-settings"
name="prism-theme-settings"
label={themeLabel}
+ labelSize="medium"
choices={themeChoices}
value={isDarkTheme}
setValue={setIsDarkTheme}
+ {...props}
/>
);
};
diff --git a/src/components/molecules/forms/select-with-tooltip.module.scss b/src/components/molecules/forms/select-with-tooltip.module.scss
index 1f91f74..bfadece 100644
--- a/src/components/molecules/forms/select-with-tooltip.module.scss
+++ b/src/components/molecules/forms/select-with-tooltip.module.scss
@@ -5,14 +5,9 @@
display: flex;
flex-flow: row wrap;
align-items: center;
- gap: var(--spacing-xs);
position: relative;
}
-.label {
- margin-right: auto;
-}
-
.select {
width: auto;
@@ -22,6 +17,8 @@
}
.btn {
+ margin-left: var(--spacing-xs);
+
&--activated {
background: var(--color-primary);
@@ -34,8 +31,7 @@
.tooltip {
position: absolute;
top: calc(100% + var(--spacing-xs));
- right: 0;
- transform-origin: top right;
+ transform-origin: top;
transition: all 0.75s ease-in-out 0s;
&--hidden {
diff --git a/src/components/molecules/forms/select-with-tooltip.stories.tsx b/src/components/molecules/forms/select-with-tooltip.stories.tsx
index d2d36fa..c63e9b8 100644
--- a/src/components/molecules/forms/select-with-tooltip.stories.tsx
+++ b/src/components/molecules/forms/select-with-tooltip.stories.tsx
@@ -17,11 +17,25 @@ export default {
required: true,
},
},
- title: {
+ 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: 'The tooltip title',
+ description: 'Field id.',
type: {
name: 'string',
required: true,
@@ -37,28 +51,31 @@ export default {
required: true,
},
},
- disabled: {
+ labelClassName: {
control: {
- type: 'boolean',
+ type: 'text',
},
- description: 'Field state: either enabled or disabled.',
+ description: 'Set additional classnames to the label.',
table: {
- category: 'Options',
- defaultValue: { summary: false },
+ category: 'Styles',
},
type: {
- name: 'boolean',
+ name: 'string',
required: false,
},
},
- id: {
+ labelSize: {
control: {
- type: 'text',
+ type: 'select',
+ },
+ description: 'The label size.',
+ options: ['medium', 'small'],
+ table: {
+ category: 'Options',
},
- description: 'Field id.',
type: {
name: 'string',
- required: true,
+ required: false,
},
},
name: {
@@ -98,6 +115,19 @@ export default {
required: false,
},
},
+ selectClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the select field.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
setValue: {
control: {
type: null,
@@ -111,6 +141,29 @@ export default {
required: true,
},
},
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The tooltip title',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ tooltipClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the tooltip.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
value: {
control: {
type: 'text',
diff --git a/src/components/molecules/forms/select-with-tooltip.tsx b/src/components/molecules/forms/select-with-tooltip.tsx
index 5e48d62..f537e1e 100644
--- a/src/components/molecules/forms/select-with-tooltip.tsx
+++ b/src/components/molecules/forms/select-with-tooltip.tsx
@@ -1,19 +1,22 @@
-import Select, { SelectProps } from '@components/atoms/forms/select';
-import { FC, useState } from 'react';
+import { useState, VFC } from 'react';
import HelpButton from '../buttons/help-button';
-import Tooltip, { TooltipProps } from '../modals/tooltip';
+import Tooltip, { type TooltipProps } from '../modals/tooltip';
+import LabelledSelect, { type LabelledSelectProps } from './labelled-select';
import styles from './select-with-tooltip.module.scss';
-export type SelectWithTooltipProps = SelectProps &
+export type SelectWithTooltipProps = Omit<
+ LabelledSelectProps,
+ 'labelPosition'
+> &
Pick<TooltipProps, 'title' | 'content'> & {
/**
* The select label.
*/
label: string;
/**
- * Set additional classes to the tooltip wrapper.
+ * Set additional classnames to the tooltip wrapper.
*/
- tooltipClasses?: string;
+ tooltipClassName?: string;
};
/**
@@ -21,12 +24,11 @@ export type SelectWithTooltipProps = SelectProps &
*
* Render a select with a button to display a tooltip about options.
*/
-const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
+const SelectWithTooltip: VFC<SelectWithTooltipProps> = ({
title,
content,
id,
- label,
- tooltipClasses = '',
+ tooltipClassName = '',
...props
}) => {
const [isTooltipOpened, setIsTooltipOpened] = useState<boolean>(false);
@@ -37,19 +39,21 @@ const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
return (
<div className={styles.wrapper}>
- <label htmlFor={id} className={styles.label}>
- {label}
- </label>
- <Select id={id} {...props} classes={styles.select} />
+ <LabelledSelect
+ labelPosition="left"
+ id={id}
+ labelClassName={styles.label}
+ {...props}
+ />
<HelpButton
onClick={() => setIsTooltipOpened(!isTooltipOpened)}
- classes={buttonModifier}
+ className={`${styles.btn} ${buttonModifier}`}
/>
<Tooltip
title={title}
content={content}
icon="?"
- classes={`${styles.tooltip} ${tooltipModifier} ${tooltipClasses}`}
+ className={`${styles.tooltip} ${tooltipModifier} ${tooltipClassName}`}
/>
</div>
);
diff --git a/src/components/molecules/forms/theme-toggle.stories.tsx b/src/components/molecules/forms/theme-toggle.stories.tsx
index 5970afd..a9bcf73 100644
--- a/src/components/molecules/forms/theme-toggle.stories.tsx
+++ b/src/components/molecules/forms/theme-toggle.stories.tsx
@@ -5,6 +5,18 @@ import ThemeToggleComponent from './theme-toggle';
export default {
title: 'Molecules/Forms',
component: ThemeToggleComponent,
+ argTypes: {
+ value: {
+ control: {
+ type: null,
+ },
+ description: 'The theme value.',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ },
} as ComponentMeta<typeof ThemeToggleComponent>;
const Template: ComponentStory<typeof ThemeToggleComponent> = (args) => (
@@ -14,3 +26,6 @@ const Template: ComponentStory<typeof ThemeToggleComponent> = (args) => (
);
export const ThemeToggle = Template.bind({});
+ThemeToggle.args = {
+ value: false,
+};
diff --git a/src/components/molecules/forms/theme-toggle.tsx b/src/components/molecules/forms/theme-toggle.tsx
index c927151..6d54591 100644
--- a/src/components/molecules/forms/theme-toggle.tsx
+++ b/src/components/molecules/forms/theme-toggle.tsx
@@ -4,17 +4,17 @@ import Toggle, {
} from '@components/atoms/forms/toggle';
import Moon from '@components/atoms/icons/moon';
import Sun from '@components/atoms/icons/sun';
-import { FC, useState } from 'react';
+import { useState, VFC } from 'react';
import { useIntl } from 'react-intl';
-export type ThemeToggleProps = Pick<ToggleProps, 'value'>;
+export type ThemeToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
/**
* ThemeToggle component
*
* Render a Toggle component to set theme.
*/
-const ThemeToggle: FC<ThemeToggleProps> = ({ value }) => {
+const ThemeToggle: VFC<ThemeToggleProps> = ({ value, ...props }) => {
const intl = useIntl();
const [isDarkTheme, setIsDarkTheme] = useState<boolean>(value);
const themeLabel = intl.formatMessage({
@@ -42,9 +42,11 @@ const ThemeToggle: FC<ThemeToggleProps> = ({ value }) => {
id="theme-settings"
name="theme-settings"
label={themeLabel}
+ labelSize="medium"
choices={themeChoices}
value={isDarkTheme}
setValue={setIsDarkTheme}
+ {...props}
/>
);
};
diff --git a/src/components/organisms/modals/settings-modal.module.scss b/src/components/organisms/modals/settings-modal.module.scss
new file mode 100644
index 0000000..f17c9b3
--- /dev/null
+++ b/src/components/organisms/modals/settings-modal.module.scss
@@ -0,0 +1,14 @@
+.wrapper {
+ max-width: 30ch;
+
+ .label {
+ margin-right: auto;
+ }
+}
+
+.tooltip {
+ width: 120%;
+ top: calc(100% + var(--spacing-sm));
+ right: -10%;
+ transform-origin: top right;
+}
diff --git a/src/components/organisms/modals/settings-modal.stories.tsx b/src/components/organisms/modals/settings-modal.stories.tsx
new file mode 100644
index 0000000..c19a6d7
--- /dev/null
+++ b/src/components/organisms/modals/settings-modal.stories.tsx
@@ -0,0 +1,31 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import SettingsModal from './settings-modal';
+
+export default {
+ title: 'Organisms/Modals',
+ component: SettingsModal,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the modal wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof SettingsModal>;
+
+const Template: ComponentStory<typeof SettingsModal> = (args) => (
+ <IntlProvider locale="en">
+ <SettingsModal {...args} />
+ </IntlProvider>
+);
+
+export const Settings = Template.bind({});
diff --git a/src/components/organisms/modals/settings-modal.test.tsx b/src/components/organisms/modals/settings-modal.test.tsx
new file mode 100644
index 0000000..44695d7
--- /dev/null
+++ b/src/components/organisms/modals/settings-modal.test.tsx
@@ -0,0 +1,34 @@
+import { render, screen } from '@test-utils';
+import SettingsModal from './settings-modal';
+
+jest.mock('next/dynamic', () => () => 'dynamic-import');
+
+describe('SettingsModal', () => {
+ it('renders a theme toggle setting', () => {
+ render(<SettingsModal />);
+ expect(
+ screen.getByRole('checkbox', { name: /^Theme:/i })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a code blocks toggle setting', () => {
+ render(<SettingsModal />);
+ expect(
+ screen.getByRole('checkbox', { name: /^Code blocks:/i })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a motion setting', () => {
+ render(<SettingsModal />);
+ expect(
+ screen.getByRole('checkbox', { name: /^Animations:/i })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a Ackee setting', () => {
+ render(<SettingsModal />);
+ expect(
+ screen.getByRole('combobox', { name: /^Tracking:/i })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/modals/settings-modal.tsx b/src/components/organisms/modals/settings-modal.tsx
new file mode 100644
index 0000000..0fac332
--- /dev/null
+++ b/src/components/organisms/modals/settings-modal.tsx
@@ -0,0 +1,51 @@
+import Form from '@components/atoms/forms/form';
+import AckeeSelect from '@components/molecules/forms/ackee-select';
+import MotionToggle from '@components/molecules/forms/motion-toggle';
+import PrismThemeToggle from '@components/molecules/forms/prism-theme-toggle';
+import ThemeToggle from '@components/molecules/forms/theme-toggle';
+import Modal from '@components/molecules/modals/modal';
+import { VFC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './settings-modal.module.scss';
+
+export type SettingsModalProps = {
+ /**
+ * Set additional classnames to modal wrapper.
+ */
+ className?: string;
+};
+
+/**
+ * SettingsModal component
+ *
+ * Render a modal with settings options.
+ */
+const SettingsModal: VFC<SettingsModalProps> = ({ className }) => {
+ const intl = useIntl();
+ const title = intl.formatMessage({
+ defaultMessage: 'Settings',
+ description: 'SettingsModal: title',
+ id: 'gPfT/K',
+ });
+
+ return (
+ <Modal
+ title={title}
+ icon="cogs"
+ className={`${styles.wrapper} ${className}`}
+ >
+ <Form onSubmit={() => null}>
+ <ThemeToggle labelClassName={styles.label} value={false} />
+ <PrismThemeToggle labelClassName={styles.label} value={false} />
+ <MotionToggle labelClassName={styles.label} value={false} />
+ <AckeeSelect
+ initialValue="full"
+ labelClassName={styles.label}
+ tooltipClassName={styles.tooltip}
+ />
+ </Form>
+ </Modal>
+ );
+};
+
+export default SettingsModal;