diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-30 12:44:11 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:15:27 +0100 |
| commit | 0e52a59917406ad03c174e030c6c1c92ab23449d (patch) | |
| tree | 693bbcc5edbe78ebd2f0050fddbc45c706e0ba61 /src | |
| parent | 84a679b0e48ed76eee2fa44d3caac83591aa3c8c (diff) | |
refactor(components): extract SettingsForm component form SettingsModal
We could use an array of items and map over it instead of repeating the
Switch component for each settings but with translations, it becomes
quickly unreadable. So I prefer to keep separate components.
Diffstat (limited to 'src')
36 files changed, 607 insertions, 476 deletions
diff --git a/src/components/molecules/forms/switch/switch.module.scss b/src/components/molecules/forms/switch/switch.module.scss index 44244e7..869e77c 100644 --- a/src/components/molecules/forms/switch/switch.module.scss +++ b/src/components/molecules/forms/switch/switch.module.scss @@ -6,8 +6,8 @@ } .switch { - display: inline-flex; - flex-flow: row wrap; + display: grid; + grid-template-columns: repeat(2, minmax(50px, 1fr)); align-items: center; width: fit-content; background: var(--color-shadow-light); @@ -24,31 +24,30 @@ .label { display: flex; - align-items: center; - min-height: 5ex; - padding: fun.convert-px(6) var(--spacing-2xs); + place-items: center; + place-content: center; + min-height: fun.convert-px(44); + padding: fun.convert-px(5) fun.convert-px(12); border-top: fun.convert-px(2) solid var(--color-border); border-bottom: fun.convert-px(2) solid var(--color-border); transition: all 0.15s linear 0s; - - @include mix.pointer("fine") { - min-height: 3ex; - } } -.item:first-of-type { - .label { - border-left: fun.convert-px(2) solid var(--color-border); - border-top-left-radius: fun.convert-px(32); - border-bottom-left-radius: fun.convert-px(32); +.item { + &:first-of-type { + .label { + border-left: fun.convert-px(2) solid var(--color-border); + border-top-left-radius: fun.convert-px(32); + border-bottom-left-radius: fun.convert-px(32); + } } -} -.item:last-of-type { - .label { - border-right: fun.convert-px(2) solid var(--color-border); - border-top-right-radius: fun.convert-px(32); - border-bottom-right-radius: fun.convert-px(32); + &:last-of-type { + .label { + border-right: fun.convert-px(2) solid var(--color-border); + border-top-right-radius: fun.convert-px(32); + border-bottom-right-radius: fun.convert-px(32); + } } } diff --git a/src/components/organisms/forms/ackee-toggle/ackee-toggle.stories.tsx b/src/components/organisms/forms/ackee-toggle/ackee-toggle.stories.tsx deleted file mode 100644 index 1b7b87b..0000000 --- a/src/components/organisms/forms/ackee-toggle/ackee-toggle.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { AckeeToggle } from './ackee-toggle'; - -/** - * AckeeToggle - Storybook Meta - */ -export default { - title: 'Organisms/Forms/Toggle', - component: AckeeToggle, - argTypes: { - defaultValue: { - control: { - type: 'select', - }, - description: 'Set the default value.', - options: ['full', 'partial'], - type: { - name: 'string', - required: true, - }, - }, - storageKey: { - control: { - type: 'text', - }, - description: 'Set local storage key.', - type: { - name: 'string', - required: true, - }, - }, - }, -} as ComponentMeta<typeof AckeeToggle>; - -const Template: ComponentStory<typeof AckeeToggle> = (args) => ( - <AckeeToggle {...args} /> -); - -/** - * Toggle Stories - Ackee - */ -export const Ackee = Template.bind({}); -Ackee.args = {}; diff --git a/src/components/organisms/forms/ackee-toggle/ackee-toggle.test.tsx b/src/components/organisms/forms/ackee-toggle/ackee-toggle.test.tsx deleted file mode 100644 index 68f8d19..0000000 --- a/src/components/organisms/forms/ackee-toggle/ackee-toggle.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../../tests/utils'; -import { AckeeToggle } from './ackee-toggle'; - -describe('AckeeToggle', () => { - // toHaveValue received undefined. Maybe because of localStorage hook... - it('renders a toggle component', () => { - render(<AckeeToggle />); - expect( - rtlScreen.getByRole('radiogroup', { - name: /Tracking:/i, - }) - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx b/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx deleted file mode 100644 index 2fea0a7..0000000 --- a/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable max-statements */ -import type { FC } from 'react'; -import { useIntl } from 'react-intl'; -import { useAckee, useBoolean } from '../../../../utils/hooks'; -import { Legend, List, ListItem } from '../../../atoms'; -import { - Switch, - type SwitchOption, - type SwitchProps, - Tooltip, - type TooltipProps, -} from '../../../molecules'; - -export type AckeeToggleProps = Omit< - SwitchProps, - 'defaultValue' | 'isInline' | 'items' | 'name' | 'onSwitch' | 'value' -> & - Pick<TooltipProps, 'direction'>; - -/** - * AckeeToggle component - * - * Render a Toggle component to set reduce motion. - */ -export const AckeeToggle: FC<AckeeToggleProps> = ({ direction, ...props }) => { - const intl = useIntl(); - const [tracking, toggleTracking] = useAckee(); - const { - deactivate: closeTooltip, - state: isTooltipOpened, - toggle: toggleTooltip, - } = useBoolean(false); - - const ackeeLabel = intl.formatMessage({ - defaultMessage: 'Tracking:', - description: 'AckeeToggle: select label', - id: '0gVlI3', - }); - 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 tooltipTitle = intl.formatMessage({ - defaultMessage: 'Ackee tracking (analytics)', - description: 'AckeeToggle: tooltip title', - id: 'nGss/j', - }); - const tooltipPartial = intl.formatMessage({ - defaultMessage: 'Partial includes only page url, views and duration.', - description: 'AckeeToggle: tooltip message', - id: 'ZB/Aw2', - }); - const tooltipFull = 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 options = [ - { id: 'ackee-full' as const, label: fullLabel, value: 'full' }, - { id: 'ackee-partial' as const, label: partialLabel, value: 'partial' }, - ] satisfies [SwitchOption, SwitchOption]; - - return ( - <Switch - {...props} - isInline - items={options} - legend={<Legend>{ackeeLabel}</Legend>} - name="ackee" - onSwitch={toggleTracking} - tooltip={ - <Tooltip - direction={direction} - heading={tooltipTitle} - isOpen={isTooltipOpened} - onClickOutside={closeTooltip} - onToggle={toggleTooltip} - > - <List> - {options.map((option) => ( - <ListItem key={option.id}> - {option.id === 'ackee-full' ? tooltipFull : tooltipPartial} - </ListItem> - ))} - </List> - </Tooltip> - } - value={tracking} - /> - ); -}; diff --git a/src/components/organisms/forms/index.ts b/src/components/organisms/forms/index.ts index e507895..324505e 100644 --- a/src/components/organisms/forms/index.ts +++ b/src/components/organisms/forms/index.ts @@ -1,7 +1,4 @@ -export * from './ackee-toggle'; export * from './comment-form'; export * from './contact-form'; -export * from './motion-toggle'; -export * from './prism-theme-toggle'; export * from './search-form'; -export * from './theme-toggle'; +export * from './settings-form'; diff --git a/src/components/organisms/forms/motion-toggle/motion-toggle.test.tsx b/src/components/organisms/forms/motion-toggle/motion-toggle.test.tsx deleted file mode 100644 index d20057e..0000000 --- a/src/components/organisms/forms/motion-toggle/motion-toggle.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../../tests/utils'; -import { MotionToggle } from './motion-toggle'; - -describe('MotionToggle', () => { - // toHaveValue received undefined. Maybe because of localStorage hook... - it('renders a toggle component', () => { - render(<MotionToggle />); - expect( - rtlScreen.getByRole('radiogroup', { - name: /Animations:/i, - }) - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/organisms/forms/motion-toggle/motion-toggle.tsx b/src/components/organisms/forms/motion-toggle/motion-toggle.tsx deleted file mode 100644 index 33527c3..0000000 --- a/src/components/organisms/forms/motion-toggle/motion-toggle.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type { FC } from 'react'; -import { useIntl } from 'react-intl'; -import { useReducedMotion } from '../../../../utils/hooks'; -import { Legend } from '../../../atoms'; -import { - Switch, - type SwitchOption, - type SwitchProps, -} from '../../../molecules'; - -export type MotionToggleProps = Omit< - SwitchProps, - 'isInline' | 'items' | 'name' | 'onSwitch' | 'value' ->; - -/** - * MotionToggle component - * - * Render a Toggle component to set reduce motion. - */ -export const MotionToggle: FC<MotionToggleProps> = ({ ...props }) => { - const intl = useIntl(); - const { isReduced, toggleReducedMotion } = useReducedMotion(); - - const reduceMotionLabel = intl.formatMessage({ - defaultMessage: 'Animations:', - description: 'MotionToggle: reduce motion label', - id: '/q5csZ', - }); - const onLabel = intl.formatMessage({ - defaultMessage: 'On', - description: 'MotionToggle: activate reduce motion label', - id: 'va65iw', - }); - const offLabel = intl.formatMessage({ - defaultMessage: 'Off', - description: 'MotionToggle: deactivate reduce motion label', - id: 'pWKyyR', - }); - - const options: [SwitchOption, SwitchOption] = [ - { - id: 'reduced-motion-on', - label: onLabel, - value: 'on', - }, - { - id: 'reduced-motion-off', - label: offLabel, - value: 'off', - }, - ]; - - return ( - <Switch - {...props} - isInline - items={options} - legend={<Legend>{reduceMotionLabel}</Legend>} - name="reduced-motion" - onSwitch={toggleReducedMotion} - value={isReduced ? 'off' : 'on'} - /> - ); -}; diff --git a/src/components/organisms/forms/prism-theme-toggle/prism-theme-toggle.test.tsx b/src/components/organisms/forms/prism-theme-toggle/prism-theme-toggle.test.tsx deleted file mode 100644 index ad8658d..0000000 --- a/src/components/organisms/forms/prism-theme-toggle/prism-theme-toggle.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../../tests/utils'; -import { PrismThemeToggle } from './prism-theme-toggle'; - -describe('PrismThemeToggle', () => { - it('renders a toggle component', () => { - render(<PrismThemeToggle />); - expect( - rtlScreen.getByRole('radiogroup', { - name: /Code blocks:/i, - }) - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/organisms/forms/prism-theme-toggle/prism-theme-toggle.tsx b/src/components/organisms/forms/prism-theme-toggle/prism-theme-toggle.tsx deleted file mode 100644 index 1eba191..0000000 --- a/src/components/organisms/forms/prism-theme-toggle/prism-theme-toggle.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import type { FC } from 'react'; -import { useIntl } from 'react-intl'; -import { usePrismTheme } from '../../../../utils/hooks'; -import { Icon, Legend } from '../../../atoms'; -import { - Switch, - type SwitchOption, - type SwitchProps, -} from '../../../molecules'; - -export type PrismThemeToggleProps = Omit< - SwitchProps, - 'isInline' | 'items' | 'name' | 'onSwitch' | 'value' ->; - -/** - * PrismThemeToggle component - * - * Render a Toggle component to set code blocks theme. - */ -export const PrismThemeToggle: FC<PrismThemeToggleProps> = (props) => { - const intl = useIntl(); - const { currentTheme, toggleTheme } = usePrismTheme(); - - const themeLabel = intl.formatMessage({ - defaultMessage: 'Code blocks:', - description: 'PrismThemeToggle: theme label', - id: 'ftXN+0', - }); - const lightThemeLabel = intl.formatMessage({ - defaultMessage: 'Light theme', - description: 'PrismThemeToggle: light theme label', - id: 'tsWh8x', - }); - const darkThemeLabel = intl.formatMessage({ - defaultMessage: 'Dark theme', - description: 'PrismThemeToggle: dark theme label', - id: 'og/zWL', - }); - - const options: [SwitchOption, SwitchOption] = [ - { - id: 'code-blocks-light', - label: ( - <> - <span className="screen-reader-text">{lightThemeLabel}</span> - <Icon shape="sun" /> - </> - ), - value: 'light', - }, - { - id: 'code-blocks-dark', - label: ( - <> - <span className="screen-reader-text">{darkThemeLabel}</span> - <Icon shape="moon" /> - </> - ), - value: 'dark', - }, - ]; - - return ( - <Switch - {...props} - isInline - items={options} - legend={<Legend>{themeLabel}</Legend>} - name="code-blocks" - onSwitch={toggleTheme} - value={currentTheme} - /> - ); -}; diff --git a/src/components/organisms/forms/settings-form/ackee-toggle/ackee-toggle.module.scss b/src/components/organisms/forms/settings-form/ackee-toggle/ackee-toggle.module.scss new file mode 100644 index 0000000..513c95c --- /dev/null +++ b/src/components/organisms/forms/settings-form/ackee-toggle/ackee-toggle.module.scss @@ -0,0 +1,10 @@ +@use "../../../../../styles/abstracts/mixins" as mix; + +.tooltip { + @include mix.media("screen") { + @include mix.dimensions("sm") { + inset-inline: 0; + max-width: 100%; + } + } +} diff --git a/src/components/organisms/forms/settings-form/ackee-toggle/ackee-toggle.stories.tsx b/src/components/organisms/forms/settings-form/ackee-toggle/ackee-toggle.stories.tsx new file mode 100644 index 0000000..abaf59f --- /dev/null +++ b/src/components/organisms/forms/settings-form/ackee-toggle/ackee-toggle.stories.tsx @@ -0,0 +1,21 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { AckeeToggle } from './ackee-toggle'; + +/** + * AckeeToggle - Storybook Meta + */ +export default { + title: 'Organisms/Forms/Settings/Items', + component: AckeeToggle, + argTypes: {}, +} as ComponentMeta<typeof AckeeToggle>; + +const Template: ComponentStory<typeof AckeeToggle> = (args) => ( + <AckeeToggle {...args} /> +); + +/** + * Toggle Stories - Ackee + */ +export const Ackee = Template.bind({}); +Ackee.args = {}; diff --git a/src/components/organisms/forms/settings-form/ackee-toggle/ackee-toggle.test.tsx b/src/components/organisms/forms/settings-form/ackee-toggle/ackee-toggle.test.tsx new file mode 100644 index 0000000..30bbf3a --- /dev/null +++ b/src/components/organisms/forms/settings-form/ackee-toggle/ackee-toggle.test.tsx @@ -0,0 +1,28 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '../../../../../../tests/utils'; +import { AckeeProvider } from '../../../../../utils/providers'; +import { AckeeToggle } from './ackee-toggle'; + +describe('AckeeToggle', () => { + it('renders a radio group of two radio buttons', () => { + const defaultValue = 'full'; + + render( + <AckeeProvider + domainId="any-id" + server="https://example.com" + storageKey="molestiae" + tracking={defaultValue} + > + <AckeeToggle /> + </AckeeProvider> + ); + + expect( + rtlScreen.getByRole('radiogroup', { + name: /Tracking:/i, + }) + ).toBeInTheDocument(); + expect(rtlScreen.getAllByRole('radio')).toHaveLength(2); + }); +}); diff --git a/src/components/organisms/forms/settings-form/ackee-toggle/ackee-toggle.tsx b/src/components/organisms/forms/settings-form/ackee-toggle/ackee-toggle.tsx new file mode 100644 index 0000000..0eee1fc --- /dev/null +++ b/src/components/organisms/forms/settings-form/ackee-toggle/ackee-toggle.tsx @@ -0,0 +1,116 @@ +import { forwardRef, type ForwardRefRenderFunction } from 'react'; +import { useIntl } from 'react-intl'; +import { useAckee, useBoolean } from '../../../../../utils/hooks'; +import { Legend, List, ListItem } from '../../../../atoms'; +import { + Switch, + type SwitchOption, + type SwitchProps, + Tooltip, + type TooltipProps, +} from '../../../../molecules'; +import styles from './ackee-toggle.module.scss'; + +export type AckeeToggleProps = Omit< + SwitchProps, + 'isInline' | 'items' | 'legend' | 'name' | 'onSwitch' | 'value' +> & + Pick<TooltipProps, 'direction'>; + +const AckeeToggleWithRef: ForwardRefRenderFunction< + HTMLFieldSetElement, + AckeeToggleProps +> = ({ direction, ...props }, ref) => { + const intl = useIntl(); + const [tracking, toggleTracking] = useAckee(); + const { + deactivate: closeTooltip, + state: isTooltipOpened, + toggle: toggleTooltip, + } = useBoolean(false); + + const messages = { + legend: intl.formatMessage({ + defaultMessage: 'Tracking:', + description: 'AckeeToggle: select label', + id: '0gVlI3', + }), + options: { + full: intl.formatMessage({ + defaultMessage: 'Full', + description: 'AckeeToggle: full option name', + id: '5eD6y2', + }), + partial: intl.formatMessage({ + defaultMessage: 'Partial', + description: 'AckeeToggle: partial option name', + id: 'tIZYpD', + }), + }, + tooltip: { + heading: intl.formatMessage({ + defaultMessage: 'Ackee tracking (analytics)', + description: 'AckeeToggle: tooltip title', + id: 'nGss/j', + }), + contents: { + full: 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', + }), + partial: intl.formatMessage({ + defaultMessage: 'Partial includes only page url, views and duration.', + description: 'AckeeToggle: tooltip message', + id: 'ZB/Aw2', + }), + }, + }, + }; + + const options = [ + { id: 'ackee-full' as const, label: messages.options.full, value: 'full' }, + { + id: 'ackee-partial' as const, + label: messages.options.partial, + value: 'partial', + }, + ] satisfies [SwitchOption, SwitchOption]; + + return ( + <Switch + {...props} + isInline + items={options} + legend={<Legend>{messages.legend}</Legend>} + // eslint-disable-next-line react/jsx-no-literals + name="ackee" + onSwitch={toggleTracking} + ref={ref} + tooltip={ + <Tooltip + className={styles.tooltip} + direction={direction} + heading={messages.tooltip.heading} + isOpen={isTooltipOpened} + onClickOutside={closeTooltip} + onToggle={toggleTooltip} + > + <List> + <ListItem>{messages.tooltip.contents.full}</ListItem> + <ListItem>{messages.tooltip.contents.partial}</ListItem> + </List> + </Tooltip> + } + value={tracking} + /> + ); +}; + +/** + * AckeeToggle component + * + * Render a Toggle component to set tracking. + */ +export const AckeeToggle = forwardRef(AckeeToggleWithRef); diff --git a/src/components/organisms/forms/ackee-toggle/index.ts b/src/components/organisms/forms/settings-form/ackee-toggle/index.ts index 7f6313c..7f6313c 100644 --- a/src/components/organisms/forms/ackee-toggle/index.ts +++ b/src/components/organisms/forms/settings-form/ackee-toggle/index.ts diff --git a/src/components/organisms/forms/settings-form/index.ts b/src/components/organisms/forms/settings-form/index.ts new file mode 100644 index 0000000..7c2a66e --- /dev/null +++ b/src/components/organisms/forms/settings-form/index.ts @@ -0,0 +1 @@ +export * from './settings-form'; diff --git a/src/components/organisms/forms/motion-toggle/index.ts b/src/components/organisms/forms/settings-form/motion-toggle/index.ts index 0e35578..0e35578 100644 --- a/src/components/organisms/forms/motion-toggle/index.ts +++ b/src/components/organisms/forms/settings-form/motion-toggle/index.ts diff --git a/src/components/organisms/forms/motion-toggle/motion-toggle.stories.tsx b/src/components/organisms/forms/settings-form/motion-toggle/motion-toggle.stories.tsx index 7adef1b..67cea37 100644 --- a/src/components/organisms/forms/motion-toggle/motion-toggle.stories.tsx +++ b/src/components/organisms/forms/settings-form/motion-toggle/motion-toggle.stories.tsx @@ -5,7 +5,7 @@ import { MotionToggle } from './motion-toggle'; * MotionToggle - Storybook Meta */ export default { - title: 'Organisms/Forms/Toggle', + title: 'Organisms/Forms/Settings/Items', component: MotionToggle, argTypes: {}, } as ComponentMeta<typeof MotionToggle>; diff --git a/src/components/organisms/forms/settings-form/motion-toggle/motion-toggle.test.tsx b/src/components/organisms/forms/settings-form/motion-toggle/motion-toggle.test.tsx new file mode 100644 index 0000000..48c7151 --- /dev/null +++ b/src/components/organisms/forms/settings-form/motion-toggle/motion-toggle.test.tsx @@ -0,0 +1,21 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '../../../../../../tests/utils'; +import { MotionProvider } from '../../../../../utils/providers'; +import { MotionToggle } from './motion-toggle'; + +describe('MotionToggle', () => { + it('renders a radio group of two radio buttons', () => { + render( + <MotionProvider attribute="enim" hasReducedMotion storageKey="autem"> + <MotionToggle /> + </MotionProvider> + ); + + expect( + rtlScreen.getByRole('radiogroup', { + name: /Animations:/i, + }) + ).toBeInTheDocument(); + expect(rtlScreen.getAllByRole('radio')).toHaveLength(2); + }); +}); diff --git a/src/components/organisms/forms/settings-form/motion-toggle/motion-toggle.tsx b/src/components/organisms/forms/settings-form/motion-toggle/motion-toggle.tsx new file mode 100644 index 0000000..9ee236a --- /dev/null +++ b/src/components/organisms/forms/settings-form/motion-toggle/motion-toggle.tsx @@ -0,0 +1,76 @@ +import { forwardRef, type ForwardRefRenderFunction } from 'react'; +import { useIntl } from 'react-intl'; +import { useReducedMotion } from '../../../../../utils/hooks'; +import { Legend } from '../../../../atoms'; +import { + Switch, + type SwitchOption, + type SwitchProps, +} from '../../../../molecules'; + +export type MotionToggleProps = Omit< + SwitchProps, + 'isInline' | 'items' | 'legend' | 'name' | 'onSwitch' | 'value' +>; + +const MotionToggleWithRef: ForwardRefRenderFunction< + HTMLFieldSetElement, + MotionToggleProps +> = (props, ref) => { + const intl = useIntl(); + const { isReduced, toggleReducedMotion } = useReducedMotion(); + + const messages = { + legend: intl.formatMessage({ + defaultMessage: 'Animations:', + description: 'MotionToggle: reduce motion label', + id: '/q5csZ', + }), + options: { + on: intl.formatMessage({ + defaultMessage: 'On', + description: 'MotionToggle: activate reduce motion label', + id: 'va65iw', + }), + off: intl.formatMessage({ + defaultMessage: 'Off', + description: 'MotionToggle: deactivate reduce motion label', + id: 'pWKyyR', + }), + }, + }; + + const options: [SwitchOption, SwitchOption] = [ + { + id: 'reduced-motion-on', + label: messages.options.on, + value: 'on', + }, + { + id: 'reduced-motion-off', + label: messages.options.off, + value: 'off', + }, + ]; + + return ( + <Switch + {...props} + isInline + items={options} + legend={<Legend>{messages.legend}</Legend>} + // eslint-disable-next-line react/jsx-no-literals + name="reduced-motion" + onSwitch={toggleReducedMotion} + ref={ref} + value={isReduced ? 'off' : 'on'} + /> + ); +}; + +/** + * MotionToggle component + * + * Render a Toggle component to set reduce motion. + */ +export const MotionToggle = forwardRef(MotionToggleWithRef); diff --git a/src/components/organisms/forms/prism-theme-toggle/index.ts b/src/components/organisms/forms/settings-form/prism-theme-toggle/index.ts index f4e490f..f4e490f 100644 --- a/src/components/organisms/forms/prism-theme-toggle/index.ts +++ b/src/components/organisms/forms/settings-form/prism-theme-toggle/index.ts diff --git a/src/components/organisms/forms/prism-theme-toggle/prism-theme-toggle.stories.tsx b/src/components/organisms/forms/settings-form/prism-theme-toggle/prism-theme-toggle.stories.tsx index 3aeb78b..9313bf2 100644 --- a/src/components/organisms/forms/prism-theme-toggle/prism-theme-toggle.stories.tsx +++ b/src/components/organisms/forms/settings-form/prism-theme-toggle/prism-theme-toggle.stories.tsx @@ -5,7 +5,7 @@ import { PrismThemeToggle } from './prism-theme-toggle'; * PrismThemeToggle - Storybook Meta */ export default { - title: 'Organisms/Forms/Toggle', + title: 'Organisms/Forms/Settings/Items', component: PrismThemeToggle, argTypes: {}, } as ComponentMeta<typeof PrismThemeToggle>; diff --git a/src/components/organisms/forms/settings-form/prism-theme-toggle/prism-theme-toggle.test.tsx b/src/components/organisms/forms/settings-form/prism-theme-toggle/prism-theme-toggle.test.tsx new file mode 100644 index 0000000..b9f05c4 --- /dev/null +++ b/src/components/organisms/forms/settings-form/prism-theme-toggle/prism-theme-toggle.test.tsx @@ -0,0 +1,27 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '../../../../../../tests/utils'; +import { PrismThemeProvider } from '../../../../../utils/providers'; +import { PrismThemeToggle } from './prism-theme-toggle'; + +describe('PrismThemeToggle', () => { + it('renders a radio group of two radio buttons', () => { + const defaultTheme = 'dark'; + + render( + <PrismThemeProvider + attribute="fuga" + storageKey="sed" + defaultTheme={defaultTheme} + > + <PrismThemeToggle /> + </PrismThemeProvider> + ); + + expect( + rtlScreen.getByRole('radiogroup', { + name: /Code blocks:/i, + }) + ).toBeInTheDocument(); + expect(rtlScreen.getAllByRole('radio')).toHaveLength(2); + }); +}); diff --git a/src/components/organisms/forms/settings-form/prism-theme-toggle/prism-theme-toggle.tsx b/src/components/organisms/forms/settings-form/prism-theme-toggle/prism-theme-toggle.tsx new file mode 100644 index 0000000..b427754 --- /dev/null +++ b/src/components/organisms/forms/settings-form/prism-theme-toggle/prism-theme-toggle.tsx @@ -0,0 +1,78 @@ +import { forwardRef, type ForwardRefRenderFunction } from 'react'; +import { useIntl } from 'react-intl'; +import { usePrismTheme } from '../../../../../utils/hooks'; +import { Icon, Legend } from '../../../../atoms'; +import { + Switch, + type SwitchOption, + type SwitchProps, +} from '../../../../molecules'; + +export type PrismThemeToggleProps = Omit< + SwitchProps, + 'isInline' | 'items' | 'legend' | 'name' | 'onSwitch' | 'value' +>; + +const PrismThemeToggleWithRef: ForwardRefRenderFunction< + HTMLFieldSetElement, + PrismThemeToggleProps +> = (props, ref) => { + const intl = useIntl(); + const { currentTheme, toggleTheme } = usePrismTheme(); + + const messages = { + legend: intl.formatMessage({ + defaultMessage: 'Code blocks:', + description: 'PrismThemeToggle: theme label', + id: 'ftXN+0', + }), + options: { + dark: intl.formatMessage({ + defaultMessage: 'Dark theme', + description: 'PrismThemeToggle: dark theme label', + id: 'og/zWL', + }), + light: intl.formatMessage({ + defaultMessage: 'Light theme', + description: 'PrismThemeToggle: light theme label', + id: 'tsWh8x', + }), + }, + }; + + const options: [SwitchOption, SwitchOption] = [ + { + id: 'code-blocks-light', + // eslint-disable-next-line react/jsx-no-literals + label: <Icon aria-label={messages.options.light} shape="sun" size="sm" />, + value: 'light', + }, + { + id: 'code-blocks-dark', + // eslint-disable-next-line react/jsx-no-literals + label: <Icon aria-label={messages.options.dark} shape="moon" size="sm" />, + value: 'dark', + }, + ]; + + return ( + <Switch + {...props} + isInline + items={options} + legend={<Legend>{messages.legend}</Legend>} + // eslint-disable-next-line react/jsx-no-literals + name="code-blocks" + onSwitch={toggleTheme} + ref={ref} + value={currentTheme} + /> + ); +}; + +/** + * PrismThemeToggle component + * + * Render a Toggle component to set code blocks theme. + */ +export const PrismThemeToggle = forwardRef(PrismThemeToggleWithRef); diff --git a/src/components/organisms/forms/settings-form/settings-form.module.scss b/src/components/organisms/forms/settings-form/settings-form.module.scss new file mode 100644 index 0000000..88a71a7 --- /dev/null +++ b/src/components/organisms/forms/settings-form/settings-form.module.scss @@ -0,0 +1,30 @@ +@use "../../../../styles/abstracts/mixins" as mix; + +.form { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + row-gap: var(--spacing-xs); + column-gap: var(--spacing-lg); + + @include mix.media("screen") { + @include mix.dimensions(null, "2xs", "height") { + row-gap: var(--spacing-2xs); + } + } +} + +.item { + flex-basis: 100%; + + > *:last-child { + flex-basis: 100%; + margin-inline-start: auto; + + @include mix.media("screen") { + @include mix.dimensions("2xs") { + flex-basis: auto; + } + } + } +} diff --git a/src/components/organisms/forms/settings-form/settings-form.stories.tsx b/src/components/organisms/forms/settings-form/settings-form.stories.tsx new file mode 100644 index 0000000..da93bfa --- /dev/null +++ b/src/components/organisms/forms/settings-form/settings-form.stories.tsx @@ -0,0 +1,21 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { SettingsForm } from './settings-form'; + +/** + * SettingsForm - Storybook Meta + */ +export default { + title: 'Organisms/Forms/Settings', + component: SettingsForm, + argTypes: {}, +} as ComponentMeta<typeof SettingsForm>; + +const Template: ComponentStory<typeof SettingsForm> = (args) => ( + <SettingsForm {...args} /> +); + +/** + * Forms Stories - Settings + */ +export const Settings = Template.bind({}); +Settings.args = {}; diff --git a/src/components/organisms/forms/settings-form/settings-form.test.tsx b/src/components/organisms/forms/settings-form/settings-form.test.tsx new file mode 100644 index 0000000..7029595 --- /dev/null +++ b/src/components/organisms/forms/settings-form/settings-form.test.tsx @@ -0,0 +1,15 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '../../../../../tests/utils'; +import { SettingsForm } from './settings-form'; + +describe('SettingsForm', () => { + it('renders a form with four settings', () => { + const label = 'voluptatem maiores rerum'; + const settingsNumber = 4; + + render(<SettingsForm aria-label={label} />); + + expect(rtlScreen.getByRole('form', { name: label })).toBeInTheDocument(); + expect(rtlScreen.getAllByRole('radiogroup')).toHaveLength(settingsNumber); + }); +}); diff --git a/src/components/organisms/forms/settings-form/settings-form.tsx b/src/components/organisms/forms/settings-form/settings-form.tsx new file mode 100644 index 0000000..117665d --- /dev/null +++ b/src/components/organisms/forms/settings-form/settings-form.tsx @@ -0,0 +1,31 @@ +import { type ForwardRefRenderFunction, forwardRef } from 'react'; +import { Form, type FormProps } from '../../../atoms'; +import { AckeeToggle } from './ackee-toggle'; +import { MotionToggle } from './motion-toggle'; +import { PrismThemeToggle } from './prism-theme-toggle'; +import styles from './settings-form.module.scss'; +import { ThemeToggle } from './theme-toggle'; + +export type SettingsFormProps = Omit<FormProps, 'children'>; + +const SettingsFormWithRef: ForwardRefRenderFunction< + HTMLFormElement, + SettingsFormProps +> = ({ className = '', ...props }, ref) => { + const formClass = `${styles.form} ${className}`; + + return ( + <Form {...props} className={formClass} ref={ref}> + <ThemeToggle className={styles.item} /> + <PrismThemeToggle className={styles.item} /> + <MotionToggle className={styles.item} /> + <AckeeToggle + className={styles.item} + // eslint-disable-next-line react/jsx-no-literals + direction="upwards" + /> + </Form> + ); +}; + +export const SettingsForm = forwardRef(SettingsFormWithRef); diff --git a/src/components/organisms/forms/theme-toggle/index.ts b/src/components/organisms/forms/settings-form/theme-toggle/index.ts index 0dbf668..0dbf668 100644 --- a/src/components/organisms/forms/theme-toggle/index.ts +++ b/src/components/organisms/forms/settings-form/theme-toggle/index.ts diff --git a/src/components/organisms/forms/theme-toggle/theme-toggle.stories.tsx b/src/components/organisms/forms/settings-form/theme-toggle/theme-toggle.stories.tsx index bfec65e..4742adf 100644 --- a/src/components/organisms/forms/theme-toggle/theme-toggle.stories.tsx +++ b/src/components/organisms/forms/settings-form/theme-toggle/theme-toggle.stories.tsx @@ -5,7 +5,7 @@ import { ThemeToggle } from './theme-toggle'; * ThemeToggle - Storybook Meta */ export default { - title: 'Organisms/Forms/Toggle', + title: 'Organisms/Forms/Settings/Items', component: ThemeToggle, argTypes: {}, } as ComponentMeta<typeof ThemeToggle>; diff --git a/src/components/organisms/forms/settings-form/theme-toggle/theme-toggle.test.tsx b/src/components/organisms/forms/settings-form/theme-toggle/theme-toggle.test.tsx new file mode 100644 index 0000000..e74842e --- /dev/null +++ b/src/components/organisms/forms/settings-form/theme-toggle/theme-toggle.test.tsx @@ -0,0 +1,27 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '../../../../../../tests/utils'; +import { ThemeProvider } from '../../../../../utils/providers'; +import { ThemeToggle } from './theme-toggle'; + +describe('ThemeToggle', () => { + it('renders a radio group of two radio buttons', () => { + const defaultTheme = 'dark'; + + render( + <ThemeProvider + attribute="voluptas" + storageKey="alias" + defaultTheme={defaultTheme} + > + <ThemeToggle /> + </ThemeProvider> + ); + + expect( + rtlScreen.getByRole('radiogroup', { + name: /Theme:/i, + }) + ).toBeInTheDocument(); + expect(rtlScreen.getAllByRole('radio')).toHaveLength(2); + }); +}); diff --git a/src/components/organisms/forms/settings-form/theme-toggle/theme-toggle.tsx b/src/components/organisms/forms/settings-form/theme-toggle/theme-toggle.tsx new file mode 100644 index 0000000..d719434 --- /dev/null +++ b/src/components/organisms/forms/settings-form/theme-toggle/theme-toggle.tsx @@ -0,0 +1,78 @@ +import { forwardRef, type ForwardRefRenderFunction } from 'react'; +import { useIntl } from 'react-intl'; +import { useTheme } from '../../../../../utils/hooks'; +import { Icon, Legend } from '../../../../atoms'; +import { + Switch, + type SwitchOption, + type SwitchProps, +} from '../../../../molecules'; + +export type ThemeToggleProps = Omit< + SwitchProps, + 'isInline' | 'items' | 'legend' | 'name' | 'onSwitch' | 'value' +>; + +const ThemeToggleWithRef: ForwardRefRenderFunction< + HTMLFieldSetElement, + ThemeToggleProps +> = (props, ref) => { + const intl = useIntl(); + const { resolvedTheme, toggleTheme } = useTheme(); + + const messages = { + legend: intl.formatMessage({ + defaultMessage: 'Theme:', + description: 'ThemeToggle: theme label', + id: 'suXOBu', + }), + options: { + dark: intl.formatMessage({ + defaultMessage: 'Dark theme', + description: 'ThemeToggle: dark theme label', + id: '2QwvtS', + }), + light: intl.formatMessage({ + defaultMessage: 'Light theme', + description: 'ThemeToggle: light theme label', + id: 'Ygea7s', + }), + }, + }; + + const options: [SwitchOption, SwitchOption] = [ + { + id: 'theme-light', + // eslint-disable-next-line react/jsx-no-literals + label: <Icon aria-label={messages.options.light} shape="sun" size="sm" />, + value: 'light', + }, + { + id: 'theme-dark', + // eslint-disable-next-line react/jsx-no-literals + label: <Icon aria-label={messages.options.dark} shape="moon" size="sm" />, + value: 'dark', + }, + ]; + + return ( + <Switch + {...props} + isInline + items={options} + legend={<Legend>{messages.legend}</Legend>} + // eslint-disable-next-line react/jsx-no-literals + name="theme" + onSwitch={toggleTheme} + ref={ref} + value={resolvedTheme} + /> + ); +}; + +/** + * ThemeToggle component + * + * Render a Toggle component to set theme. + */ +export const ThemeToggle = forwardRef(ThemeToggleWithRef); diff --git a/src/components/organisms/forms/theme-toggle/theme-toggle.test.tsx b/src/components/organisms/forms/theme-toggle/theme-toggle.test.tsx deleted file mode 100644 index d735936..0000000 --- a/src/components/organisms/forms/theme-toggle/theme-toggle.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../../tests/utils'; -import { ThemeToggle } from './theme-toggle'; - -describe('ThemeToggle', () => { - it('renders a toggle component', () => { - render(<ThemeToggle />); - expect( - rtlScreen.getByRole('radiogroup', { - name: /Theme:/i, - }) - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/organisms/forms/theme-toggle/theme-toggle.tsx b/src/components/organisms/forms/theme-toggle/theme-toggle.tsx deleted file mode 100644 index 88f3c75..0000000 --- a/src/components/organisms/forms/theme-toggle/theme-toggle.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import type { FC } from 'react'; -import { useIntl } from 'react-intl'; -import { Icon, Legend } from '../../../atoms'; -import { - Switch, - type SwitchOption, - type SwitchProps, -} from '../../../molecules'; -import { useTheme } from 'src/utils/hooks'; - -export type ThemeToggleProps = Omit< - SwitchProps, - 'isInline' | 'items' | 'name' | 'onSwitch' | 'value' ->; - -/** - * ThemeToggle component - * - * Render a Toggle component to set theme. - */ -export const ThemeToggle: FC<ThemeToggleProps> = (props) => { - const intl = useIntl(); - const { resolvedTheme, toggleTheme } = useTheme(); - const isDarkTheme = resolvedTheme === 'dark'; - - const themeLabel = intl.formatMessage({ - defaultMessage: 'Theme:', - description: 'ThemeToggle: theme label', - id: 'suXOBu', - }); - const lightThemeLabel = intl.formatMessage({ - defaultMessage: 'Light theme', - description: 'ThemeToggle: light theme label', - id: 'Ygea7s', - }); - const darkThemeLabel = intl.formatMessage({ - defaultMessage: 'Dark theme', - description: 'ThemeToggle: dark theme label', - id: '2QwvtS', - }); - - const options: [SwitchOption, SwitchOption] = [ - { - id: 'theme-light', - label: ( - <> - <span className="screen-reader-text">{lightThemeLabel}</span> - <Icon shape="sun" /> - </> - ), - value: 'light', - }, - { - id: 'theme-dark', - label: ( - <> - <span className="screen-reader-text">{darkThemeLabel}</span> - <Icon shape="moon" /> - </> - ), - value: 'dark', - }, - ]; - - return ( - <Switch - {...props} - isInline - items={options} - legend={<Legend>{themeLabel}</Legend>} - name="theme" - onSwitch={toggleTheme} - value={isDarkTheme ? 'dark' : 'light'} - /> - ); -}; diff --git a/src/components/organisms/modals/settings-modal.module.scss b/src/components/organisms/modals/settings-modal.module.scss index 6576cf5..47af656 100644 --- a/src/components/organisms/modals/settings-modal.module.scss +++ b/src/components/organisms/modals/settings-modal.module.scss @@ -14,21 +14,6 @@ } } -.form { - display: flex; - flex-flow: row wrap; - column-gap: var(--spacing-lg); -} - -.item { - width: 100%; - margin: 0 0 var(--spacing-2xs); - - > *:last-child { - margin-left: auto; - } -} - .icon { margin-right: var(--spacing-2xs); } diff --git a/src/components/organisms/modals/settings-modal.tsx b/src/components/organisms/modals/settings-modal.tsx index 5fea491..94d69e2 100644 --- a/src/components/organisms/modals/settings-modal.tsx +++ b/src/components/organisms/modals/settings-modal.tsx @@ -1,12 +1,7 @@ import { useCallback, type FC, type FormEvent } from 'react'; import { useIntl } from 'react-intl'; -import { Form, Heading, Icon, Modal, type ModalProps } from '../../atoms'; -import { - AckeeToggle, - MotionToggle, - PrismThemeToggle, - ThemeToggle, -} from '../forms'; +import { Heading, Icon, Modal, type ModalProps } from '../../atoms'; +import { SettingsForm } from '../forms'; import styles from './settings-modal.module.scss'; export type SettingsModalProps = Pick<ModalProps, 'className'>; @@ -43,16 +38,7 @@ export const SettingsModal: FC<SettingsModalProps> = ({ className = '' }) => { </Heading> } > - <Form - aria-label={ariaLabel} - className={styles.form} - onSubmit={submitHandler} - > - <ThemeToggle className={styles.item} /> - <PrismThemeToggle className={styles.item} /> - <MotionToggle className={styles.item} /> - <AckeeToggle className={styles.item} direction="upwards" /> - </Form> + <SettingsForm aria-label={ariaLabel} onSubmit={submitHandler} /> </Modal> ); }; diff --git a/src/components/organisms/toolbar/toolbar.module.scss b/src/components/organisms/toolbar/toolbar.module.scss index 1254f64..ac7c892 100644 --- a/src/components/organisms/toolbar/toolbar.module.scss +++ b/src/components/organisms/toolbar/toolbar.module.scss @@ -56,7 +56,7 @@ .modal { &--search, &--settings { - min-width: fun.convert-px(380); + min-width: fun.convert-px(400); } } } |
