summaryrefslogtreecommitdiffstats
path: root/src/components/molecules/forms
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-06-01 19:34:43 +0200
committerArmand Philippot <git@armandphilippot.com>2022-06-01 22:32:16 +0200
commit6be20422494e3806fba3d1c5ad5c3e98bd6e67e5 (patch)
tree7c679e54ba4bbadaf0a59bbde780f5742e3b875d /src/components/molecules/forms
parent8320b1d39ea6402c32e907dbb35082efc6af9f5a (diff)
chore: replace the Ackee select by a toggle component
Diffstat (limited to 'src/components/molecules/forms')
-rw-r--r--src/components/molecules/forms/ackee-select.stories.tsx86
-rw-r--r--src/components/molecules/forms/ackee-select.test.tsx26
-rw-r--r--src/components/molecules/forms/ackee-select.tsx103
-rw-r--r--src/components/molecules/forms/ackee-toggle.fixture.tsx (renamed from src/components/molecules/forms/ackee-select.fixture.tsx)0
-rw-r--r--src/components/molecules/forms/ackee-toggle.module.scss (renamed from src/components/molecules/forms/ackee-select.module.scss)5
-rw-r--r--src/components/molecules/forms/ackee-toggle.stories.tsx112
-rw-r--r--src/components/molecules/forms/ackee-toggle.test.tsx15
-rw-r--r--src/components/molecules/forms/ackee-toggle.tsx143
-rw-r--r--src/components/molecules/forms/fieldset.fixture.tsx6
-rw-r--r--src/components/molecules/forms/fieldset.module.scss (renamed from src/components/molecules/forms/select-with-tooltip.module.scss)47
-rw-r--r--src/components/molecules/forms/fieldset.stories.tsx165
-rw-r--r--src/components/molecules/forms/fieldset.test.tsx22
-rw-r--r--src/components/molecules/forms/fieldset.tsx118
-rw-r--r--src/components/molecules/forms/motion-toggle.stories.tsx26
-rw-r--r--src/components/molecules/forms/motion-toggle.tsx2
-rw-r--r--src/components/molecules/forms/prism-theme-toggle.stories.tsx26
-rw-r--r--src/components/molecules/forms/prism-theme-toggle.tsx2
-rw-r--r--src/components/molecules/forms/radio-group.stories.tsx52
-rw-r--r--src/components/molecules/forms/radio-group.tsx6
-rw-r--r--src/components/molecules/forms/select-with-tooltip.stories.tsx223
-rw-r--r--src/components/molecules/forms/select-with-tooltip.test.tsx32
-rw-r--r--src/components/molecules/forms/select-with-tooltip.tsx78
-rw-r--r--src/components/molecules/forms/theme-toggle.stories.tsx26
-rw-r--r--src/components/molecules/forms/theme-toggle.tsx2
24 files changed, 748 insertions, 575 deletions
diff --git a/src/components/molecules/forms/ackee-select.stories.tsx b/src/components/molecules/forms/ackee-select.stories.tsx
deleted file mode 100644
index f8d04f6..0000000
--- a/src/components/molecules/forms/ackee-select.stories.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
-import AckeeSelect from './ackee-select';
-import { storageKey } from './ackee-select.fixture';
-
-/**
- * AckeeSelect - Storybook Meta
- */
-export default {
- title: 'Molecules/Forms/Select',
- component: AckeeSelect,
- argTypes: {
- className: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the select wrapper.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- initialValue: {
- control: {
- type: 'select',
- },
- description: 'Initial selected option.',
- options: ['full', 'partial'],
- type: {
- name: 'string',
- required: true,
- },
- },
- labelClassName: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the label wrapper.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- storageKey: {
- control: {
- type: 'text',
- },
- description: 'Set Ackee settings local storage key.',
- type: {
- name: 'string',
- required: true,
- },
- },
- tooltipClassName: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the tooltip wrapper.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- },
-} as ComponentMeta<typeof AckeeSelect>;
-
-const Template: ComponentStory<typeof AckeeSelect> = (args) => (
- <AckeeSelect {...args} />
-);
-
-/**
- * Select Stories - Ackee select
- */
-export const Ackee = Template.bind({});
-Ackee.args = {
- initialValue: 'full',
- storageKey,
-};
diff --git a/src/components/molecules/forms/ackee-select.test.tsx b/src/components/molecules/forms/ackee-select.test.tsx
deleted file mode 100644
index d255b00..0000000
--- a/src/components/molecules/forms/ackee-select.test.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import user from '@testing-library/user-event';
-import { act, render, screen } from '@test-utils';
-import AckeeSelect from './ackee-select';
-import { storageKey } from './ackee-select.fixture';
-
-describe('Select', () => {
- it('should correctly set default option', () => {
- render(<AckeeSelect storageKey={storageKey} initialValue="full" />);
- expect(screen.getByRole('combobox')).toHaveValue('full');
- expect(screen.queryByRole('combobox')).not.toHaveValue('partial');
- });
-
- it('should correctly change value when user choose another option', async () => {
- render(<AckeeSelect storageKey={storageKey} initialValue="full" />);
-
- await act(async () => {
- await user.selectOptions(
- screen.getByRole('combobox'),
- screen.getByRole('option', { name: 'Partial' })
- );
- });
-
- expect(screen.getByRole('combobox')).toHaveValue('partial');
- expect(screen.queryByRole('combobox')).not.toHaveValue('full');
- });
-});
diff --git a/src/components/molecules/forms/ackee-select.tsx b/src/components/molecules/forms/ackee-select.tsx
deleted file mode 100644
index f00ca74..0000000
--- a/src/components/molecules/forms/ackee-select.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { type SelectOptions } from '@components/atoms/forms/select';
-import useLocalStorage from '@utils/hooks/use-local-storage';
-import useUpdateAckeeOptions, {
- type AckeeOptions,
-} from '@utils/hooks/use-update-ackee-options';
-import { Dispatch, FC, SetStateAction } from 'react';
-import { useIntl } from 'react-intl';
-import SelectWithTooltip, {
- type SelectWithTooltipProps,
-} from './select-with-tooltip';
-
-export type AckeeSelectProps = Pick<
- SelectWithTooltipProps,
- 'className' | 'labelClassName' | 'tooltipClassName'
-> & {
- /**
- * A default value for Ackee settings.
- */
- initialValue: AckeeOptions;
- /**
- * The local storage key to save preference.
- */
- storageKey: string;
-};
-
-/**
- * AckeeSelect component
- *
- * Render a select to set Ackee settings.
- */
-const AckeeSelect: FC<AckeeSelectProps> = ({
- initialValue,
- storageKey,
- ...props
-}) => {
- const intl = useIntl();
- const { value, setValue } = useLocalStorage<AckeeOptions>(
- storageKey,
- initialValue
- );
- useUpdateAckeeOptions(value);
-
- const ackeeLabel = intl.formatMessage({
- defaultMessage: 'Tracking:',
- description: 'AckeeSelect: select label',
- id: '2pmylc',
- });
- const tooltipTitle = intl.formatMessage({
- defaultMessage: 'Ackee tracking (analytics)',
- description: 'AckeeSelect: tooltip title',
- id: 'F1EQX3',
- });
- const tooltipContent = [
- intl.formatMessage({
- defaultMessage: 'Partial includes only page url, views and duration.',
- description: 'AckeeSelect: tooltip message',
- id: 'skb4W5',
- }),
- intl.formatMessage({
- defaultMessage:
- 'Full includes all information from partial as well as information about referrer, operating system, device, browser, screen size and language.',
- description: 'AckeeSelect: tooltip message',
- id: 'Ogccx6',
- }),
- ];
- const options: SelectOptions[] = [
- {
- id: 'partial',
- name: intl.formatMessage({
- defaultMessage: 'Partial',
- description: 'AckeeSelect: partial option name',
- id: 'e/8Kyj',
- }),
- value: 'partial',
- },
- {
- id: 'full',
- name: intl.formatMessage({
- defaultMessage: 'Full',
- description: 'AckeeSelect: full option name',
- id: 'PzRpPw',
- }),
- value: 'full',
- },
- ];
-
- return (
- <SelectWithTooltip
- id="ackee-settings"
- name="ackee-settings"
- label={ackeeLabel}
- labelSize="medium"
- options={options}
- title={tooltipTitle}
- content={tooltipContent}
- value={value}
- setValue={setValue as Dispatch<SetStateAction<string>>}
- {...props}
- />
- );
-};
-
-export default AckeeSelect;
diff --git a/src/components/molecules/forms/ackee-select.fixture.tsx b/src/components/molecules/forms/ackee-toggle.fixture.tsx
index 04602f2..04602f2 100644
--- a/src/components/molecules/forms/ackee-select.fixture.tsx
+++ b/src/components/molecules/forms/ackee-toggle.fixture.tsx
diff --git a/src/components/molecules/forms/ackee-select.module.scss b/src/components/molecules/forms/ackee-toggle.module.scss
index 87cd9ee..f238bda 100644
--- a/src/components/molecules/forms/ackee-select.module.scss
+++ b/src/components/molecules/forms/ackee-toggle.module.scss
@@ -4,8 +4,3 @@
align-items: center;
position: relative;
}
-
-.tooltip {
- position: absolute;
- bottom: -100%;
-}
diff --git a/src/components/molecules/forms/ackee-toggle.stories.tsx b/src/components/molecules/forms/ackee-toggle.stories.tsx
new file mode 100644
index 0000000..bbc6fb4
--- /dev/null
+++ b/src/components/molecules/forms/ackee-toggle.stories.tsx
@@ -0,0 +1,112 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import AckeeToggleComponent from './ackee-toggle';
+import { storageKey } from './ackee-toggle.fixture';
+
+/**
+ * AckeeToggle - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Forms/Toggle',
+ component: AckeeToggleComponent,
+ argTypes: {
+ bodyClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the fieldset body wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the toggle wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ defaultValue: {
+ control: {
+ type: 'select',
+ },
+ description: 'Set the default value.',
+ options: ['full', 'partial'],
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ groupClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the radio group wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ legendClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the legend.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ storageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set local storage key.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ tooltipClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the tooltip wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof AckeeToggleComponent>;
+
+const Template: ComponentStory<typeof AckeeToggleComponent> = (args) => (
+ <AckeeToggleComponent {...args} />
+);
+
+/**
+ * Toggle Stories - Ackee
+ */
+export const Ackee = Template.bind({});
+Ackee.args = {
+ defaultValue: 'full',
+ storageKey,
+};
diff --git a/src/components/molecules/forms/ackee-toggle.test.tsx b/src/components/molecules/forms/ackee-toggle.test.tsx
new file mode 100644
index 0000000..8a57ce7
--- /dev/null
+++ b/src/components/molecules/forms/ackee-toggle.test.tsx
@@ -0,0 +1,15 @@
+import { render, screen } from '@test-utils';
+import AckeeToggle from './ackee-toggle';
+import { storageKey } from './ackee-toggle.fixture';
+
+describe('AckeeToggle', () => {
+ // toHaveValue received undefined. Maybe because of localStorage hook...
+ it('renders a toggle component', () => {
+ render(<AckeeToggle storageKey={storageKey} defaultValue="full" />);
+ expect(
+ screen.getByRole('radiogroup', {
+ name: /Tracking:/i,
+ })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/forms/ackee-toggle.tsx b/src/components/molecules/forms/ackee-toggle.tsx
new file mode 100644
index 0000000..a666731
--- /dev/null
+++ b/src/components/molecules/forms/ackee-toggle.tsx
@@ -0,0 +1,143 @@
+import useLocalStorage from '@utils/hooks/use-local-storage';
+import useUpdateAckeeOptions, {
+ type AckeeOptions,
+} from '@utils/hooks/use-update-ackee-options';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import RadioGroup, {
+ type RadioGroupCallback,
+ type RadioGroupCallbackProps,
+ type RadioGroupOption,
+ type RadioGroupProps,
+} from './radio-group';
+import Tooltip, { type TooltipProps } from '../modals/tooltip';
+
+export type AckeeToggleProps = Pick<
+ RadioGroupProps,
+ 'bodyClassName' | 'groupClassName' | 'legendClassName'
+> & {
+ /**
+ * Set additional classnames to the toggle wrapper.
+ */
+ className?: string;
+ /**
+ * True if motion should be reduced by default.
+ */
+ defaultValue: AckeeOptions;
+ /**
+ * The local storage key to save preference.
+ */
+ storageKey: string;
+ /**
+ * Set additional classnames to the tooltip wrapper.
+ */
+ tooltipClassName?: TooltipProps['className'];
+};
+
+/**
+ * AckeeToggle component
+ *
+ * Render a Toggle component to set reduce motion.
+ */
+const AckeeToggle: FC<AckeeToggleProps> = ({
+ defaultValue,
+ storageKey,
+ tooltipClassName,
+ ...props
+}) => {
+ const intl = useIntl();
+ const { value, setValue } = useLocalStorage<AckeeOptions>(
+ storageKey,
+ defaultValue
+ );
+ useUpdateAckeeOptions(value);
+
+ const ackeeLabel = intl.formatMessage({
+ defaultMessage: 'Tracking:',
+ description: 'AckeeToggle: select label',
+ id: '0gVlI3',
+ });
+ const tooltipTitle = intl.formatMessage({
+ defaultMessage: 'Ackee tracking (analytics)',
+ description: 'AckeeToggle: tooltip title',
+ id: 'nGss/j',
+ });
+ const tooltipContent = [
+ intl.formatMessage({
+ defaultMessage: 'Partial includes only page url, views and duration.',
+ description: 'AckeeToggle: tooltip message',
+ id: 'ZB/Aw2',
+ }),
+ intl.formatMessage({
+ defaultMessage:
+ 'Full includes all information from partial as well as information about referrer, operating system, device, browser, screen size and language.',
+ description: 'AckeeToggle: tooltip message',
+ id: '7zDlQo',
+ }),
+ ];
+ const partialLabel = intl.formatMessage({
+ defaultMessage: 'Partial',
+ description: 'AckeeToggle: partial option name',
+ id: 'tIZYpD',
+ });
+ const fullLabel = intl.formatMessage({
+ defaultMessage: 'Full',
+ description: 'AckeeToggle: full option name',
+ id: '5eD6y2',
+ });
+
+ const options: RadioGroupOption[] = [
+ {
+ id: 'ackee-full',
+ label: fullLabel,
+ name: 'ackee',
+ value: 'full',
+ },
+ {
+ id: 'ackee-partial',
+ label: partialLabel,
+ name: 'ackee',
+ value: 'partial',
+ },
+ ];
+
+ /**
+ * Handle change events.
+ *
+ * @param {RadioGroupCallbackProps} props - An object with choices.
+ */
+ const handleChange: RadioGroupCallback = ({
+ choices,
+ updateChoice,
+ }: RadioGroupCallbackProps) => {
+ let newChoice: AckeeOptions = choices.new as AckeeOptions;
+
+ if (choices.new === choices.prev) {
+ newChoice = choices.new === 'full' ? 'partial' : 'full';
+ updateChoice(newChoice);
+ }
+
+ setValue(newChoice);
+ };
+
+ return (
+ <RadioGroup
+ initialChoice={value}
+ kind="toggle"
+ legend={ackeeLabel}
+ onChange={handleChange}
+ options={options}
+ Tooltip={
+ <Tooltip
+ title={tooltipTitle}
+ content={tooltipContent}
+ icon="?"
+ className={tooltipClassName}
+ />
+ }
+ {...props}
+ />
+ );
+};
+
+export default AckeeToggle;
diff --git a/src/components/molecules/forms/fieldset.fixture.tsx b/src/components/molecules/forms/fieldset.fixture.tsx
new file mode 100644
index 0000000..b94f340
--- /dev/null
+++ b/src/components/molecules/forms/fieldset.fixture.tsx
@@ -0,0 +1,6 @@
+import { TooltipProps } from '../modals/tooltip';
+import { Help } from '../modals/tooltip.stories';
+
+export const body = 'doloribus magni aut';
+export const legend = 'maiores autem est';
+export const Tooltip = <Help {...(Help.args as TooltipProps)} />;
diff --git a/src/components/molecules/forms/select-with-tooltip.module.scss b/src/components/molecules/forms/fieldset.module.scss
index bfadece..3102bf7 100644
--- a/src/components/molecules/forms/select-with-tooltip.module.scss
+++ b/src/components/molecules/forms/fieldset.module.scss
@@ -1,23 +1,16 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-
-.wrapper {
- display: flex;
- flex-flow: row wrap;
- align-items: center;
- position: relative;
-}
-
-.select {
- width: auto;
-
- @include mix.pointer("fine") {
- padding: fun.convert-px(3) var(--spacing-xs);
+.legend {
+ float: left;
+ color: var(--color-primary-darker);
+ font-size: var(--font-size-md);
+ font-weight: 600;
+
+ &#{&}--has-tooltip {
+ padding: 0 var(--spacing-xs) 0 0;
}
}
.btn {
- margin-left: var(--spacing-xs);
+ margin: 0 var(--spacing-2xs) var(--spacing-2xs) 0;
&--activated {
background: var(--color-primary);
@@ -29,12 +22,12 @@
}
.tooltip {
- position: absolute;
top: calc(100% + var(--spacing-xs));
transform-origin: top;
transition: all 0.75s ease-in-out 0s;
&--hidden {
+ flex: 0 0 0;
opacity: 0;
visibility: hidden;
transform: scale(0);
@@ -46,3 +39,23 @@
transform: scale(1);
}
}
+
+.wrapper {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ max-width: 100%;
+ padding: 0;
+ position: relative;
+ border: none;
+
+ &--stacked {
+ .body {
+ flex: 1 0 100%;
+ }
+ }
+
+ .tooltip {
+ position: absolute;
+ }
+}
diff --git a/src/components/molecules/forms/fieldset.stories.tsx b/src/components/molecules/forms/fieldset.stories.tsx
new file mode 100644
index 0000000..0778094
--- /dev/null
+++ b/src/components/molecules/forms/fieldset.stories.tsx
@@ -0,0 +1,165 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { TooltipProps } from '../modals/tooltip';
+import { Help } from '../modals/tooltip.stories';
+import FieldsetComponent from './fieldset';
+import { body, legend, Tooltip } from './fieldset.fixture';
+
+/**
+ * Fieldset - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Forms/Fieldset',
+ component: FieldsetComponent,
+ args: {
+ legendPosition: 'stacked',
+ role: 'group',
+ },
+ argTypes: {
+ bodyClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the body wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ children: {
+ control: {
+ type: null,
+ },
+ description: 'The fieldset body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the fieldset.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ legend: {
+ control: {
+ type: 'text',
+ },
+ description: 'The fieldset legend.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ legendClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the legend.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ legendPosition: {
+ control: {
+ type: 'select',
+ },
+ description: 'Determine the legend position.',
+ options: ['inline', 'stacked'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'inline' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ role: {
+ control: {
+ type: 'select',
+ },
+ description: 'An accessible role.',
+ table: {
+ category: 'Accessibility',
+ defaultValue: { summary: 'group' },
+ },
+ options: ['group', 'radiogroup', 'presentation', 'none'],
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ Tooltip: {
+ control: {
+ type: null,
+ },
+ description: 'Add an optional tooltip.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof FieldsetComponent>;
+
+const Template: ComponentStory<typeof FieldsetComponent> = (args) => (
+ <FieldsetComponent {...args} />
+);
+
+/**
+ * Fieldset Stories - Stacked legend
+ */
+export const StackedLegend = Template.bind({});
+StackedLegend.args = {
+ children: body,
+ legend: legend,
+};
+
+/**
+ * Fieldset Stories - Inlined legend
+ */
+export const InlinedLegend = Template.bind({});
+InlinedLegend.args = {
+ children: body,
+ legend: legend,
+ legendPosition: 'inline',
+};
+
+/**
+ * Fieldset Stories - Stacked legend with tooltip
+ */
+export const StackedLegendWithTooltip = Template.bind({});
+StackedLegendWithTooltip.args = {
+ children: body,
+ legend: legend,
+ Tooltip,
+};
+
+/**
+ * Fieldset Stories - Inlined legend with tooltip
+ */
+export const InlinedLegendWithTooltip = Template.bind({});
+InlinedLegendWithTooltip.args = {
+ children: body,
+ legend: legend,
+ legendPosition: 'inline',
+ Tooltip,
+};
diff --git a/src/components/molecules/forms/fieldset.test.tsx b/src/components/molecules/forms/fieldset.test.tsx
new file mode 100644
index 0000000..de89e31
--- /dev/null
+++ b/src/components/molecules/forms/fieldset.test.tsx
@@ -0,0 +1,22 @@
+import { render, screen } from '@test-utils';
+import Fieldset from './fieldset';
+import { body, legend, Tooltip } from './fieldset.fixture';
+
+describe('Fieldset', () => {
+ // Cannot use toBeInTheDocument because of body is not an HTMLElement.
+
+ it('renders a legend and a body', () => {
+ render(<Fieldset legend={legend}>{body}</Fieldset>);
+ expect(screen.findByRole('group', { name: legend })).toBeTruthy();
+ expect(screen.findByText(body)).toBeTruthy();
+ });
+
+ it('renders a button to open a tooltip', () => {
+ render(
+ <Fieldset legend={legend} Tooltip={Tooltip}>
+ {body}
+ </Fieldset>
+ );
+ expect(screen.findByRole('button', { name: /Help/i })).toBeTruthy();
+ });
+});
diff --git a/src/components/molecules/forms/fieldset.tsx b/src/components/molecules/forms/fieldset.tsx
new file mode 100644
index 0000000..9f46247
--- /dev/null
+++ b/src/components/molecules/forms/fieldset.tsx
@@ -0,0 +1,118 @@
+import useClickOutside from '@utils/hooks/use-click-outside';
+import {
+ cloneElement,
+ FC,
+ ReactComponentElement,
+ ReactNode,
+ useRef,
+ useState,
+} from 'react';
+import HelpButton from '../buttons/help-button';
+import Tooltip from '../modals/tooltip';
+import styles from './fieldset.module.scss';
+
+export type FieldsetProps = {
+ /**
+ * Set additional classnames to the body wrapper.
+ */
+ bodyClassName?: string;
+ /**
+ * The fieldset body.
+ */
+ children: ReactNode | ReactNode[];
+ /**
+ * Set additional classnames to the fieldset wrapper.
+ */
+ className?: string;
+ /**
+ * The fieldset legend.
+ */
+ legend: string;
+ /**
+ * Set additional classnames to the legend.
+ */
+ legendClassName?: string;
+ /**
+ * The legend position. Default: stacked.
+ */
+ legendPosition?: 'inline' | 'stacked';
+ /**
+ * An accessible role. Default: group.
+ */
+ role?: 'group' | 'radiogroup' | 'presentation' | 'none';
+ /**
+ * An optional tooltip component.
+ */
+ Tooltip?: ReactComponentElement<typeof Tooltip>;
+};
+
+/**
+ * Fieldset component
+ *
+ * Render a fieldset with a legend.
+ */
+const Fieldset: FC<FieldsetProps> = ({
+ bodyClassName = '',
+ children,
+ className = '',
+ legend,
+ legendClassName = '',
+ legendPosition = 'stacked',
+ Tooltip: TooltipComponent,
+ ...props
+}) => {
+ const [isTooltipOpened, setIsTooltipOpened] = useState<boolean>(false);
+ const buttonRef = useRef<HTMLButtonElement>(null);
+ const tooltipRef = useRef<HTMLDivElement>(null);
+ const wrapperModifier = `wrapper--${legendPosition}`;
+ const buttonModifier = isTooltipOpened ? styles['btn--activated'] : '';
+ const legendModifier =
+ TooltipComponent === undefined ? '' : 'legend--has-tooltip';
+ const tooltipModifier = isTooltipOpened
+ ? 'tooltip--visible'
+ : 'tooltip--hidden';
+
+ /**
+ * Close the tooltip if the event target is outside.
+ *
+ * @param {EventTarget} target - The event target.
+ */
+ const closeTooltip = (target: EventTarget) => {
+ if (buttonRef.current && !buttonRef.current.contains(target as Node))
+ setIsTooltipOpened(false);
+ };
+
+ useClickOutside(
+ tooltipRef,
+ (target) => isTooltipOpened && closeTooltip(target)
+ );
+
+ return (
+ <fieldset
+ className={`${styles.wrapper} ${styles[wrapperModifier]} ${className}`}
+ {...props}
+ >
+ <legend
+ className={`${styles.legend} ${styles[legendModifier]} ${legendClassName}`}
+ >
+ {legend}
+ </legend>
+ {TooltipComponent && (
+ <>
+ <HelpButton
+ className={`${styles.btn} ${buttonModifier}`}
+ onClick={() => setIsTooltipOpened(!isTooltipOpened)}
+ ref={buttonRef}
+ />
+ {cloneElement(TooltipComponent, {
+ cloneClassName: `${styles.tooltip} ${styles[tooltipModifier]}`,
+ ref: tooltipRef,
+ })}
+ </>
+ )}
+ <div className={`${styles.body} ${bodyClassName}`}>{children}</div>
+ </fieldset>
+ );
+};
+
+export default Fieldset;
diff --git a/src/components/molecules/forms/motion-toggle.stories.tsx b/src/components/molecules/forms/motion-toggle.stories.tsx
index 5c524a8..541ca8e 100644
--- a/src/components/molecules/forms/motion-toggle.stories.tsx
+++ b/src/components/molecules/forms/motion-toggle.stories.tsx
@@ -9,6 +9,19 @@ export default {
title: 'Molecules/Forms/Toggle',
component: MotionToggleComponent,
argTypes: {
+ bodyClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the fieldset body wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
defaultValue: {
control: {
type: 'select',
@@ -20,6 +33,19 @@ export default {
required: true,
},
},
+ groupClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the radio group wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
legendClassName: {
control: {
type: 'text',
diff --git a/src/components/molecules/forms/motion-toggle.tsx b/src/components/molecules/forms/motion-toggle.tsx
index 6925248..ec2d950 100644
--- a/src/components/molecules/forms/motion-toggle.tsx
+++ b/src/components/molecules/forms/motion-toggle.tsx
@@ -13,7 +13,7 @@ export type MotionToggleValue = 'on' | 'off';
export type MotionToggleProps = Pick<
RadioGroupProps,
- 'groupClassName' | 'legendClassName'
+ 'bodyClassName' | 'groupClassName' | 'legendClassName'
> & {
/**
* True if motion should be reduced by default.
diff --git a/src/components/molecules/forms/prism-theme-toggle.stories.tsx b/src/components/molecules/forms/prism-theme-toggle.stories.tsx
index 3f57fa5..86f9773 100644
--- a/src/components/molecules/forms/prism-theme-toggle.stories.tsx
+++ b/src/components/molecules/forms/prism-theme-toggle.stories.tsx
@@ -8,6 +8,32 @@ export default {
title: 'Molecules/Forms/Toggle',
component: PrismThemeToggle,
argTypes: {
+ bodyClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the fieldset body wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ groupClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the radio group wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
legendClassName: {
control: {
type: 'text',
diff --git a/src/components/molecules/forms/prism-theme-toggle.tsx b/src/components/molecules/forms/prism-theme-toggle.tsx
index 66be056..7bf5b7c 100644
--- a/src/components/molecules/forms/prism-theme-toggle.tsx
+++ b/src/components/molecules/forms/prism-theme-toggle.tsx
@@ -12,7 +12,7 @@ import RadioGroup, {
export type PrismThemeToggleProps = Pick<
RadioGroupProps,
- 'groupClassName' | 'legendClassName'
+ 'bodyClassName' | 'groupClassName' | 'legendClassName'
>;
/**
diff --git a/src/components/molecules/forms/radio-group.stories.tsx b/src/components/molecules/forms/radio-group.stories.tsx
index 3c01af5..ad1bd6d 100644
--- a/src/components/molecules/forms/radio-group.stories.tsx
+++ b/src/components/molecules/forms/radio-group.stories.tsx
@@ -13,6 +13,19 @@ export default {
labelSize: 'small',
},
argTypes: {
+ bodyClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the fieldset body wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
className: {
control: {
type: 'text',
@@ -26,6 +39,19 @@ export default {
required: false,
},
},
+ groupClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the radio group wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
initialChoice: {
control: {
type: 'text',
@@ -131,6 +157,19 @@ export default {
required: false,
},
},
+ onClick: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to handle click on a choice.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
optionClassName: {
control: {
type: 'text',
@@ -152,6 +191,19 @@ export default {
value: {},
},
},
+ Tooltip: {
+ control: {
+ type: null,
+ },
+ description: 'Add an optional tooltip.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
},
} as ComponentMeta<typeof RadioGroup>;
diff --git a/src/components/molecules/forms/radio-group.tsx b/src/components/molecules/forms/radio-group.tsx
index 45f585e..64bdaa0 100644
--- a/src/components/molecules/forms/radio-group.tsx
+++ b/src/components/molecules/forms/radio-group.tsx
@@ -1,4 +1,6 @@
-import Fieldset, { type FieldsetProps } from '@components/atoms/forms/fieldset';
+import Fieldset, {
+ type FieldsetProps,
+} from '@components/molecules/forms/fieldset';
import useStateChange from '@utils/hooks/use-state-change';
import { ChangeEvent, FC, MouseEvent, SetStateAction, useState } from 'react';
import LabelledBooleanField, {
@@ -23,7 +25,7 @@ export type RadioGroupOption = Pick<
export type RadioGroupProps = Pick<
FieldsetProps,
- 'className' | 'legend' | 'legendClassName'
+ 'bodyClassName' | 'className' | 'legend' | 'legendClassName' | 'Tooltip'
> &
Pick<LabelledBooleanFieldProps, 'labelPosition' | 'labelSize'> & {
/**
diff --git a/src/components/molecules/forms/select-with-tooltip.stories.tsx b/src/components/molecules/forms/select-with-tooltip.stories.tsx
deleted file mode 100644
index d6206a9..0000000
--- a/src/components/molecules/forms/select-with-tooltip.stories.tsx
+++ /dev/null
@@ -1,223 +0,0 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { useState } from 'react';
-import SelectWithTooltip from './select-with-tooltip';
-
-/**
- * SelectWithTooltip - Storybook Meta
- */
-export default {
- title: 'Molecules/Forms/Select',
- component: SelectWithTooltip,
- argTypes: {
- className: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the select wrapper.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- content: {
- control: {
- type: 'text',
- },
- description: 'The tooltip body.',
- type: {
- name: 'string',
- required: true,
- },
- },
- 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.',
- type: {
- name: 'string',
- required: true,
- },
- },
- label: {
- control: {
- type: 'text',
- },
- description: 'The select label.',
- type: {
- name: 'string',
- required: true,
- },
- },
- labelClassName: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the label.',
- table: {
- category: 'Styles',
- },
- 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',
- },
- description: 'Field name.',
- type: {
- name: 'string',
- required: true,
- },
- },
- options: {
- control: {
- type: null,
- },
- description: 'Select options.',
- type: {
- name: 'array',
- required: true,
- value: {
- name: 'string',
- },
- },
- },
- required: {
- control: {
- type: 'boolean',
- },
- description: 'Determine if the field is required.',
- table: {
- category: 'Options',
- defaultValue: { summary: false },
- },
- type: {
- name: 'boolean',
- 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,
- },
- description: 'Callback function to set field value.',
- table: {
- category: 'Events',
- },
- type: {
- name: 'function',
- 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',
- },
- description: 'Field value.',
- type: {
- name: 'string',
- required: true,
- },
- },
- },
-} as ComponentMeta<typeof SelectWithTooltip>;
-
-const selectOptions = [
- { id: 'option1', name: 'Option 1', value: 'option1' },
- { id: 'option2', name: 'Option 2', value: 'option2' },
- { id: 'option3', name: 'Option 3', value: 'option3' },
-];
-
-const Template: ComponentStory<typeof SelectWithTooltip> = ({
- value: _value,
- setValue: _setValue,
- ...args
-}) => {
- const [selected, setSelected] = useState<string>('option1');
- return (
- <SelectWithTooltip value={selected} setValue={setSelected} {...args} />
- );
-};
-
-/**
- * Select Stories - With tooltip
- */
-export const WithTooltip = Template.bind({});
-WithTooltip.args = {
- content: 'Illo voluptatibus quia minima placeat sit nostrum excepturi.',
- title: 'Possimus quidem dolor',
- id: 'storybook-select',
- label: 'Officiis:',
- name: 'storybook-select',
- options: selectOptions,
-};
diff --git a/src/components/molecules/forms/select-with-tooltip.test.tsx b/src/components/molecules/forms/select-with-tooltip.test.tsx
deleted file mode 100644
index 7a423f5..0000000
--- a/src/components/molecules/forms/select-with-tooltip.test.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { render, screen } from '@test-utils';
-import SelectWithTooltip from './select-with-tooltip';
-
-const selectOptions = [
- { id: 'option1', name: 'Option 1', value: 'option1' },
- { id: 'option2', name: 'Option 2', value: 'option2' },
- { id: 'option3', name: 'Option 3', value: 'option3' },
-];
-const selectLabel = 'Jest select';
-const selectValue = selectOptions[0].value;
-const tooltipTitle = 'Jest tooltip';
-const tooltipContent = 'Nesciunt voluptatibus voluptatem omnis at quia libero.';
-
-describe('SelectWithTooltip', () => {
- it('renders a select', () => {
- render(
- <SelectWithTooltip
- id="jest-select"
- name="jest-select"
- label={selectLabel}
- options={selectOptions}
- value={selectValue}
- setValue={() => null}
- title={tooltipTitle}
- content={tooltipContent}
- />
- );
- expect(screen.getByRole('combobox', { name: selectLabel })).toHaveValue(
- selectValue
- );
- });
-});
diff --git a/src/components/molecules/forms/select-with-tooltip.tsx b/src/components/molecules/forms/select-with-tooltip.tsx
deleted file mode 100644
index 46075c2..0000000
--- a/src/components/molecules/forms/select-with-tooltip.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import useClickOutside from '@utils/hooks/use-click-outside';
-import { FC, useRef, useState } from 'react';
-import HelpButton from '../buttons/help-button';
-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 = Omit<
- LabelledSelectProps,
- 'labelPosition'
-> &
- Pick<TooltipProps, 'title' | 'content'> & {
- /**
- * Set additional classnames to the select wrapper.
- */
- className?: string;
- /**
- * Set additional classnames to the tooltip wrapper.
- */
- tooltipClassName?: TooltipProps['className'];
- };
-
-/**
- * SelectWithTooltip component
- *
- * Render a select with a button to display a tooltip about options.
- */
-const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
- className = '',
- content,
- id,
- title,
- tooltipClassName = '',
- ...props
-}) => {
- const [isTooltipOpened, setIsTooltipOpened] = useState<boolean>(false);
- const buttonRef = useRef<HTMLButtonElement>(null);
- const tooltipRef = useRef<HTMLDivElement>(null);
- const buttonModifier = isTooltipOpened ? styles['btn--activated'] : '';
- const tooltipModifier = isTooltipOpened
- ? styles['tooltip--visible']
- : styles['tooltip--hidden'];
-
- const closeTooltip = (target: EventTarget) => {
- if (buttonRef.current && !buttonRef.current.contains(target as Node))
- setIsTooltipOpened(false);
- };
-
- useClickOutside(
- tooltipRef,
- (target) => isTooltipOpened && closeTooltip(target)
- );
-
- return (
- <div className={`${styles.wrapper} ${className}`}>
- <LabelledSelect
- labelPosition="left"
- id={id}
- labelClassName={styles.label}
- {...props}
- />
- <HelpButton
- className={`${styles.btn} ${buttonModifier}`}
- onClick={() => setIsTooltipOpened(!isTooltipOpened)}
- ref={buttonRef}
- />
- <Tooltip
- title={title}
- content={content}
- icon="?"
- className={`${styles.tooltip} ${tooltipModifier} ${tooltipClassName}`}
- ref={tooltipRef}
- />
- </div>
- );
-};
-
-export default SelectWithTooltip;
diff --git a/src/components/molecules/forms/theme-toggle.stories.tsx b/src/components/molecules/forms/theme-toggle.stories.tsx
index cd59d7e..ff1034d 100644
--- a/src/components/molecules/forms/theme-toggle.stories.tsx
+++ b/src/components/molecules/forms/theme-toggle.stories.tsx
@@ -8,6 +8,32 @@ export default {
title: 'Molecules/Forms/Toggle',
component: ThemeToggle,
argTypes: {
+ bodyClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the fieldset body wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ groupClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the radio group wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
legendClassName: {
control: {
type: 'text',
diff --git a/src/components/molecules/forms/theme-toggle.tsx b/src/components/molecules/forms/theme-toggle.tsx
index 30bc55c..b796b27 100644
--- a/src/components/molecules/forms/theme-toggle.tsx
+++ b/src/components/molecules/forms/theme-toggle.tsx
@@ -12,7 +12,7 @@ import RadioGroup, {
export type ThemeToggleProps = Pick<
RadioGroupProps,
- 'groupClassName' | 'legendClassName'
+ 'bodyClassName' | 'groupClassName' | 'legendClassName'
>;
/**