aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/molecules
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-10-07 18:44:14 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:14:41 +0100
commitd75b9a1e150ab211c1052fb49bede9bd16320aca (patch)
treee5bb221d2b8dc83151697fe646e9194f921b5807 /src/components/molecules
parent12a03a9a72f7895d571dbaeeb245d92aa277a610 (diff)
feat(components): add a generic Flip component
The flipping animation is used at several places so it makes sense to use a single component to handle the animation. It will avoid styles duplication.
Diffstat (limited to 'src/components/molecules')
-rw-r--r--src/components/molecules/forms/flipping-label/flipping-label.module.scss58
-rw-r--r--src/components/molecules/forms/flipping-label/flipping-label.stories.tsx12
-rw-r--r--src/components/molecules/forms/flipping-label/flipping-label.test.tsx15
-rw-r--r--src/components/molecules/forms/flipping-label/flipping-label.tsx45
-rw-r--r--src/components/molecules/images/flipping-logo.module.scss59
-rw-r--r--src/components/molecules/images/flipping-logo.stories.tsx72
-rw-r--r--src/components/molecules/images/flipping-logo.test.tsx26
-rw-r--r--src/components/molecules/images/flipping-logo.tsx63
-rw-r--r--src/components/molecules/images/index.ts1
-rw-r--r--src/components/molecules/layout/branding.module.scss32
-rw-r--r--src/components/molecules/layout/branding.stories.tsx26
-rw-r--r--src/components/molecules/layout/branding.test.tsx75
-rw-r--r--src/components/molecules/layout/branding.tsx54
13 files changed, 186 insertions, 352 deletions
diff --git a/src/components/molecules/forms/flipping-label/flipping-label.module.scss b/src/components/molecules/forms/flipping-label/flipping-label.module.scss
index 4e7947f..169bde3 100644
--- a/src/components/molecules/forms/flipping-label/flipping-label.module.scss
+++ b/src/components/molecules/forms/flipping-label/flipping-label.module.scss
@@ -1,61 +1,17 @@
@use "../../../../styles/abstracts/functions" as fun;
-.label {
- display: block;
- width: var(--btn-size, #{fun.convert-px(60)});
- height: var(--btn-size, #{fun.convert-px(60)});
+.wrapper {
+ --size: var(--btn-size, #{fun.convert-px(60)});
+ --flipper-speed: 0.5s;
+
+ width: var(--size);
+ height: var(--size);
}
+.wrapper,
.front,
.back {
display: flex;
place-content: center;
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- right: 0;
- backface-visibility: hidden;
- transition: all 0.6s ease-in 0s;
-}
-
-.front {
- z-index: 20;
-}
-
-.back {
- z-index: 10;
-}
-
-.wrapper {
- display: flex;
- place-content: center;
place-items: center;
- width: 100%;
- height: 100%;
- position: relative;
- transition: all 0.5s ease-in-out 0s;
- transform-style: preserve-3d;
-
- &--active {
- transform: rotateY(180deg);
-
- .front {
- transform: scale(0.2);
- }
-
- .back {
- transform: scale(1) rotateY(180deg);
- }
- }
-
- &--inactive {
- .front {
- transform: scale(1);
- }
-
- .back {
- transform: scale(0.2) rotateY(180deg);
- }
- }
}
diff --git a/src/components/molecules/forms/flipping-label/flipping-label.stories.tsx b/src/components/molecules/forms/flipping-label/flipping-label.stories.tsx
index bf5724e..c3c4f9a 100644
--- a/src/components/molecules/forms/flipping-label/flipping-label.stories.tsx
+++ b/src/components/molecules/forms/flipping-label/flipping-label.stories.tsx
@@ -1,6 +1,6 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { useCallback, useState } from 'react';
-import { Icon } from '../../../atoms';
+import { Button, Icon } from '../../../atoms';
import { FlippingLabel } from './flipping-label';
export default {
@@ -78,20 +78,22 @@ const Template: ComponentStory<typeof FlippingLabel> = ({
const updateState = useCallback(() => setActive((prev) => !prev), []);
return (
- <button onClick={updateState} type="button">
+ <Button kind="neutral" onClick={updateState} shape="initial" type="button">
<FlippingLabel {...args} isActive={active} />
- </button>
+ </Button>
);
};
export const Active = Template.bind({});
Active.args = {
- children: <Icon shape="magnifying-glass" />,
+ icon: <Icon shape="magnifying-glass" />,
isActive: true,
+ label: 'Close the search',
};
export const Inactive = Template.bind({});
Inactive.args = {
- children: <Icon shape="magnifying-glass" />,
+ icon: <Icon shape="magnifying-glass" />,
isActive: false,
+ label: 'Open the search',
};
diff --git a/src/components/molecules/forms/flipping-label/flipping-label.test.tsx b/src/components/molecules/forms/flipping-label/flipping-label.test.tsx
index 71ea2ba..d59c5f3 100644
--- a/src/components/molecules/forms/flipping-label/flipping-label.test.tsx
+++ b/src/components/molecules/forms/flipping-label/flipping-label.test.tsx
@@ -1,15 +1,18 @@
import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../../tests/utils';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { Icon } from '../../../atoms';
import { FlippingLabel } from './flipping-label';
describe('FlippingLabel', () => {
it('renders a label', () => {
- const ariaLabel = 'vero quo inventore';
+ const label = 'vero quo inventore';
render(
- <FlippingLabel aria-label={ariaLabel} isActive={false}>
- <>Test</>
- </FlippingLabel>
+ <FlippingLabel
+ icon={<Icon shape="arrow" />}
+ isActive={false}
+ label={label}
+ />
);
- expect(screen.getByLabelText(ariaLabel)).toBeInTheDocument();
+ expect(rtlScreen.getByText(label)).toBeInTheDocument();
});
});
diff --git a/src/components/molecules/forms/flipping-label/flipping-label.tsx b/src/components/molecules/forms/flipping-label/flipping-label.tsx
index e9d6a10..586301f 100644
--- a/src/components/molecules/forms/flipping-label/flipping-label.tsx
+++ b/src/components/molecules/forms/flipping-label/flipping-label.tsx
@@ -1,37 +1,54 @@
-import type { FC } from 'react';
-import { Icon, Label, type LabelProps } from '../../../atoms';
+import type { FC, ReactNode } from 'react';
+import {
+ Icon,
+ Label,
+ VisuallyHidden,
+ type LabelProps,
+ Flip,
+ FlipSide,
+} from '../../../atoms';
import styles from './flipping-label.module.scss';
-export type FlippingLabelProps = Pick<
+export type FlippingLabelProps = Omit<
LabelProps,
- 'aria-label' | 'className' | 'htmlFor'
+ 'children' | 'isHidden' | 'isRequired'
> & {
/**
* The front icon.
*/
- children: JSX.Element;
+ icon: ReactNode;
/**
* Which side of the label should be displayed? True for the close icon.
*/
isActive: boolean;
+ /**
+ * An accessible name for the label.
+ */
+ label: string;
};
export const FlippingLabel: FC<FlippingLabelProps> = ({
- children,
className = '',
+ icon,
isActive,
+ label,
...props
}) => {
- const wrapperModifier = isActive ? 'wrapper--active' : 'wrapper--inactive';
+ const wrapperClass = `${styles.wrapper} ${className}`;
return (
- <Label {...props} className={`${styles.label} ${className}`}>
- <span className={`${styles.wrapper} ${styles[wrapperModifier]}`}>
- <span className={styles.front}>{children}</span>
- <span className={styles.back}>
- <Icon aria-hidden={true} shape="cross" />
- </span>
- </span>
+ <Label {...props} className={wrapperClass}>
+ <VisuallyHidden>{label}</VisuallyHidden>
+ <Flip
+ aria-hidden
+ // eslint-disable-next-line react/jsx-no-literals -- Shape allowed
+ showBack={isActive}
+ >
+ <FlipSide className={styles.front}>{icon}</FlipSide>
+ <FlipSide className={styles.back} isBack>
+ <Icon aria-hidden shape="cross" />
+ </FlipSide>
+ </Flip>
</Label>
);
};
diff --git a/src/components/molecules/images/flipping-logo.module.scss b/src/components/molecules/images/flipping-logo.module.scss
deleted file mode 100644
index b3b7c96..0000000
--- a/src/components/molecules/images/flipping-logo.module.scss
+++ /dev/null
@@ -1,59 +0,0 @@
-@use "../../../styles/abstracts/functions" as fun;
-
-.logo {
- width: var(--logo-size, fun.convert-px(100));
- height: var(--logo-size, fun.convert-px(100));
- position: relative;
- border-radius: 50%;
- transform-style: preserve-3d;
- transition: all 0.6s linear 0s;
-
- &__front,
- &__back {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- backface-visibility: hidden;
- background: var(--color-bg);
- border: fun.convert-px(2) solid var(--color-primary-dark);
- border-radius: 50%;
- transition: all 0.6s linear 0s;
-
- svg,
- img {
- // !important is required to override next/image styles...
- padding: fun.convert-px(2) !important;
- border-radius: 50%;
- }
- }
-
- &__front {
- box-shadow: fun.convert-px(1) fun.convert-px(2) fun.convert-px(1) 0
- var(--color-shadow-light),
- fun.convert-px(2) fun.convert-px(3) fun.convert-px(3) 0
- var(--color-shadow-light);
- }
-
- &__back {
- transform: rotateY(180deg);
- }
-
- &:hover {
- transform: rotateY(180deg);
- }
-
- &:hover & {
- &__front {
- box-shadow: none;
- }
-
- &__back {
- box-shadow: fun.convert-px(1) fun.convert-px(2) fun.convert-px(1) 0
- var(--color-shadow-light),
- fun.convert-px(2) fun.convert-px(3) fun.convert-px(3) 0
- var(--color-shadow-light);
- }
- }
-}
diff --git a/src/components/molecules/images/flipping-logo.stories.tsx b/src/components/molecules/images/flipping-logo.stories.tsx
deleted file mode 100644
index ae4739a..0000000
--- a/src/components/molecules/images/flipping-logo.stories.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { FlippingLogo as FlippingLogoComponent } from './flipping-logo';
-
-/**
- * FlippingLogo - Storybook Meta
- */
-export default {
- title: 'Molecules/Images',
- component: FlippingLogoComponent,
- argTypes: {
- altText: {
- control: {
- type: 'text',
- },
- description: 'Photo alternative text.',
- type: {
- name: 'string',
- required: true,
- },
- },
- className: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the logo wrapper.',
- table: {
- category: 'Options',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- logoTitle: {
- control: {
- type: 'text',
- },
- description: 'An accessible name for the logo.',
- table: {
- category: 'Accessibility',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- photo: {
- control: {
- type: 'text',
- },
- description: 'Photo url.',
- type: {
- name: 'string',
- required: true,
- },
- },
- },
-} as ComponentMeta<typeof FlippingLogoComponent>;
-
-const Template: ComponentStory<typeof FlippingLogoComponent> = (args) => (
- <FlippingLogoComponent {...args} />
-);
-
-/**
- * Images Stories - Flipping Logo
- */
-export const FlippingLogo = Template.bind({});
-FlippingLogo.args = {
- altText: 'Website picture',
- logoTitle: 'Website logo',
- photo: 'http://placeimg.com/640/480',
-};
diff --git a/src/components/molecules/images/flipping-logo.test.tsx b/src/components/molecules/images/flipping-logo.test.tsx
deleted file mode 100644
index ec0b787..0000000
--- a/src/components/molecules/images/flipping-logo.test.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../tests/utils';
-import { FlippingLogo } from './flipping-logo';
-
-describe('FlippingLogo', () => {
- it('renders a photo', () => {
- render(
- <FlippingLogo
- altText="Alternative text"
- photo="http://placeimg.com/640/480"
- />
- );
- expect(screen.getByAltText('Alternative text')).toBeInTheDocument();
- });
-
- it('renders a logo', () => {
- render(
- <FlippingLogo
- altText="Alternative text"
- logoTitle="A logo title"
- photo="http://placeimg.com/640/480"
- />
- );
- expect(screen.getByTitle('A logo title')).toBeInTheDocument();
- });
-});
diff --git a/src/components/molecules/images/flipping-logo.tsx b/src/components/molecules/images/flipping-logo.tsx
deleted file mode 100644
index 703d5d6..0000000
--- a/src/components/molecules/images/flipping-logo.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import NextImage, { type ImageProps } from 'next/image';
-import {
- type ForwardedRef,
- forwardRef,
- type ForwardRefRenderFunction,
-} from 'react';
-import { Logo, type LogoProps } from '../../atoms';
-import styles from './flipping-logo.module.scss';
-
-export type FlippingLogoProps = {
- /**
- * Set additional classnames to the logo wrapper.
- */
- className?: string;
- /**
- * Photo alternative text.
- */
- altText: string;
- /**
- * Logo image title.
- */
- logoTitle?: LogoProps['heading'];
- /**
- * Photo url.
- */
- photo: ImageProps['src'];
-};
-
-const FlippingLogoWithRef: ForwardRefRenderFunction<
- HTMLDivElement,
- FlippingLogoProps
-> = (
- { className = '', altText, logoTitle, photo, ...props },
- ref: ForwardedRef<HTMLDivElement>
-) => {
- const wrapperClass = `${styles.logo} ${className}`;
- const size = 100;
-
- return (
- <div className={wrapperClass} ref={ref}>
- <div className={styles.logo__front}>
- <NextImage
- {...props}
- alt={altText}
- height={size}
- src={photo}
- style={{ objectFit: 'cover' }}
- width={size}
- />
- </div>
- <div className={styles.logo__back}>
- <Logo heading={logoTitle} />
- </div>
- </div>
- );
-};
-
-/**
- * FlippingLogo component
- *
- * Render a logo and a photo with a flipping effect.
- */
-export const FlippingLogo = forwardRef(FlippingLogoWithRef);
diff --git a/src/components/molecules/images/index.ts b/src/components/molecules/images/index.ts
index 33ec886..a00c6c2 100644
--- a/src/components/molecules/images/index.ts
+++ b/src/components/molecules/images/index.ts
@@ -1,2 +1 @@
-export * from './flipping-logo';
export * from './responsive-image';
diff --git a/src/components/molecules/layout/branding.module.scss b/src/components/molecules/layout/branding.module.scss
index 4d9e32c..bacf381 100644
--- a/src/components/molecules/layout/branding.module.scss
+++ b/src/components/molecules/layout/branding.module.scss
@@ -42,7 +42,7 @@
@include mix.media("screen") {
@include mix.dimensions("2xs") {
grid-template-columns:
- var(--logo-size, fun.convert-px(100))
+ var(--logo-size)
minmax(0, 1fr);
grid-template-rows: 1fr min-content;
align-items: center;
@@ -55,6 +55,8 @@
.logo {
grid-row: span 2;
margin-bottom: var(--spacing-sm);
+ border-radius: 50%;
+ animation: flip-logo 9s ease-in 0s 1;
@include mix.media("screen") {
@include mix.dimensions("2xs") {
@@ -103,3 +105,31 @@
}
}
}
+
+.flip {
+ width: var(--logo-size);
+ height: var(--logo-size);
+ border: fun.convert-px(2) solid var(--color-primary-dark);
+ border-radius: 50%;
+ box-shadow:
+ fun.convert-px(1) fun.convert-px(2) fun.convert-px(1) 0
+ var(--color-shadow-light),
+ fun.convert-px(2) fun.convert-px(3) fun.convert-px(3) 0
+ var(--color-shadow-light);
+
+ > * {
+ padding: fun.convert-px(2);
+ border-radius: 50%;
+ }
+}
+
+@keyframes flip-logo {
+ 0%,
+ 90% {
+ transform: rotateY(180deg);
+ }
+
+ 100% {
+ transform: rotateY(0deg);
+ }
+}
diff --git a/src/components/molecules/layout/branding.stories.tsx b/src/components/molecules/layout/branding.stories.tsx
index 04844e2..7ff88c9 100644
--- a/src/components/molecules/layout/branding.stories.tsx
+++ b/src/components/molecules/layout/branding.stories.tsx
@@ -1,4 +1,6 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import NextImage from 'next/image';
+import { Logo } from '../../atoms';
import { Branding } from './branding';
/**
@@ -82,8 +84,16 @@ const Template: ComponentStory<typeof Branding> = (args) => (
*/
export const Default = Template.bind({});
Default.args = {
+ logo: <Logo heading="A logo example" />,
+ photo: (
+ <NextImage
+ alt="A photo example"
+ height={200}
+ src="https://picsum.photos/200"
+ width={200}
+ />
+ ),
title: 'Website title',
- photo: 'http://placeimg.com/640/480',
};
/**
@@ -91,7 +101,15 @@ Default.args = {
*/
export const WithBaseline = Template.bind({});
WithBaseline.args = {
- title: 'Website title',
baseline: 'Maiores corporis qui',
- photo: 'http://placeimg.com/640/480',
+ logo: <Logo heading="A logo example" />,
+ photo: (
+ <NextImage
+ alt="A photo example"
+ height={200}
+ src="https://picsum.photos/200"
+ width={200}
+ />
+ ),
+ title: 'Website title',
};
diff --git a/src/components/molecules/layout/branding.test.tsx b/src/components/molecules/layout/branding.test.tsx
index 4b76446..cfb55c5 100644
--- a/src/components/molecules/layout/branding.test.tsx
+++ b/src/components/molecules/layout/branding.test.tsx
@@ -1,62 +1,109 @@
import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../tests/utils';
+import NextImage from 'next/image';
+import { render, screen as rtlScreen } from '../../../../tests/utils';
+import { Logo } from '../../atoms';
import { Branding } from './branding';
describe('Branding', () => {
it('renders a photo', () => {
+ const altText = 'A photo example';
+
render(
<Branding
- photo="http://placeimg.com/640/480/city"
+ logo={<Logo />}
+ photo={
+ <NextImage
+ alt="A photo example"
+ height={200}
+ src="https://picsum.photos/200"
+ width={200}
+ />
+ }
title="Website title"
/>
);
- expect(
- screen.getByRole('img', { name: 'Website title picture' })
- ).toBeInTheDocument();
+ expect(rtlScreen.getByRole('img', { name: altText })).toBeInTheDocument();
});
it('renders a logo', () => {
+ const logoHeading = 'sed enim voluptatem';
+
render(
- <Branding photo="http://placeimg.com/640/480/city" title="Website name" />
+ <Branding
+ logo={<Logo heading={logoHeading} />}
+ photo={
+ <NextImage
+ alt="A photo example"
+ height={200}
+ src="https://picsum.photos/200"
+ width={200}
+ />
+ }
+ title="Website name"
+ />
);
- expect(screen.getByTitle('Website name logo')).toBeInTheDocument();
+ expect(rtlScreen.getByTitle(logoHeading)).toBeInTheDocument();
});
it('renders a baseline', () => {
render(
<Branding
- photo="http://placeimg.com/640/480"
+ logo={<Logo />}
+ photo={
+ <NextImage
+ alt="A photo example"
+ height={200}
+ src="https://picsum.photos/200"
+ width={200}
+ />
+ }
title="Website title"
baseline="Website baseline"
/>
);
- expect(screen.getByText('Website baseline')).toBeInTheDocument();
+ expect(rtlScreen.getByText('Website baseline')).toBeInTheDocument();
});
it('renders a title wrapped with h1 element', () => {
render(
<Branding
- photo="http://placeimg.com/640/480"
+ logo={<Logo />}
+ photo={
+ <NextImage
+ alt="A photo example"
+ height={200}
+ src="https://picsum.photos/200"
+ width={200}
+ />
+ }
title="Website title"
isHome={true}
/>
);
expect(
- screen.getByRole('heading', { level: 1, name: 'Website title' })
+ rtlScreen.getByRole('heading', { level: 1, name: 'Website title' })
).toBeInTheDocument();
});
it('renders a title with h1 styles', () => {
render(
<Branding
- photo="http://placeimg.com/640/480"
+ logo={<Logo />}
+ photo={
+ <NextImage
+ alt="A photo example"
+ height={200}
+ src="https://picsum.photos/200"
+ width={200}
+ />
+ }
title="Website title"
isHome={false}
/>
);
expect(
- screen.queryByRole('heading', { level: 1, name: 'Website title' })
+ rtlScreen.queryByRole('heading', { level: 1, name: 'Website title' })
).not.toBeInTheDocument();
- expect(screen.getByText('Website title')).toHaveClass('heading--1');
+ expect(rtlScreen.getByText('Website title')).toHaveClass('heading--1');
});
});
diff --git a/src/components/molecules/layout/branding.tsx b/src/components/molecules/layout/branding.tsx
index dceee5e..c3d3b7c 100644
--- a/src/components/molecules/layout/branding.tsx
+++ b/src/components/molecules/layout/branding.tsx
@@ -1,11 +1,9 @@
-import { type FC, useRef } from 'react';
-import { useIntl } from 'react-intl';
+import { type FC, useRef, type ReactNode } from 'react';
import { useStyles } from '../../../utils/hooks';
-import { Heading, Link } from '../../atoms';
-import { FlippingLogo, type FlippingLogoProps } from '../images';
+import { Flip, FlipSide, Heading, Link } from '../../atoms';
import styles from './branding.module.scss';
-export type BrandingProps = Pick<FlippingLogoProps, 'photo'> & {
+export type BrandingProps = {
/**
* The Branding baseline.
*/
@@ -15,6 +13,14 @@ export type BrandingProps = Pick<FlippingLogoProps, 'photo'> & {
*/
isHome?: boolean;
/**
+ * The website logo.
+ */
+ logo: ReactNode;
+ /**
+ * Your photo.
+ */
+ photo: ReactNode;
+ /**
* The Branding title;
*/
title: string;
@@ -32,31 +38,14 @@ export type BrandingProps = Pick<FlippingLogoProps, 'photo'> & {
export const Branding: FC<BrandingProps> = ({
baseline,
isHome = false,
+ logo,
photo,
title,
withLink = false,
...props
}) => {
const baselineRef = useRef<HTMLParagraphElement>(null);
- const logoRef = useRef<HTMLDivElement>(null);
const titleRef = useRef<HTMLHeadingElement | HTMLParagraphElement>(null);
- const intl = useIntl();
- const altText = intl.formatMessage(
- {
- defaultMessage: '{website} picture',
- description: 'Branding: photo alternative text',
- id: 'dDK5oc',
- },
- { website: title }
- );
- const logoTitle = intl.formatMessage(
- {
- defaultMessage: '{website} logo',
- description: 'Branding: logo title',
- id: 'x55qsD',
- },
- { website: title }
- );
useStyles({
property: '--typing-animation',
@@ -69,22 +58,15 @@ export const Branding: FC<BrandingProps> = ({
'hide-text 4.25s linear 0s 1, blink 0.8s ease-in-out 4.25s 2, typing 3.8s linear 4.25s 1',
target: baselineRef,
});
- useStyles({
- property: 'animation',
- styles: 'flip-logo 9s ease-in 0s 1',
- target: logoRef,
- });
return (
<div className={styles.wrapper}>
- <FlippingLogo
- {...props}
- altText={altText}
- className={styles.logo}
- logoTitle={logoTitle}
- photo={photo}
- ref={logoRef}
- />
+ <Flip {...props} className={styles.logo}>
+ <FlipSide className={styles.flip}>{photo}</FlipSide>
+ <FlipSide className={styles.flip} isBack>
+ {logo}
+ </FlipSide>
+ </Flip>
<Heading
className={styles.title}
isFake={!isHome}