diff options
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); } } } |
