diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-05-18 14:27:11 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-05-18 14:27:11 +0200 |
| commit | b214baab3e17d92f784b4f782863deafc5558ee4 (patch) | |
| tree | cdc20c7e77ba6926285917eead8bb088bdc843f8 /src/components/organisms | |
| parent | 54883bb5c36cf21462a421605a709fdd6f04b150 (diff) | |
chore: close toolbar modals on click/focus outside
Diffstat (limited to 'src/components/organisms')
16 files changed, 199 insertions, 118 deletions
diff --git a/src/components/organisms/forms/settings-form.module.scss b/src/components/organisms/forms/settings-form.module.scss new file mode 100644 index 0000000..a6a2077 --- /dev/null +++ b/src/components/organisms/forms/settings-form.module.scss @@ -0,0 +1,11 @@ +@use "@styles/abstracts/mixins" as mix; + +.label { + margin-right: auto; + + @include mix.media("screen") { + @include mix.dimensions(null, "2xs", "height") { + font-size: var(--font-size-sm); + } + } +} diff --git a/src/components/organisms/forms/settings-form.stories.tsx b/src/components/organisms/forms/settings-form.stories.tsx new file mode 100644 index 0000000..46305e7 --- /dev/null +++ b/src/components/organisms/forms/settings-form.stories.tsx @@ -0,0 +1,47 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import SettingsForm from './settings-form'; + +/** + * SettingsModal - Storybook Meta + */ +export default { + title: 'Organisms/Forms', + component: SettingsForm, + argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the modal wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + tooltipClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the tooltip wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta<typeof SettingsForm>; + +const Template: ComponentStory<typeof SettingsForm> = (args) => ( + <SettingsForm {...args} /> +); + +/** + * Form Stories - Settings + */ +export const Settings = Template.bind({}); diff --git a/src/components/organisms/forms/settings-form.test.tsx b/src/components/organisms/forms/settings-form.test.tsx new file mode 100644 index 0000000..beb65ec --- /dev/null +++ b/src/components/organisms/forms/settings-form.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from '@test-utils'; +import SettingsForm from './settings-form'; + +describe('SettingsForm', () => { + it('renders a form', () => { + render(<SettingsForm />); + expect( + screen.getByRole('form', { name: /^Settings form/i }) + ).toBeInTheDocument(); + }); + + it('renders a theme toggle setting', () => { + render(<SettingsForm />); + expect( + screen.getByRole('checkbox', { name: /^Theme:/i }) + ).toBeInTheDocument(); + }); + + it('renders a code blocks toggle setting', () => { + render(<SettingsForm />); + expect( + screen.getByRole('checkbox', { name: /^Code blocks:/i }) + ).toBeInTheDocument(); + }); + + it('renders a motion setting', () => { + render(<SettingsForm />); + expect( + screen.getByRole('checkbox', { name: /^Animations:/i }) + ).toBeInTheDocument(); + }); + + it('renders a Ackee setting', () => { + render(<SettingsForm />); + expect( + screen.getByRole('combobox', { name: /^Tracking:/i }) + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/organisms/forms/settings-form.tsx b/src/components/organisms/forms/settings-form.tsx new file mode 100644 index 0000000..0a34601 --- /dev/null +++ b/src/components/organisms/forms/settings-form.tsx @@ -0,0 +1,36 @@ +import Form from '@components/atoms/forms/form'; +import AckeeSelect, { + type AckeeSelectProps, +} from '@components/molecules/forms/ackee-select'; +import MotionToggle from '@components/molecules/forms/motion-toggle'; +import PrismThemeToggle from '@components/molecules/forms/prism-theme-toggle'; +import ThemeToggle from '@components/molecules/forms/theme-toggle'; +import { FC } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './settings-form.module.scss'; + +export type SettingsFormProps = Pick<AckeeSelectProps, 'tooltipClassName'>; + +const SettingsForm: FC<SettingsFormProps> = ({ tooltipClassName }) => { + const intl = useIntl(); + const ariaLabel = intl.formatMessage({ + defaultMessage: 'Settings form', + id: 'gX+YVy', + description: 'SettingsForm: an accessible form name', + }); + + return ( + <Form aria-label={ariaLabel} onSubmit={() => null}> + <ThemeToggle labelClassName={styles.label} value={false} /> + <PrismThemeToggle labelClassName={styles.label} value={false} /> + <MotionToggle labelClassName={styles.label} value={false} /> + <AckeeSelect + initialValue="full" + labelClassName={styles.label} + tooltipClassName={tooltipClassName} + /> + </Form> + ); +}; + +export default SettingsForm; diff --git a/src/components/organisms/modals/search-modal.tsx b/src/components/organisms/modals/search-modal.tsx index 866bc25..e92bf1b 100644 --- a/src/components/organisms/modals/search-modal.tsx +++ b/src/components/organisms/modals/search-modal.tsx @@ -1,9 +1,18 @@ +import Spinner from '@components/atoms/loaders/spinner'; import Modal, { type ModalProps } from '@components/molecules/modals/modal'; +import dynamic from 'next/dynamic'; import { FC } from 'react'; import { useIntl } from 'react-intl'; -import SearchForm, { SearchFormProps } from '../forms/search-form'; +import { type SearchFormProps } from '../forms/search-form'; import styles from './search-modal.module.scss'; +const DynamicSearchForm = dynamic( + () => import('@components/organisms/forms/search-form'), + { + loading: () => <Spinner />, + } +); + export type SearchModalProps = Pick<SearchFormProps, 'searchPage'> & { /** * Set additional classnames to modal wrapper. @@ -26,7 +35,7 @@ const SearchModal: FC<SearchModalProps> = ({ className, searchPage }) => { return ( <Modal title={modalTitle} className={`${styles.wrapper} ${className}`}> - <SearchForm hideLabel={true} searchPage={searchPage} /> + <DynamicSearchForm hideLabel={true} searchPage={searchPage} /> </Modal> ); }; diff --git a/src/components/organisms/modals/settings-modal.module.scss b/src/components/organisms/modals/settings-modal.module.scss index ebae3da..a6a2077 100644 --- a/src/components/organisms/modals/settings-modal.module.scss +++ b/src/components/organisms/modals/settings-modal.module.scss @@ -1,21 +1,11 @@ @use "@styles/abstracts/mixins" as mix; -.wrapper { - .label { - margin-right: auto; - } +.label { + margin-right: auto; @include mix.media("screen") { @include mix.dimensions(null, "2xs", "height") { font-size: var(--font-size-sm); - - .heading { - font-size: var(--font-size-lg); - } - - .label { - font-size: var(--font-size-sm); - } } } } diff --git a/src/components/organisms/modals/settings-modal.stories.tsx b/src/components/organisms/modals/settings-modal.stories.tsx index 0abe004..0fe8c18 100644 --- a/src/components/organisms/modals/settings-modal.stories.tsx +++ b/src/components/organisms/modals/settings-modal.stories.tsx @@ -1,5 +1,4 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { IntlProvider } from 'react-intl'; import SettingsModal from './settings-modal'; /** @@ -36,13 +35,6 @@ export default { }, }, }, - decorators: [ - (Story) => ( - <IntlProvider locale="en"> - <Story /> - </IntlProvider> - ), - ], } as ComponentMeta<typeof SettingsModal>; const Template: ComponentStory<typeof SettingsModal> = (args) => ( diff --git a/src/components/organisms/modals/settings-modal.test.tsx b/src/components/organisms/modals/settings-modal.test.tsx index 6291e54..acbf7d1 100644 --- a/src/components/organisms/modals/settings-modal.test.tsx +++ b/src/components/organisms/modals/settings-modal.test.tsx @@ -2,31 +2,8 @@ import { render, screen } from '@test-utils'; import SettingsModal from './settings-modal'; describe('SettingsModal', () => { - it('renders a theme toggle setting', () => { + it('renders a fake heading', () => { render(<SettingsModal />); - expect( - screen.getByRole('checkbox', { name: /^Theme:/i }) - ).toBeInTheDocument(); - }); - - it('renders a code blocks toggle setting', () => { - render(<SettingsModal />); - expect( - screen.getByRole('checkbox', { name: /^Code blocks:/i }) - ).toBeInTheDocument(); - }); - - it('renders a motion setting', () => { - render(<SettingsModal />); - expect( - screen.getByRole('checkbox', { name: /^Animations:/i }) - ).toBeInTheDocument(); - }); - - it('renders a Ackee setting', () => { - render(<SettingsModal />); - expect( - screen.getByRole('combobox', { name: /^Tracking:/i }) - ).toBeInTheDocument(); + expect(screen.getByText(/Settings/i)).toBeInTheDocument(); }); }); diff --git a/src/components/organisms/modals/settings-modal.tsx b/src/components/organisms/modals/settings-modal.tsx index 20d2605..e724076 100644 --- a/src/components/organisms/modals/settings-modal.tsx +++ b/src/components/organisms/modals/settings-modal.tsx @@ -1,25 +1,20 @@ -import Form from '@components/atoms/forms/form'; -import AckeeSelect, { - type AckeeSelectProps, -} from '@components/molecules/forms/ackee-select'; -import MotionToggle from '@components/molecules/forms/motion-toggle'; -import PrismThemeToggle from '@components/molecules/forms/prism-theme-toggle'; -import ThemeToggle from '@components/molecules/forms/theme-toggle'; +import Spinner from '@components/atoms/loaders/spinner'; import Modal, { type ModalProps } from '@components/molecules/modals/modal'; +import dynamic from 'next/dynamic'; import { FC } from 'react'; import { useIntl } from 'react-intl'; +import { type SettingsFormProps } from '../forms/settings-form'; import styles from './settings-modal.module.scss'; -export type SettingsModalProps = { - /** - * Set additional classnames to the modal wrapper. - */ - className?: ModalProps['className']; - /** - * Set additional classnames to the tooltip wrapper. - */ - tooltipClassName?: AckeeSelectProps['tooltipClassName']; -}; +const DynamicSettingsForm = dynamic( + () => import('@components/organisms/forms/settings-form'), + { + loading: () => <Spinner />, + } +); + +export type SettingsModalProps = Pick<ModalProps, 'className'> & + Pick<SettingsFormProps, 'tooltipClassName'>; /** * SettingsModal component @@ -28,7 +23,7 @@ export type SettingsModalProps = { */ const SettingsModal: FC<SettingsModalProps> = ({ className = '', - tooltipClassName = '', + ...props }) => { const intl = useIntl(); const title = intl.formatMessage({ @@ -44,16 +39,7 @@ const SettingsModal: FC<SettingsModalProps> = ({ className={`${styles.wrapper} ${className}`} headingClassName={styles.heading} > - <Form onSubmit={() => null}> - <ThemeToggle labelClassName={styles.label} value={false} /> - <PrismThemeToggle labelClassName={styles.label} value={false} /> - <MotionToggle labelClassName={styles.label} value={false} /> - <AckeeSelect - initialValue="full" - labelClassName={styles.label} - tooltipClassName={tooltipClassName} - /> - </Form> + <DynamicSettingsForm {...props} /> </Modal> ); }; diff --git a/src/components/organisms/toolbar/main-nav.tsx b/src/components/organisms/toolbar/main-nav.tsx index 35e3fd6..d205112 100644 --- a/src/components/organisms/toolbar/main-nav.tsx +++ b/src/components/organisms/toolbar/main-nav.tsx @@ -5,7 +5,7 @@ import Nav, { type NavProps, type NavItem, } from '@components/molecules/nav/nav'; -import { FC } from 'react'; +import { forwardRef, ForwardRefRenderFunction } from 'react'; import { useIntl } from 'react-intl'; import mainNavStyles from './main-nav.module.scss'; import sharedStyles from './toolbar-items.module.scss'; @@ -34,12 +34,10 @@ export type MainNavProps = { * * Render the main navigation. */ -const MainNav: FC<MainNavProps> = ({ - className = '', - isActive, - items, - setIsActive, -}) => { +const MainNav: ForwardRefRenderFunction<HTMLDivElement, MainNavProps> = ( + { className = '', isActive, items, setIsActive }, + ref +) => { const intl = useIntl(); const label = isActive ? intl.formatMessage({ @@ -54,7 +52,7 @@ const MainNav: FC<MainNavProps> = ({ }); return ( - <div className={`${sharedStyles.item} ${mainNavStyles.item}`}> + <div className={`${sharedStyles.item} ${mainNavStyles.item}`} ref={ref}> <Checkbox id="main-nav-button" name="main-nav-button" @@ -79,4 +77,4 @@ const MainNav: FC<MainNavProps> = ({ ); }; -export default MainNav; +export default forwardRef(MainNav); diff --git a/src/components/organisms/toolbar/search.stories.tsx b/src/components/organisms/toolbar/search.stories.tsx index c6063a0..6aaffde 100644 --- a/src/components/organisms/toolbar/search.stories.tsx +++ b/src/components/organisms/toolbar/search.stories.tsx @@ -1,6 +1,5 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; import { useState } from 'react'; -import { IntlProvider } from 'react-intl'; import Search from './search'; /** diff --git a/src/components/organisms/toolbar/search.test.tsx b/src/components/organisms/toolbar/search.test.tsx index a18b679..7c77eac 100644 --- a/src/components/organisms/toolbar/search.test.tsx +++ b/src/components/organisms/toolbar/search.test.tsx @@ -11,9 +11,4 @@ describe('Search', () => { render(<Search searchPage="#" isActive={true} setIsActive={() => null} />); expect(screen.getByRole('checkbox')).toHaveAccessibleName('Close search'); }); - - it('renders a search form', () => { - render(<Search searchPage="#" isActive={true} setIsActive={() => null} />); - expect(screen.getByRole('searchbox')).toBeInTheDocument(); - }); }); diff --git a/src/components/organisms/toolbar/search.tsx b/src/components/organisms/toolbar/search.tsx index a1471ef..5695348 100644 --- a/src/components/organisms/toolbar/search.tsx +++ b/src/components/organisms/toolbar/search.tsx @@ -1,7 +1,7 @@ import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox'; import Label from '@components/atoms/forms/label'; import MagnifyingGlass from '@components/atoms/icons/magnifying-glass'; -import { FC } from 'react'; +import { forwardRef, ForwardRefRenderFunction } from 'react'; import { useIntl } from 'react-intl'; import SearchModal, { type SearchModalProps } from '../modals/search-modal'; import searchStyles from './search.module.scss'; @@ -26,12 +26,10 @@ export type SearchProps = { setIsActive: CheckboxProps['setValue']; }; -const Search: FC<SearchProps> = ({ - className = '', - isActive, - searchPage, - setIsActive, -}) => { +const Search: ForwardRefRenderFunction<HTMLDivElement, SearchProps> = ( + { className = '', isActive, searchPage, setIsActive }, + ref +) => { const intl = useIntl(); const label = isActive ? intl.formatMessage({ @@ -46,7 +44,7 @@ const Search: FC<SearchProps> = ({ }); return ( - <div className={`${sharedStyles.item} ${searchStyles.item}`}> + <div className={`${sharedStyles.item} ${searchStyles.item}`} ref={ref}> <Checkbox id="search-button" name="search-button" @@ -69,4 +67,4 @@ const Search: FC<SearchProps> = ({ ); }; -export default Search; +export default forwardRef(Search); diff --git a/src/components/organisms/toolbar/settings.stories.tsx b/src/components/organisms/toolbar/settings.stories.tsx index 1ec0897..aab4b9e 100644 --- a/src/components/organisms/toolbar/settings.stories.tsx +++ b/src/components/organisms/toolbar/settings.stories.tsx @@ -1,6 +1,5 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; import { useState } from 'react'; -import { IntlProvider } from 'react-intl'; import Settings from './settings'; /** @@ -57,13 +56,6 @@ export default { }, }, }, - decorators: [ - (Story) => ( - <IntlProvider locale="en"> - <Story /> - </IntlProvider> - ), - ], } as ComponentMeta<typeof Settings>; const Template: ComponentStory<typeof Settings> = ({ diff --git a/src/components/organisms/toolbar/settings.tsx b/src/components/organisms/toolbar/settings.tsx index 3b10226..43d3190 100644 --- a/src/components/organisms/toolbar/settings.tsx +++ b/src/components/organisms/toolbar/settings.tsx @@ -1,7 +1,7 @@ import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox'; import Label from '@components/atoms/forms/label'; import Cog from '@components/atoms/icons/cog'; -import { FC } from 'react'; +import { FC, forwardRef, ForwardRefRenderFunction } from 'react'; import { useIntl } from 'react-intl'; import SettingsModal, { type SettingsModalProps, @@ -28,12 +28,10 @@ export type SettingsProps = { tooltipClassName?: SettingsModalProps['tooltipClassName']; }; -const Settings: FC<SettingsProps> = ({ - className = '', - isActive, - setIsActive, - tooltipClassName = '', -}) => { +const Settings: ForwardRefRenderFunction<HTMLDivElement, SettingsProps> = ( + { className = '', isActive, setIsActive, tooltipClassName = '' }, + ref +) => { const intl = useIntl(); const label = isActive ? intl.formatMessage({ @@ -48,7 +46,7 @@ const Settings: FC<SettingsProps> = ({ }); return ( - <div className={`${sharedStyles.item} ${settingsStyles.item}`}> + <div className={`${sharedStyles.item} ${settingsStyles.item}`} ref={ref}> <Checkbox id="settings-button" name="settings-button" @@ -71,4 +69,4 @@ const Settings: FC<SettingsProps> = ({ ); }; -export default Settings; +export default forwardRef(Settings); diff --git a/src/components/organisms/toolbar/toolbar.tsx b/src/components/organisms/toolbar/toolbar.tsx index 6593055..e4188fe 100644 --- a/src/components/organisms/toolbar/toolbar.tsx +++ b/src/components/organisms/toolbar/toolbar.tsx @@ -1,4 +1,5 @@ -import { FC, useState } from 'react'; +import useClickOutside from '@utils/hooks/use-click-outside'; +import { FC, useRef, useState } from 'react'; import MainNav, { type MainNavProps } from '../toolbar/main-nav'; import Search, { type SearchProps } from '../toolbar/search'; import Settings from '../toolbar/settings'; @@ -22,8 +23,18 @@ export type ToolbarProps = Pick<SearchProps, 'searchPage'> & { */ const Toolbar: FC<ToolbarProps> = ({ className = '', nav, searchPage }) => { const [isNavOpened, setIsNavOpened] = useState<boolean>(false); - const [isSettingsOpened, setIsSettingsOpened] = useState<boolean>(false); const [isSearchOpened, setIsSearchOpened] = useState<boolean>(false); + const [isSettingsOpened, setIsSettingsOpened] = useState<boolean>(false); + const mainNavRef = useRef<HTMLDivElement>(null); + const searchRef = useRef<HTMLDivElement>(null); + const settingsRef = useRef<HTMLDivElement>(null); + + useClickOutside(mainNavRef, () => isNavOpened && setIsNavOpened(false)); + useClickOutside(searchRef, () => isSearchOpened && setIsSearchOpened(false)); + useClickOutside( + settingsRef, + () => isSettingsOpened && setIsSettingsOpened(false) + ); return ( <div className={`${styles.wrapper} ${className}`}> @@ -32,18 +43,21 @@ const Toolbar: FC<ToolbarProps> = ({ className = '', nav, searchPage }) => { isActive={isNavOpened} setIsActive={setIsNavOpened} className={styles.modal} + ref={mainNavRef} /> <Search searchPage={searchPage} isActive={isSearchOpened} setIsActive={setIsSearchOpened} className={`${styles.modal} ${styles['modal--search']}`} + ref={searchRef} /> <Settings isActive={isSettingsOpened} setIsActive={setIsSettingsOpened} className={`${styles.modal} ${styles['modal--settings']}`} tooltipClassName={styles.tooltip} + ref={settingsRef} /> </div> ); |
