diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-31 16:00:45 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:15:27 +0100 |
| commit | 3ff4c37a7a2c40340c17f9e6c1754444bce0f839 (patch) | |
| tree | 551ca3df148d46af2bd27995fa98c01378030644 /src/components/molecules/modals/tooltip | |
| parent | 0e52a59917406ad03c174e030c6c1c92ab23449d (diff) | |
refactor(components): rewrite Modal component
* add an optional close button
* add an icon prop
Diffstat (limited to 'src/components/molecules/modals/tooltip')
5 files changed, 231 insertions, 0 deletions
diff --git a/src/components/molecules/modals/tooltip/index.ts b/src/components/molecules/modals/tooltip/index.ts new file mode 100644 index 0000000..ed8326d --- /dev/null +++ b/src/components/molecules/modals/tooltip/index.ts @@ -0,0 +1 @@ +export * from './tooltip'; diff --git a/src/components/molecules/modals/tooltip/tooltip.module.scss b/src/components/molecules/modals/tooltip/tooltip.module.scss new file mode 100644 index 0000000..8e6f877 --- /dev/null +++ b/src/components/molecules/modals/tooltip/tooltip.module.scss @@ -0,0 +1,46 @@ +@use "../../../../styles/abstracts/functions" as fun; +@use "../../../../styles/abstracts/mixins" as mix; +@use "../../../../styles/abstracts/variables" as var; + +.btn { + margin-right: var(--spacing-xs); +} + +.tooltip { + position: absolute; + z-index: 10; + font-size: var(--font-size-sm); + transition: all 0.75s ease-in-out 0s; + + @media screen and (max-height: #{var.get-breakpoint("2xs")}) { + width: calc(97.5vw - var(--spacing-md)); + right: 0; + } + + &--down { + top: calc(100% + var(--spacing-xs)); + transform-origin: top; + } + + &--up { + bottom: calc(100% + var(--spacing-2xs)); + transform-origin: bottom; + } + + &--hidden { + flex: 0 0 0; + opacity: 0; + visibility: hidden; + transform: scale(0); + } + + &--visible { + opacity: 1; + visibility: visible; + transform: scale(1); + } +} + +.heading { + font-size: var(--font-size-sm); +} diff --git a/src/components/molecules/modals/tooltip/tooltip.stories.tsx b/src/components/molecules/modals/tooltip/tooltip.stories.tsx new file mode 100644 index 0000000..0cff339 --- /dev/null +++ b/src/components/molecules/modals/tooltip/tooltip.stories.tsx @@ -0,0 +1,43 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { useBoolean } from '../../../../utils/hooks'; +import { Tooltip } from './tooltip'; + +/** + * Switch - Storybook Meta + */ +export default { + title: 'Molecules/Modals/Tooltip', + component: Tooltip, + args: {}, + argTypes: {}, +} as ComponentMeta<typeof Tooltip>; + +const Template: ComponentStory<typeof Tooltip> = ({ + isOpen, + onToggle: _onToggle, + ...args +}) => { + const { deactivate, state: isOpened, toggle } = useBoolean(isOpen); + + return ( + <div style={{ position: 'relative' }}> + <Tooltip + {...args} + isOpen={isOpened} + onClickOutside={deactivate} + onToggle={toggle} + /> + </div> + ); +}; + +/** + * Tooltip Stories - Example + */ +export const Example = Template.bind({}); +Example.args = { + children: + 'Inventore natus dignissimos aut illum modi asperiores. Et voluptatibus delectus.', + heading: 'A title', + isOpen: false, +}; diff --git a/src/components/molecules/modals/tooltip/tooltip.test.tsx b/src/components/molecules/modals/tooltip/tooltip.test.tsx new file mode 100644 index 0000000..1596670 --- /dev/null +++ b/src/components/molecules/modals/tooltip/tooltip.test.tsx @@ -0,0 +1,42 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '../../../../../tests/utils'; +import { Tooltip } from './tooltip'; + +const title = 'A custom title'; +const children = + 'Labore ullam delectus sit modi quam dolores. Ratione id sint aliquid facilis ipsum. Unde necessitatibus provident minus.'; + +describe('Tooltip', () => { + it('renders a title and a body', () => { + render(<Tooltip heading={title}>{children}</Tooltip>); + + expect(rtlScreen.getByText(title)).toBeInTheDocument(); + expect(rtlScreen.getByText(children)).toBeInTheDocument(); + }); + + it('can render a hidden modal', () => { + render( + <Tooltip heading={title} isOpen={false}> + {children} + </Tooltip> + ); + + // Neither toBeVisible or toHaveStyle are working. + //expect(rtlScreen.getByText(children)).not.toBeVisible(); + //expect(rtlScreen.getByText(children)).toHaveStyle({ visibility: 'hidden' }); + expect(rtlScreen.getByText(children)).toHaveClass('tooltip--hidden'); + }); + + it('can render a visible modal', () => { + render( + <Tooltip heading={title} isOpen> + {children} + </Tooltip> + ); + + expect(rtlScreen.getByText(children)).toBeVisible(); + expect(rtlScreen.getByText(children)).toHaveStyle({ + visibility: 'visible', + }); + }); +}); diff --git a/src/components/molecules/modals/tooltip/tooltip.tsx b/src/components/molecules/modals/tooltip/tooltip.tsx new file mode 100644 index 0000000..b3a3f5a --- /dev/null +++ b/src/components/molecules/modals/tooltip/tooltip.tsx @@ -0,0 +1,99 @@ +import { type FC, type MouseEventHandler, useRef } from 'react'; +import { useIntl } from 'react-intl'; +import { useOnClickOutside } from '../../../../utils/hooks'; +import { Heading, Icon } from '../../../atoms'; +import { HelpButton } from '../../buttons'; +import { Modal, type ModalProps } from '../modal'; +import styles from './tooltip.module.scss'; + +export type TooltipProps = Omit<ModalProps, 'heading'> & { + /** + * The tooltip direction when opening. + * + * @default "downwards" + */ + direction?: 'downwards' | 'upwards'; + /** + * The tooltip heading. + */ + heading: string; + /** + * Should the tooltip be opened? + * + * @default false + */ + isOpen?: boolean; + /** + * A callback function to trigger when clicking outside the modal. + */ + onClickOutside?: () => void; + /** + * An event handler when clicking on the help button. + */ + onToggle?: MouseEventHandler<HTMLButtonElement>; +}; + +/** + * Tooltip component + * + * Render a button and a modal. Note: you should add a CSS rule + * `position: relative;` on the consumer. + */ +export const Tooltip: FC<TooltipProps> = ({ + children, + className = '', + direction = 'downwards', + heading, + isOpen, + onClickOutside, + onToggle, + ...props +}) => { + const intl = useIntl(); + const helpLabel = intl.formatMessage({ + defaultMessage: 'Show help', + description: 'Tooltip: show help label', + id: '1Xgg7+', + }); + const directionModifier = + direction === 'upwards' ? 'tooltip--up' : 'tooltip--down'; + const visibilityModifier = isOpen ? 'tooltip--visible' : 'tooltip--hidden'; + const tooltipClass = `${styles.tooltip} ${styles[directionModifier]} ${styles[visibilityModifier]} ${className}`; + const btnRef = useRef<HTMLButtonElement>(null); + + const closeModal = (target: Node) => { + if (!onClickOutside) return; + + if (btnRef.current && !btnRef.current.contains(target)) { + onClickOutside(); + } + }; + + const modalRef = useOnClickOutside<HTMLDivElement>(closeModal); + + return ( + <> + <Modal + {...props} + className={tooltipClass} + heading={ + <Heading className={styles.heading} isFake level={6}> + {heading} + </Heading> + } + icon={<Icon aria-hidden shape="help" size="sm" />} + kind="secondary" + ref={modalRef} + > + {children} + </Modal> + <HelpButton + className={styles.btn} + isPressed={isOpen} + label={helpLabel} + onClick={onToggle} + ref={btnRef} + /> + </> + ); +}; |
