diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-04-07 14:18:18 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-04-07 14:30:26 +0200 |
| commit | ff2b6c55cc691f0b62396d9ba481c75fc870cd6a (patch) | |
| tree | 6fc436cc68e9e2abc16d25d8f17c8b067da5e165 /src/components/molecules/modals | |
| parent | 2d5d015d23409b456e36a0370466ee42aa47631f (diff) | |
chore: add a Tooltip component
Diffstat (limited to 'src/components/molecules/modals')
| -rw-r--r-- | src/components/molecules/modals/modal.module.scss | 21 | ||||
| -rw-r--r-- | src/components/molecules/modals/modal.stories.tsx | 56 | ||||
| -rw-r--r-- | src/components/molecules/modals/modal.test.tsx | 9 | ||||
| -rw-r--r-- | src/components/molecules/modals/modal.tsx | 48 | ||||
| -rw-r--r-- | src/components/molecules/modals/tooltip.module.scss | 46 | ||||
| -rw-r--r-- | src/components/molecules/modals/tooltip.stories.tsx | 41 | ||||
| -rw-r--r-- | src/components/molecules/modals/tooltip.test.tsx | 24 | ||||
| -rw-r--r-- | src/components/molecules/modals/tooltip.tsx | 57 |
8 files changed, 302 insertions, 0 deletions
diff --git a/src/components/molecules/modals/modal.module.scss b/src/components/molecules/modals/modal.module.scss new file mode 100644 index 0000000..2fff562 --- /dev/null +++ b/src/components/molecules/modals/modal.module.scss @@ -0,0 +1,21 @@ +@use "@styles/abstracts/functions" as fun; + +.wrapper { + padding: var(--spacing-md); + background: var(--color-bg-secondary); + border: fun.convert-px(4) solid; + border-image: radial-gradient( + ellipse at top, + var(--color-primary-lighter) 20%, + var(--color-primary) 100% + ) + 1; + box-shadow: fun.convert-px(2) fun.convert-px(-2) fun.convert-px(3) + fun.convert-px(-1) var(--color-shadow-dark); +} + +.icon { + --icon-size: #{fun.convert-px(30)}; + + margin-right: var(--spacing-2xs); +} diff --git a/src/components/molecules/modals/modal.stories.tsx b/src/components/molecules/modals/modal.stories.tsx new file mode 100644 index 0000000..b68a24b --- /dev/null +++ b/src/components/molecules/modals/modal.stories.tsx @@ -0,0 +1,56 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import ModalComponent from './modal'; + +export default { + title: 'Molecules/Modals', + component: ModalComponent, + argTypes: { + children: { + control: { + type: 'text', + }, + description: 'The modal body.', + type: { + name: 'string', + required: true, + }, + }, + icon: { + control: { + type: 'select', + }, + description: 'The title icon.', + options: ['', 'cogs', 'search'], + table: { + category: 'Options', + }, + type: { + name: 'string', + required: false, + }, + }, + title: { + control: { + type: 'text', + }, + description: 'The modal title.', + table: { + category: 'Options', + }, + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta<typeof ModalComponent>; + +const Template: ComponentStory<typeof ModalComponent> = (args) => ( + <ModalComponent {...args} /> +); + +export const Modal = Template.bind({}); +Modal.args = { + children: + 'Inventore natus dignissimos aut illum modi asperiores. Et voluptatibus delectus.', +}; diff --git a/src/components/molecules/modals/modal.test.tsx b/src/components/molecules/modals/modal.test.tsx new file mode 100644 index 0000000..14fb224 --- /dev/null +++ b/src/components/molecules/modals/modal.test.tsx @@ -0,0 +1,9 @@ +import { render, screen } from '@test-utils'; +import Modal from './modal'; + +describe('Modal', () => { + it('renders a title', () => { + render(<Modal title="A custom title" />); + expect(screen.getByText('A custom title')).toBeInTheDocument(); + }); +}); diff --git a/src/components/molecules/modals/modal.tsx b/src/components/molecules/modals/modal.tsx new file mode 100644 index 0000000..4dc3b0a --- /dev/null +++ b/src/components/molecules/modals/modal.tsx @@ -0,0 +1,48 @@ +import Heading from '@components/atoms/headings/heading'; +import dynamic from 'next/dynamic'; +import { FC, ReactNode } from 'react'; +import styles from './modal.module.scss'; + +export type Icons = 'cogs' | 'search'; + +export type ModalProps = { + icon?: Icons; + title?: string; +}; + +const CogIcon = dynamic<ReactNode>(() => import('@components/atoms/icons/cog')); +const SearchIcon = dynamic<ReactNode>( + () => import('@components/atoms/icons/magnifying-glass') +); + +/** + * Modal component + * + * Render a modal component with an optional title and icon. + */ +const Modal: FC<ModalProps> = ({ children, icon, title }) => { + const getIcon = (id: Icons) => { + switch (id) { + case 'cogs': + return <CogIcon />; + case 'search': + return <SearchIcon />; + default: + return <></>; + } + }; + + return ( + <div className={styles.wrapper}> + {title && ( + <Heading isFake={true} level={3}> + {icon && <span className={styles.icon}>{getIcon(icon)}</span>} + {title} + </Heading> + )} + {children} + </div> + ); +}; + +export default Modal; diff --git a/src/components/molecules/modals/tooltip.module.scss b/src/components/molecules/modals/tooltip.module.scss new file mode 100644 index 0000000..94aa3dd --- /dev/null +++ b/src/components/molecules/modals/tooltip.module.scss @@ -0,0 +1,46 @@ +@use "@styles/abstracts/functions" as fun; + +.wrapper { + --title-height: #{fun.convert-px(40)}; + + margin-top: calc(var(--title-height) / 2); + padding: calc((var(--title-height) / 2) + var(--spacing-sm)) var(--spacing-sm) + var(--spacing-sm); + position: relative; + background: var(--color-bg); + border: fun.convert-px(2) solid var(--color-primary-dark); + border-radius: fun.convert-px(3); + box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow), + fun.convert-px(2) fun.convert-px(2) fun.convert-px(1) fun.convert-px(1) + var(--color-shadow-light); +} + +.title { + display: flex; + align-items: center; + height: var(--title-height); + padding-right: var(--spacing-xs); + position: absolute; + top: calc(var(--title-height) / -2); + left: var(--spacing-xs); + background: var(--color-bg); + border: fun.convert-px(1) solid var(--color-primary-dark); + box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow); + color: var(--color-primary-darker); + font-size: var(--font-size-sm); + font-variant: small-caps; + font-weight: 500; +} + +.icon { + display: flex; + align-items: center; + height: var(--title-height); + margin-right: var(--spacing-xs); + padding: 0 var(--spacing-2xs); + background: var(--color-primary-dark); + border: fun.convert-px(1) solid var(--color-primary-dark); + box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow); + color: var(--color-fg-inverted); + font-weight: 600; +} diff --git a/src/components/molecules/modals/tooltip.stories.tsx b/src/components/molecules/modals/tooltip.stories.tsx new file mode 100644 index 0000000..a57cf34 --- /dev/null +++ b/src/components/molecules/modals/tooltip.stories.tsx @@ -0,0 +1,41 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import TooltipComponent from './tooltip'; + +export default { + title: 'Molecules/Modals', + component: TooltipComponent, + argTypes: { + content: { + control: { + type: 'text', + }, + description: 'The tooltip body.', + type: { + name: 'string', + required: true, + }, + }, + title: { + control: { + type: 'text', + }, + description: 'The tooltip title', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof TooltipComponent>; + +const Template: ComponentStory<typeof TooltipComponent> = (args) => ( + <TooltipComponent {...args} /> +); + +export const Tooltip = Template.bind({}); +Tooltip.args = { + content: + 'Minima tempora fuga omnis ratione doloribus ut. Totam ea vitae consequatur. Fuga hic ipsum. In non debitis ex assumenda ut dicta. Sit ut maxime eligendi est.', + icon: '?', + title: 'Laborum enim vero', +}; diff --git a/src/components/molecules/modals/tooltip.test.tsx b/src/components/molecules/modals/tooltip.test.tsx new file mode 100644 index 0000000..24f20d8 --- /dev/null +++ b/src/components/molecules/modals/tooltip.test.tsx @@ -0,0 +1,24 @@ +import { render, screen } from '@test-utils'; +import Tooltip from './tooltip'; + +const title = 'Illum eum at'; +const content = + 'Non accusantium ad. Est et impedit iste animi voluptas cum accusamus accusantium. Repellat ut sint pariatur cumque cupiditate. Animi occaecati odio ut debitis ipsam similique. Repudiandae aut earum occaecati consequatur laborum ut nobis iusto. Adipisci laboriosam id.'; +const icon = '?'; + +describe('Tooltip', () => { + it('renders a title', () => { + render(<Tooltip title={title} content={content} icon={icon} />); + expect(screen.getByText(title)).toBeInTheDocument(); + }); + + it('renders an explanation', () => { + render(<Tooltip title={title} content={content} icon={icon} />); + expect(screen.getByText(content)).toBeInTheDocument(); + }); + + it('renders an icon', () => { + render(<Tooltip title={title} content={content} icon={icon} />); + expect(screen.getByText(icon)).toBeInTheDocument(); + }); +}); diff --git a/src/components/molecules/modals/tooltip.tsx b/src/components/molecules/modals/tooltip.tsx new file mode 100644 index 0000000..ceb0b14 --- /dev/null +++ b/src/components/molecules/modals/tooltip.tsx @@ -0,0 +1,57 @@ +import List, { type ListItem } from '@components/atoms/lists/list'; +import { FC, ReactNode } from 'react'; +import styles from './tooltip.module.scss'; + +export type TooltipProps = { + /** + * Set additional classes to the tooltip wrapper. + */ + classes?: string; + /** + * The tooltip body. + */ + content: string | string[]; + /** + * An icon to illustrate tooltip content. + */ + icon: ReactNode; + /** + * The tooltip title. + */ + title: string; +}; + +/** + * Tooltip component + * + * Render a tooltip modal. + */ +const Tooltip: FC<TooltipProps> = ({ classes = '', content, icon, title }) => { + /** + * Format an array of strings to an array of object with id and value. + * + * @param {string[]} array - An array of strings. + * @returns {ListItem[]} The array formatted to be used as list items. + */ + const getListItems = (array: string[]): ListItem[] => { + return array.map((string, index) => { + return { id: `item-${index}`, value: string }; + }); + }; + + return ( + <div className={`${styles.wrapper} ${classes}`}> + <div className={styles.title}> + <span className={styles.icon}>{icon}</span> + {title} + </div> + {Array.isArray(content) ? ( + <List items={getListItems(content)} /> + ) : ( + content + )} + </div> + ); +}; + +export default Tooltip; |
