From 3ff4c37a7a2c40340c17f9e6c1754444bce0f839 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Tue, 31 Oct 2023 16:00:45 +0100 Subject: refactor(components): rewrite Modal component * add an optional close button * add an icon prop --- src/components/atoms/index.ts | 1 - src/components/atoms/modal/index.ts | 1 - src/components/atoms/modal/modal.module.scss | 66 ------- src/components/atoms/modal/modal.stories.tsx | 59 ------- src/components/atoms/modal/modal.test.tsx | 26 --- src/components/atoms/modal/modal.tsx | 49 ------ src/components/molecules/forms/switch/switch.tsx | 2 +- src/components/molecules/index.ts | 2 +- src/components/molecules/modals/index.ts | 2 + src/components/molecules/modals/modal/index.ts | 1 + .../molecules/modals/modal/modal.module.scss | 159 +++++++++++++++++ .../molecules/modals/modal/modal.stories.tsx | 191 +++++++++++++++++++++ .../molecules/modals/modal/modal.test.tsx | 48 ++++++ src/components/molecules/modals/modal/modal.tsx | 103 +++++++++++ src/components/molecules/modals/tooltip/index.ts | 1 + .../molecules/modals/tooltip/tooltip.module.scss | 46 +++++ .../molecules/modals/tooltip/tooltip.stories.tsx | 43 +++++ .../molecules/modals/tooltip/tooltip.test.tsx | 42 +++++ .../molecules/modals/tooltip/tooltip.tsx | 99 +++++++++++ src/components/molecules/tooltip/index.ts | 1 - .../molecules/tooltip/tooltip.module.scss | 66 ------- .../molecules/tooltip/tooltip.stories.tsx | 42 ----- src/components/molecules/tooltip/tooltip.test.tsx | 40 ----- src/components/molecules/tooltip/tooltip.tsx | 98 ----------- src/components/organisms/modals/search-modal.tsx | 5 +- .../organisms/modals/settings-modal.module.scss | 2 +- src/components/organisms/modals/settings-modal.tsx | 5 +- .../organisms/toolbar/search.module.scss | 8 + .../organisms/toolbar/settings.module.scss | 12 +- src/components/organisms/toolbar/settings.tsx | 13 +- .../organisms/toolbar/toolbar.module.scss | 2 +- 31 files changed, 769 insertions(+), 466 deletions(-) delete mode 100644 src/components/atoms/modal/index.ts delete mode 100644 src/components/atoms/modal/modal.module.scss delete mode 100644 src/components/atoms/modal/modal.stories.tsx delete mode 100644 src/components/atoms/modal/modal.test.tsx delete mode 100644 src/components/atoms/modal/modal.tsx create mode 100644 src/components/molecules/modals/index.ts create mode 100644 src/components/molecules/modals/modal/index.ts create mode 100644 src/components/molecules/modals/modal/modal.module.scss create mode 100644 src/components/molecules/modals/modal/modal.stories.tsx create mode 100644 src/components/molecules/modals/modal/modal.test.tsx create mode 100644 src/components/molecules/modals/modal/modal.tsx create mode 100644 src/components/molecules/modals/tooltip/index.ts create mode 100644 src/components/molecules/modals/tooltip/tooltip.module.scss create mode 100644 src/components/molecules/modals/tooltip/tooltip.stories.tsx create mode 100644 src/components/molecules/modals/tooltip/tooltip.test.tsx create mode 100644 src/components/molecules/modals/tooltip/tooltip.tsx delete mode 100644 src/components/molecules/tooltip/index.ts delete mode 100644 src/components/molecules/tooltip/tooltip.module.scss delete mode 100644 src/components/molecules/tooltip/tooltip.stories.tsx delete mode 100644 src/components/molecules/tooltip/tooltip.test.tsx delete mode 100644 src/components/molecules/tooltip/tooltip.tsx (limited to 'src') diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts index 31beda9..9791e66 100644 --- a/src/components/atoms/index.ts +++ b/src/components/atoms/index.ts @@ -8,7 +8,6 @@ export * from './layout'; export * from './links'; export * from './lists'; export * from './loaders'; -export * from './modal'; export * from './notice'; export * from './sidebar'; export * from './visually-hidden'; diff --git a/src/components/atoms/modal/index.ts b/src/components/atoms/modal/index.ts deleted file mode 100644 index 133aa74..0000000 --- a/src/components/atoms/modal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './modal'; diff --git a/src/components/atoms/modal/modal.module.scss b/src/components/atoms/modal/modal.module.scss deleted file mode 100644 index 6650235..0000000 --- a/src/components/atoms/modal/modal.module.scss +++ /dev/null @@ -1,66 +0,0 @@ -@use "../../../styles/abstracts/functions" as fun; -@use "../../../styles/abstracts/mixins" as mix; - -.modal { - position: relative; - box-shadow: - fun.convert-px(0.2) fun.convert-px(0.2) fun.convert-px(0.3) 0 - var(--color-shadow), - fun.convert-px(1.5) fun.convert-px(1.5) fun.convert-px(2.5) - fun.convert-px(-0.3) var(--color-shadow), - fun.convert-px(4.7) fun.convert-px(4.7) fun.convert-px(8) fun.convert-px(-1) - var(--color-shadow); - - &--primary { - padding: clamp(var(--spacing-xs), 2.5vw, var(--spacing-md)); - background: var(--color-bg-secondary); - border: fun.convert-px(3) solid; - border-image: radial-gradient( - ellipse at top, - var(--color-primary-lighter) 20%, - var(--color-primary) 100% - ) - 1; - - .title { - margin-bottom: var(--spacing-2xs); - } - - @include mix.media("screen") { - @include mix.dimensions(null, "sm") { - border-left: none; - border-right: none; - } - } - } - - &--secondary { - padding: clamp(var(--spacing-xs), 2.2vw, var(--spacing-sm)); - background: var(--color-bg); - border: fun.convert-px(2) solid var(--color-primary-dark); - border-radius: fun.convert-px(3); - - .title { - padding-inline: 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-variant: small-caps; - - > * { - margin-block: 0; - } - } - } - - &--secondary#{&}--has-title { - --title-height: #{fun.convert-px(40)}; - - .title { - width: fit-content; - height: var(--title-height); - margin: calc(var(--title-height) * -1) auto var(--spacing-xs); - } - } -} diff --git a/src/components/atoms/modal/modal.stories.tsx b/src/components/atoms/modal/modal.stories.tsx deleted file mode 100644 index 0490a8f..0000000 --- a/src/components/atoms/modal/modal.stories.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Heading } from '../heading'; -import { Modal } from './modal'; - -/** - * Switch - Storybook Meta - */ -export default { - title: 'Atoms/Modals', - component: Modal, - args: {}, - argTypes: {}, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ; - -/** - * Modal Stories - Primary - */ -export const Primary = Template.bind({}); -Primary.args = { - children: - 'Inventore natus dignissimos aut illum modi asperiores. Et voluptatibus delectus.', -}; - -/** - * Modal Stories - Primary With Heading - */ -export const PrimaryWithHeading = Template.bind({}); -PrimaryWithHeading.args = { - children: - 'Inventore natus dignissimos aut illum modi asperiores. Et voluptatibus delectus.', - heading: Aut provident eum, -}; - -/** - * Modal Stories - Secondary - */ -export const Secondary = Template.bind({}); -Secondary.args = { - children: - 'Inventore natus dignissimos aut illum modi asperiores. Et voluptatibus delectus.', - kind: 'secondary', -}; - -/** - * Modal Stories - Secondary with heading - */ -export const SecondaryWithHeading = Template.bind({}); -SecondaryWithHeading.args = { - children: - 'Inventore natus dignissimos aut illum modi asperiores. Et voluptatibus delectus.', - heading: ( - - Aut provident eum - - ), - kind: 'secondary', -}; diff --git a/src/components/atoms/modal/modal.test.tsx b/src/components/atoms/modal/modal.test.tsx deleted file mode 100644 index dfa4a88..0000000 --- a/src/components/atoms/modal/modal.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../tests/utils'; -import { Heading } from '../heading'; -import { Modal } from './modal'; - -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('Modal', () => { - it('renders a title', () => { - const level = 2; - - render( - {title}}> - {children} - - ); - expect(rtlScreen.getByRole('heading', { level })).toHaveTextContent(title); - }); - - it('renders the modal body', () => { - render({children}); - expect(rtlScreen.getByText(children)).toBeInTheDocument(); - }); -}); diff --git a/src/components/atoms/modal/modal.tsx b/src/components/atoms/modal/modal.tsx deleted file mode 100644 index 6f5506f..0000000 --- a/src/components/atoms/modal/modal.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { - type ForwardRefRenderFunction, - type HTMLAttributes, - type ReactElement, - type ReactNode, - forwardRef, -} from 'react'; -import type { HeadingProps } from '../heading'; -import styles from './modal.module.scss'; - -export type ModalProps = HTMLAttributes & { - /** - * The modal body. - */ - children: ReactNode; - /** - * The modal title. - */ - heading?: ReactElement; - /** - * The modal kind. - * - * @default 'primary' - */ - kind?: 'primary' | 'secondary'; -}; - -const ModalWithRef: ForwardRefRenderFunction = ( - { children, className = '', heading, kind = 'primary', ...props }, - ref -) => { - const headingModifier = heading ? 'modal--has-title' : ''; - const kindModifier = `modal--${kind}`; - const modalClass = `${styles.modal} ${styles[headingModifier]} ${styles[kindModifier]} ${className}`; - - return ( -
- {heading ?
{heading}
: null} - {children} -
- ); -}; - -/** - * Modal component - * - * Render a modal component. - */ -export const Modal = forwardRef(ModalWithRef); diff --git a/src/components/molecules/forms/switch/switch.tsx b/src/components/molecules/forms/switch/switch.tsx index ad3e514..c6c1c69 100644 --- a/src/components/molecules/forms/switch/switch.tsx +++ b/src/components/molecules/forms/switch/switch.tsx @@ -14,7 +14,7 @@ import { Label, Radio, } from '../../../atoms'; -import type { TooltipProps } from '../../tooltip'; +import type { TooltipProps } from '../../modals'; import styles from './switch.module.scss'; type SwitchItemProps = Omit & diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts index c2c94d0..04c669f 100644 --- a/src/components/molecules/index.ts +++ b/src/components/molecules/index.ts @@ -10,5 +10,5 @@ export * from './grid'; export * from './images'; export * from './layout'; export * from './meta-list'; +export * from './modals'; export * from './nav'; -export * from './tooltip'; diff --git a/src/components/molecules/modals/index.ts b/src/components/molecules/modals/index.ts new file mode 100644 index 0000000..595be13 --- /dev/null +++ b/src/components/molecules/modals/index.ts @@ -0,0 +1,2 @@ +export * from './modal'; +export * from './tooltip'; diff --git a/src/components/molecules/modals/modal/index.ts b/src/components/molecules/modals/modal/index.ts new file mode 100644 index 0000000..133aa74 --- /dev/null +++ b/src/components/molecules/modals/modal/index.ts @@ -0,0 +1 @@ +export * from './modal'; diff --git a/src/components/molecules/modals/modal/modal.module.scss b/src/components/molecules/modals/modal/modal.module.scss new file mode 100644 index 0000000..81047f3 --- /dev/null +++ b/src/components/molecules/modals/modal/modal.module.scss @@ -0,0 +1,159 @@ +@use "../../../../styles/abstracts/functions" as fun; +@use "../../../../styles/abstracts/mixins" as mix; + +.modal { + --btn-border-size: #{fun.convert-px(1)}; + --header-size: #{fun.convert-px(44)}; + --padding: clamp(var(--spacing-sm), 2.5vw, var(--spacing-md)); + + max-width: 100%; + padding: var(--padding); + position: relative; + box-shadow: + fun.convert-px(0.2) fun.convert-px(0.2) fun.convert-px(0.3) 0 + var(--color-shadow), + fun.convert-px(1.5) fun.convert-px(1.5) fun.convert-px(2.5) + fun.convert-px(-0.3) var(--color-shadow), + fun.convert-px(4.7) fun.convert-px(4.7) fun.convert-px(8) fun.convert-px(-1) + var(--color-shadow); + + &--primary { + background: var(--color-bg-secondary); + border: fun.convert-px(3) solid; + border-image: radial-gradient( + ellipse at top, + var(--color-primary-lighter) 20%, + var(--color-primary) 100% + ) + 1; + } + + &--secondary { + background: var(--color-bg); + border: fun.convert-px(2) solid var(--color-primary-dark); + border-radius: fun.convert-px(3); + } + + &--primary#{&}--has-btn { + --btn-offset-y: #{fun.convert-px(-15)}; + + margin-top: calc(var(--btn-offset-y) * -1); + } + + &--secondary#{&}--has-header { + margin-top: calc(var(--header-size) / 2); + } +} + +.header { + display: flex; + flex-flow: row nowrap; +} + +:where(.header) { + > .icon, + > .title { + display: flex; + flex-flow: row wrap; + align-items: center; + } +} + +.btn { + width: var(--header-size); + min-height: var(--header-size); + background: var(--color-bg); + border: var(--btn-border-size) solid var(--color-primary); + box-shadow: + fun.convert-px(0.2) fun.convert-px(0.2) fun.convert-px(0.3) 0 + var(--color-shadow), + fun.convert-px(1.5) fun.convert-px(1.5) fun.convert-px(2.5) + fun.convert-px(-0.3) var(--color-shadow); +} + +:where(.modal--primary) { + > .header { + align-items: center; + gap: var(--spacing-2xs); + margin-bottom: var(--spacing-2xs); + } + + :where(.header) > .btn { + position: absolute; + top: var(--btn-offset-y); + right: fun.convert-px(-10); + border-image: radial-gradient( + ellipse at top, + var(--color-primary-lighter) 20%, + var(--color-primary) 100% + ) + 1; + } +} + +:where(.modal--secondary) { + :where(.header) { + > .icon, + > .title { + min-height: var(--header-size); + border: fun.convert-px(1) solid var(--color-primary-dark); + box-shadow: + fun.convert-px(0.2) fun.convert-px(0.2) fun.convert-px(0.3) 0 + var(--color-shadow), + fun.convert-px(1.5) fun.convert-px(1.5) fun.convert-px(2.5) + fun.convert-px(-0.3) var(--color-shadow); + } + + > .icon, + > .btn { + flex: 0 0 var(--header-size); + } + + > .icon { + display: flex; + place-content: center; + background: var(--color-primary); + color: var(--color-fg-inverted); + + path { + fill: var(--color-fg-inverted); + stroke: var(--color-fg-inverted); + } + } + + > .title { + min-height: var(--header-size); + padding-inline: var(--spacing-xs); + background: var(--color-bg); + color: var(--color-primary-darker); + font-variant: small-caps; + + > * { + margin-block: 0; + } + } + } + + > .header { + justify-content: center; + width: 100%; + margin: calc(var(--header-size) / -2 - var(--padding)) auto + var(--spacing-sm); + + > * + * { + margin-inline-start: calc(var(--btn-border-size) * -1); + } + } +} + +.btn:where(:hover, :focus) { + .icon { + transform: scale(1.2); + } +} + +.btn:where(:active) { + .icon { + transform: scale(0.9); + } +} diff --git a/src/components/molecules/modals/modal/modal.stories.tsx b/src/components/molecules/modals/modal/modal.stories.tsx new file mode 100644 index 0000000..744d21f --- /dev/null +++ b/src/components/molecules/modals/modal/modal.stories.tsx @@ -0,0 +1,191 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Heading, Icon } from '../../../atoms'; +import { Modal } from './modal'; + +/** + * Modals - Storybook Meta + */ +export default { + title: 'Molecules/Modals/Modal', + component: Modal, + args: {}, + argTypes: {}, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; + +/** + * Modal Stories - Primary + */ +export const Primary = Template.bind({}); +Primary.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', +}; + +/** + * Modal Stories - Primary with close button + */ +export const PrimaryWithCloseBtn = Template.bind({}); +PrimaryWithCloseBtn.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + closeBtnLabel: 'Close the modal', +}; + +/** + * Modal Stories - Primary with icon + */ +export const PrimaryWithIcon = Template.bind({}); +PrimaryWithIcon.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + icon: , +}; + +/** + * Modal Stories - Primary with heading + */ +export const PrimaryWithHeading = Template.bind({}); +PrimaryWithHeading.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + heading: Aut provident eum, +}; + +/** + * Modal Stories - Primary with icon and heading + */ +export const PrimaryWithIconAndHeading = Template.bind({}); +PrimaryWithIconAndHeading.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + heading: Aut provident eum, + icon: , +}; + +/** + * Modal Stories - Primary with close button and heading + */ +export const PrimaryWithCloseBtnAndHeading = Template.bind({}); +PrimaryWithCloseBtnAndHeading.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + closeBtnLabel: 'Close the modal', + heading: Aut provident eum, +}; + +/** + * Modal Stories - Primary with close button and icon + */ +export const PrimaryWithCloseBtnAndIcon = Template.bind({}); +PrimaryWithCloseBtnAndIcon.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + closeBtnLabel: 'Close the modal', + icon: , +}; + +/** + * Modal Stories - Primary with close button, icon and heading + */ +export const PrimaryWithCloseBtnIconAndHeading = Template.bind({}); +PrimaryWithCloseBtnIconAndHeading.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + closeBtnLabel: 'Close the modal', + heading: Aut provident eum, + icon: , +}; + +/** + * Modal Stories - Secondary + */ +export const Secondary = Template.bind({}); +Secondary.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + kind: 'secondary', +}; + +/** + * Modal Stories - Secondary with close button + */ +export const SecondaryWithCloseBtn = Template.bind({}); +SecondaryWithCloseBtn.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + kind: 'secondary', + closeBtnLabel: 'Close the modal', +}; + +/** + * Modal Stories - Secondary with heading + */ +export const SecondaryWithHeading = Template.bind({}); +SecondaryWithHeading.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + heading: Aut provident eum, + kind: 'secondary', +}; + +/** + * Modal Stories - Secondary with icon + */ +export const SecondaryWithIcon = Template.bind({}); +SecondaryWithIcon.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + kind: 'secondary', + icon: , +}; + +/** + * Modal Stories - Secondary with close button and heading + */ +export const SecondaryWithCloseBtnAndHeading = Template.bind({}); +SecondaryWithCloseBtnAndHeading.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + heading: Aut provident eum, + kind: 'secondary', + closeBtnLabel: 'Close the modal', +}; + +/** + * Modal Stories - Secondary with close button and icon + */ +export const SecondaryWithCloseBtnAndIcon = Template.bind({}); +SecondaryWithCloseBtnAndIcon.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + closeBtnLabel: 'Close the modal', + icon: , + kind: 'secondary', +}; + +/** + * Modal Stories - Secondary with icon and heading + */ +export const SecondaryWithIconAndHeading = Template.bind({}); +SecondaryWithIconAndHeading.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + heading: Aut provident eum, + icon: , + kind: 'secondary', +}; + +/** + * Modal Stories - Secondary with close button, icon and heading + */ +export const SecondaryWithCloseBtnIconAndHeading = Template.bind({}); +SecondaryWithCloseBtnIconAndHeading.args = { + children: + 'Sed atque molestiae voluptatem possimus nisi recusandae qui assumenda. Quia rerum sed. Et autem impedit ut nam impedit. Quam ex facere pariatur est. Voluptatem hic beatae asperiores suscipit. Accusamus dolorum fugit placeat alias vel tenetur. Expedita fuga quos ipsum cum ea est expedita quia eaque.', + heading: Aut provident eum, + closeBtnLabel: 'Close the modal', + icon: , + kind: 'secondary', +}; diff --git a/src/components/molecules/modals/modal/modal.test.tsx b/src/components/molecules/modals/modal/modal.test.tsx new file mode 100644 index 0000000..82b7487 --- /dev/null +++ b/src/components/molecules/modals/modal/modal.test.tsx @@ -0,0 +1,48 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Heading, Icon } from '../../../atoms'; +import { Modal } from './modal'; + +const children = + 'Labore ullam delectus sit modi quam dolores. Ratione id sint aliquid facilis ipsum. Unde necessitatibus provident minus.'; + +describe('Modal', () => { + it('renders the modal contents', () => { + render({children}); + + expect(rtlScreen.getByText(children)).toBeInTheDocument(); + }); + + it('can render a heading', () => { + const heading = 'A custom heading'; + const level = 2; + + render( + {heading}}> + {children} + + ); + + expect(rtlScreen.getByRole('heading', { level })).toHaveTextContent( + heading + ); + }); + + it('can render an icon', () => { + const label = 'maxime ut eius'; + + render( + }>{children} + ); + + expect(rtlScreen.getByLabelText(label)).toBeInTheDocument(); + }); + + it('can render a close button', () => { + const btn = 'consequatur'; + + render({children}); + + expect(rtlScreen.getByRole('button', { name: btn })).toBeInTheDocument(); + }); +}); diff --git a/src/components/molecules/modals/modal/modal.tsx b/src/components/molecules/modals/modal/modal.tsx new file mode 100644 index 0000000..ed55488 --- /dev/null +++ b/src/components/molecules/modals/modal/modal.tsx @@ -0,0 +1,103 @@ +import { + type ForwardRefRenderFunction, + type HTMLAttributes, + type ReactNode, + forwardRef, +} from 'react'; +import { Button, Icon } from '../../../atoms'; +import styles from './modal.module.scss'; + +export type ModalProps = HTMLAttributes & { + /** + * The modal body. + */ + children: ReactNode; + /** + * The close button label. + */ + closeBtnLabel?: string; + /** + * The modal title. + */ + heading?: ReactNode; + /** + * Define an icon to illustrate the modal. + */ + icon?: ReactNode; + /** + * The modal kind. + * + * @default 'primary' + */ + kind?: 'primary' | 'secondary'; + /** + * A callback function to handle close button action. + */ + onClose?: () => void; +}; + +const ModalWithRef: ForwardRefRenderFunction = ( + { + children, + className = '', + closeBtnLabel, + heading, + icon, + kind = 'primary', + onClose, + ...props + }, + ref +) => { + const hasHeader = !!heading || !!icon || !!closeBtnLabel; + const modalClass = [ + styles.modal, + styles[hasHeader ? 'modal--has-header' : ''], + styles[closeBtnLabel ? 'modal--has-btn' : ''], + styles[`modal--${kind}`], + className, + ].join(' '); + + return ( +
+ {hasHeader ? ( +
+ {icon ? ( +
+ {icon} +
+ ) : null} + {heading ?
{heading}
: null} + {closeBtnLabel ? ( + + ) : null} +
+ ) : null} + {children} +
+ ); +}; + +/** + * Modal component + * + * Render a modal component. + */ +export const Modal = forwardRef(ModalWithRef); 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; + +const Template: ComponentStory = ({ + isOpen, + onToggle: _onToggle, + ...args +}) => { + const { deactivate, state: isOpened, toggle } = useBoolean(isOpen); + + return ( +
+ +
+ ); +}; + +/** + * 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({children}); + + expect(rtlScreen.getByText(title)).toBeInTheDocument(); + expect(rtlScreen.getByText(children)).toBeInTheDocument(); + }); + + it('can render a hidden modal', () => { + render( + + {children} + + ); + + // 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( + + {children} + + ); + + 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 & { + /** + * 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; +}; + +/** + * Tooltip component + * + * Render a button and a modal. Note: you should add a CSS rule + * `position: relative;` on the consumer. + */ +export const Tooltip: FC = ({ + 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(null); + + const closeModal = (target: Node) => { + if (!onClickOutside) return; + + if (btnRef.current && !btnRef.current.contains(target)) { + onClickOutside(); + } + }; + + const modalRef = useOnClickOutside(closeModal); + + return ( + <> + + {heading} + + } + icon={} + kind="secondary" + ref={modalRef} + > + {children} + + + + ); +}; diff --git a/src/components/molecules/tooltip/index.ts b/src/components/molecules/tooltip/index.ts deleted file mode 100644 index ed8326d..0000000 --- a/src/components/molecules/tooltip/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './tooltip'; diff --git a/src/components/molecules/tooltip/tooltip.module.scss b/src/components/molecules/tooltip/tooltip.module.scss deleted file mode 100644 index 557d9c7..0000000 --- a/src/components/molecules/tooltip/tooltip.module.scss +++ /dev/null @@ -1,66 +0,0 @@ -@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 { - display: flex; - flex-flow: row nowrap; - align-items: center; - height: 100%; - margin-left: calc(var(--spacing-xs) * -1.1); - font-size: var(--font-size-sm); -} - -.icon { - align-self: stretch; - margin-right: var(--spacing-xs); - 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); - - :global { - path { - fill: var(--color-fg-inverted); - margin-inline: var(--spacing-2xs); - } - } -} diff --git a/src/components/molecules/tooltip/tooltip.stories.tsx b/src/components/molecules/tooltip/tooltip.stories.tsx deleted file mode 100644 index 8a22a06..0000000 --- a/src/components/molecules/tooltip/tooltip.stories.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Tooltip } from './tooltip'; -import { useState } from 'react'; - -/** - * Switch - Storybook Meta - */ -export default { - title: 'Molecules/Tooltip', - component: Tooltip, - args: {}, - argTypes: {}, -} as ComponentMeta; - -const Template: ComponentStory = ({ - isOpen, - onToggle: _onToggle, - ...args -}) => { - const [isOpened, setIsOpened] = useState(isOpen); - - const toggle = () => { - setIsOpened((prev) => !prev); - }; - - return ( -
- -
- ); -}; - -/** - * 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/tooltip/tooltip.test.tsx b/src/components/molecules/tooltip/tooltip.test.tsx deleted file mode 100644 index 25a1614..0000000 --- a/src/components/molecules/tooltip/tooltip.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen } 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({children}); - - expect(screen.getByText(title)).toBeInTheDocument(); - expect(screen.getByText(children)).toBeInTheDocument(); - }); - - it('can render a hidden modal', () => { - render( - - {children} - - ); - - // Neither toBeVisible or toHaveStyle are working. - //expect(screen.getByText(children)).not.toBeVisible(); - //expect(screen.getByText(children)).toHaveStyle({ visibility: 'hidden' }); - expect(screen.getByText(children)).toHaveClass('tooltip--hidden'); - }); - - it('can render a visible modal', () => { - render( - - {children} - - ); - - expect(screen.getByText(children)).toBeVisible(); - expect(screen.getByText(children)).toHaveStyle({ visibility: 'visible' }); - }); -}); diff --git a/src/components/molecules/tooltip/tooltip.tsx b/src/components/molecules/tooltip/tooltip.tsx deleted file mode 100644 index 1f54d68..0000000 --- a/src/components/molecules/tooltip/tooltip.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { type FC, type MouseEventHandler, useRef } from 'react'; -import { useIntl } from 'react-intl'; -import { useOnClickOutside } from '../../../utils/hooks'; -import { Heading, Icon, Modal, type ModalProps } from '../../atoms'; -import { HelpButton } from '../buttons'; -import styles from './tooltip.module.scss'; - -export type TooltipProps = Omit & { - /** - * 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; -}; - -/** - * Tooltip component - * - * Render a button and a modal. Note: you should add a CSS rule - * `position: relative;` on the consumer. - */ -export const Tooltip: FC = ({ - 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(null); - - const closeModal = (target: Node) => { - if (!onClickOutside) return; - - if (btnRef.current && !btnRef.current.contains(target)) { - onClickOutside(); - } - }; - - const modalRef = useOnClickOutside(closeModal); - - return ( - <> - - - {heading} - - } - kind="secondary" - ref={modalRef} - > - {children} - - - - ); -}; diff --git a/src/components/organisms/modals/search-modal.tsx b/src/components/organisms/modals/search-modal.tsx index 7d772df..be9d489 100644 --- a/src/components/organisms/modals/search-modal.tsx +++ b/src/components/organisms/modals/search-modal.tsx @@ -1,6 +1,7 @@ -import { forwardRef, ForwardRefRenderFunction } from 'react'; +import { forwardRef, type ForwardRefRenderFunction } from 'react'; import { useIntl } from 'react-intl'; -import { Heading, Modal, type ModalProps } from '../../atoms'; +import { Heading } from '../../atoms'; +import { Modal, type ModalProps } from '../../molecules'; import { SearchForm, type SearchFormProps } from '../forms'; import styles from './search-modal.module.scss'; diff --git a/src/components/organisms/modals/settings-modal.module.scss b/src/components/organisms/modals/settings-modal.module.scss index 47af656..68bce98 100644 --- a/src/components/organisms/modals/settings-modal.module.scss +++ b/src/components/organisms/modals/settings-modal.module.scss @@ -5,7 +5,7 @@ width: 100%; @media screen and (max-height: #{var.get-breakpoint("2xs")}) and (max-width: #{var.get-breakpoint("sm")}) { - --first-col-width: #{fun.convert-px(160)}; + --first-col-width: #{fun.convert-px(140)}; --col-gap: var(--spacing-xl); display: grid; diff --git a/src/components/organisms/modals/settings-modal.tsx b/src/components/organisms/modals/settings-modal.tsx index 94d69e2..36c5977 100644 --- a/src/components/organisms/modals/settings-modal.tsx +++ b/src/components/organisms/modals/settings-modal.tsx @@ -1,6 +1,7 @@ import { useCallback, type FC, type FormEvent } from 'react'; import { useIntl } from 'react-intl'; -import { Heading, Icon, Modal, type ModalProps } from '../../atoms'; +import { Heading, Icon } from '../../atoms'; +import { Modal, type ModalProps } from '../../molecules'; import { SettingsForm } from '../forms'; import styles from './settings-modal.module.scss'; @@ -31,9 +32,9 @@ export const SettingsModal: FC = ({ className = '' }) => { return ( } heading={ - {title} } diff --git a/src/components/organisms/toolbar/search.module.scss b/src/components/organisms/toolbar/search.module.scss index c310594..0dc36de 100644 --- a/src/components/organisms/toolbar/search.module.scss +++ b/src/components/organisms/toolbar/search.module.scss @@ -1,3 +1,11 @@ +@use "../../../styles/abstracts/mixins" as mix; + .modal { padding-bottom: var(--spacing-md); + + @include mix.media("screen") { + @include mix.dimensions(null, "sm") { + border-inline: 0; + } + } } diff --git a/src/components/organisms/toolbar/settings.module.scss b/src/components/organisms/toolbar/settings.module.scss index 59c44f8..2c473b7 100644 --- a/src/components/organisms/toolbar/settings.module.scss +++ b/src/components/organisms/toolbar/settings.module.scss @@ -1,5 +1,9 @@ -.item .tooltip { - top: unset; - bottom: calc(100% + var(--spacing-2xs)); - transform-origin: bottom center; +@use "../../../styles/abstracts/mixins" as mix; + +.modal { + @include mix.media("screen") { + @include mix.dimensions(null, "sm") { + border-inline: 0; + } + } } diff --git a/src/components/organisms/toolbar/settings.tsx b/src/components/organisms/toolbar/settings.tsx index 1b68db8..a0aad8c 100644 --- a/src/components/organisms/toolbar/settings.tsx +++ b/src/components/organisms/toolbar/settings.tsx @@ -3,7 +3,8 @@ import { useIntl } from 'react-intl'; import { BooleanField, type BooleanFieldProps, Icon } from '../../atoms'; import { FlippingLabel } from '../../molecules'; import { SettingsModal, type SettingsModalProps } from '../modals'; -import styles from './toolbar-items.module.scss'; +import styles from './settings.module.scss'; +import sharedStyles from './toolbar-items.module.scss'; export type SettingsProps = SettingsModalProps & { /** @@ -34,9 +35,9 @@ const SettingsWithRef: ForwardRefRenderFunction< }); return ( -
+
} isActive={isActive} label={label} /> - +
); }; diff --git a/src/components/organisms/toolbar/toolbar.module.scss b/src/components/organisms/toolbar/toolbar.module.scss index ac7c892..6c138a3 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(400); + min-width: fun.convert-px(420); } } } -- cgit v1.2.3