aboutsummaryrefslogtreecommitdiffstats
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
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.
-rw-r--r--src/components/atoms/flip/flip-side.tsx35
-rw-r--r--src/components/atoms/flip/flip.module.scss49
-rw-r--r--src/components/atoms/flip/flip.stories.tsx61
-rw-r--r--src/components/atoms/flip/flip.test.tsx72
-rw-r--r--src/components/atoms/flip/flip.tsx50
-rw-r--r--src/components/atoms/flip/index.ts2
-rw-r--r--src/components/atoms/index.ts1
-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
-rw-r--r--src/components/organisms/layout/site-header.stories.tsx12
-rw-r--r--src/components/organisms/layout/site-header.test.tsx24
-rw-r--r--src/components/organisms/toolbar/search.tsx7
-rw-r--r--src/components/organisms/toolbar/settings.tsx7
-rw-r--r--src/components/templates/layout/layout.tsx31
-rw-r--r--src/styles/base/_animations.scss42
26 files changed, 543 insertions, 388 deletions
diff --git a/src/components/atoms/flip/flip-side.tsx b/src/components/atoms/flip/flip-side.tsx
new file mode 100644
index 0000000..31c23f5
--- /dev/null
+++ b/src/components/atoms/flip/flip-side.tsx
@@ -0,0 +1,35 @@
+import {
+ type ForwardRefRenderFunction,
+ type HTMLAttributes,
+ forwardRef,
+ type ReactNode,
+} from 'react';
+import styles from './flip.module.scss';
+
+export type FlipSideProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
+ /**
+ * The side contents.
+ */
+ children: ReactNode;
+ /**
+ * Is it the back side of the flip component?
+ *
+ * @default false
+ */
+ isBack?: boolean;
+};
+
+const FlipSideWithRef: ForwardRefRenderFunction<
+ HTMLDivElement,
+ FlipSideProps
+> = ({ children, className = '', isBack = false, ...props }, ref) => {
+ const sideClass = [isBack ? styles.back : styles.front, className].join(' ');
+
+ return (
+ <div {...props} className={sideClass} ref={ref}>
+ {children}
+ </div>
+ );
+};
+
+export const FlipSide = forwardRef(FlipSideWithRef);
diff --git a/src/components/atoms/flip/flip.module.scss b/src/components/atoms/flip/flip.module.scss
new file mode 100644
index 0000000..20b1715
--- /dev/null
+++ b/src/components/atoms/flip/flip.module.scss
@@ -0,0 +1,49 @@
+@use "../../../styles/abstracts/functions" as fun;
+
+.front,
+.back {
+ grid-area: 1 / 1 / 2 / 2;
+ backface-visibility: hidden;
+ transition: all var(--flipper-speed, 0.6s) linear 0s;
+}
+
+.back {
+ transform: var(--rotation);
+}
+
+.wrapper {
+ display: grid;
+ transform-style: preserve-3d;
+
+ &--dynamic {
+ &:hover,
+ &:focus,
+ &:focus-within {
+ .back {
+ transform: rotate(0);
+ }
+
+ .front {
+ transform: var(--rotation);
+ }
+ }
+ }
+
+ &--manual#{&}--is-back {
+ .back {
+ transform: rotate(0);
+ }
+
+ .front {
+ transform: var(--rotation);
+ }
+ }
+
+ &--horizontal {
+ --rotation: rotateY(180deg);
+ }
+
+ &--vertical {
+ --rotation: rotateX(180deg);
+ }
+}
diff --git a/src/components/atoms/flip/flip.stories.tsx b/src/components/atoms/flip/flip.stories.tsx
new file mode 100644
index 0000000..1e470b1
--- /dev/null
+++ b/src/components/atoms/flip/flip.stories.tsx
@@ -0,0 +1,61 @@
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Flip as FlipComponent } from './flip';
+import { FlipSide } from './flip-side';
+
+/**
+ * Flip - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Flip',
+ component: FlipComponent,
+ argTypes: {},
+} as ComponentMeta<typeof FlipComponent>;
+
+const Template: ComponentStory<typeof FlipComponent> = (args) => (
+ <FlipComponent {...args} tabIndex={0} />
+);
+
+/**
+ * Images Stories - Horizontal Flipping
+ */
+export const Horizontal = Template.bind({});
+Horizontal.args = {
+ children: (
+ <>
+ <FlipSide style={{ padding: '10px' }}>
+ Consequatur natus possimus quia consequatur placeat consectetur. Quia
+ vel magnam. Dolorem in quas non inventore aut sapiente. Consequuntur est
+ cum et.
+ </FlipSide>
+ <FlipSide isBack style={{ background: '#eee', padding: '10px' }}>
+ Iusto voluptatem repudiandae odit quo amet. Dolores vitae et neque
+ minima velit. Ad consequatur assumenda qui placeat aut consectetur
+ officia numquam illo. Neque quos voluptate ipsam eum ipsa officiis et
+ autem non.
+ </FlipSide>
+ </>
+ ),
+};
+
+/**
+ * Images Stories - Vertical Flipping
+ */
+export const Vertical = Template.bind({});
+Vertical.args = {
+ children: (
+ <>
+ <FlipSide style={{ padding: '10px' }}>
+ Consequatur natus possimus quia consequatur placeat consectetur. Quia
+ vel magnam. Dolorem in quas non inventore aut sapiente. Consequuntur est
+ cum et.
+ </FlipSide>
+ <FlipSide isBack style={{ background: '#eee', padding: '10px' }}>
+ Iusto voluptatem repudiandae odit quo amet. Dolores vitae et neque
+ minima velit. Ad consequatur assumenda qui placeat aut consectetur
+ officia numquam illo. Neque quos voluptate ipsam eum ipsa officiis et
+ autem non.
+ </FlipSide>
+ </>
+ ),
+ direction: 'vertical',
+};
diff --git a/src/components/atoms/flip/flip.test.tsx b/src/components/atoms/flip/flip.test.tsx
new file mode 100644
index 0000000..0fd986e
--- /dev/null
+++ b/src/components/atoms/flip/flip.test.tsx
@@ -0,0 +1,72 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { Flip } from './flip';
+import { FlipSide } from './flip-side';
+
+describe('Flip', () => {
+ it('renders the back and front sides', () => {
+ const front = 'laboriosam sint rem';
+ const back = 'tempore autem ea';
+
+ render(
+ <Flip>
+ <FlipSide>{front}</FlipSide>
+ <FlipSide isBack>{back}</FlipSide>
+ </Flip>
+ );
+
+ expect(rtlScreen.getByText(front)).toBeInTheDocument();
+ expect(rtlScreen.getByText(back)).toBeInTheDocument();
+ });
+
+ it('can be animated horizontally', () => {
+ const front = 'repudiandae maiores sunt';
+ const back = 'facilis nostrum voluptatibus';
+
+ render(
+ <Flip direction="horizontal">
+ <FlipSide>{front}</FlipSide>
+ <FlipSide isBack>{back}</FlipSide>
+ </Flip>
+ );
+
+ expect(rtlScreen.getByText(front).parentElement).toHaveClass(
+ 'wrapper--horizontal'
+ );
+ });
+
+ it('can be animated vertically', () => {
+ const front = 'quis et id';
+ const back = 'quis est itaque';
+
+ render(
+ <Flip direction="vertical">
+ <FlipSide>{front}</FlipSide>
+ <FlipSide isBack>{back}</FlipSide>
+ </Flip>
+ );
+
+ expect(rtlScreen.getByText(front).parentElement).toHaveClass(
+ 'wrapper--vertical'
+ );
+ });
+
+ it('can be animated manually', () => {
+ const front = 'quis et id';
+ const back = 'quis est itaque';
+
+ render(
+ <Flip showBack>
+ <FlipSide>{front}</FlipSide>
+ <FlipSide isBack>{back}</FlipSide>
+ </Flip>
+ );
+
+ expect(rtlScreen.getByText(front).parentElement).toHaveClass(
+ 'wrapper--manual'
+ );
+ expect(rtlScreen.getByText(front).parentElement).toHaveClass(
+ 'wrapper--is-back'
+ );
+ });
+});
diff --git a/src/components/atoms/flip/flip.tsx b/src/components/atoms/flip/flip.tsx
new file mode 100644
index 0000000..df77e9a
--- /dev/null
+++ b/src/components/atoms/flip/flip.tsx
@@ -0,0 +1,50 @@
+import {
+ type ForwardRefRenderFunction,
+ type HTMLAttributes,
+ type ReactNode,
+ forwardRef,
+} from 'react';
+import styles from './flip.module.scss';
+
+export type FlipProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
+ /**
+ * The front and back sides.
+ */
+ children: ReactNode;
+ /**
+ * The animation direction.
+ *
+ * @default 'horizontal'
+ */
+ direction?: 'horizontal' | 'vertical';
+ /**
+ * Should we show back side?
+ *
+ * It let you control dynamically which side to show. When set to `true` the
+ * hover/focus animation will be removed.
+ *
+ * @default undefined
+ */
+ showBack?: boolean;
+};
+
+const FlipWithRef: ForwardRefRenderFunction<HTMLDivElement, FlipProps> = (
+ { children, className = '', direction = 'horizontal', showBack, ...props },
+ ref
+) => {
+ const wrapperClass = [
+ styles.wrapper,
+ styles[`wrapper--${direction}`],
+ styles[showBack === undefined ? 'wrapper--dynamic' : 'wrapper--manual'],
+ styles[showBack ? 'wrapper--is-back' : 'wrapper--is-front'],
+ className,
+ ].join(' ');
+
+ return (
+ <div {...props} className={wrapperClass} ref={ref}>
+ {children}
+ </div>
+ );
+};
+
+export const Flip = forwardRef(FlipWithRef);
diff --git a/src/components/atoms/flip/index.ts b/src/components/atoms/flip/index.ts
new file mode 100644
index 0000000..cd01743
--- /dev/null
+++ b/src/components/atoms/flip/index.ts
@@ -0,0 +1,2 @@
+export * from './flip';
+export * from './flip-side';
diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts
index 399fdde..e9c41ed 100644
--- a/src/components/atoms/index.ts
+++ b/src/components/atoms/index.ts
@@ -1,4 +1,5 @@
export * from './buttons';
+export * from './flip';
export * from './forms';
export * from './heading';
export * from './images';
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}
diff --git a/src/components/organisms/layout/site-header.stories.tsx b/src/components/organisms/layout/site-header.stories.tsx
index 56f1689..2b57263 100644
--- a/src/components/organisms/layout/site-header.stories.tsx
+++ b/src/components/organisms/layout/site-header.stories.tsx
@@ -1,4 +1,6 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import NextImage from 'next/image';
+import { Logo } from '../../atoms';
import { SiteHeader as SiteHeaderComponent } from './site-header';
/**
@@ -147,7 +149,15 @@ const nav = [
*/
export const SiteHeader = Template.bind({});
SiteHeader.args = {
+ logo: <Logo />,
nav,
- photo: 'http://placeimg.com/640/480/people',
+ photo: (
+ <NextImage
+ alt="A photo"
+ height={200}
+ src="https://picsum.photos/200"
+ width={200}
+ />
+ ),
title: 'Website title',
};
diff --git a/src/components/organisms/layout/site-header.test.tsx b/src/components/organisms/layout/site-header.test.tsx
index e75f99f..dc0e00d 100644
--- a/src/components/organisms/layout/site-header.test.tsx
+++ b/src/components/organisms/layout/site-header.test.tsx
@@ -1,5 +1,7 @@
import { describe, expect, it } from '@jest/globals';
+import NextImage from 'next/image';
import { render, screen as rtlScreen } from '../../../../tests/utils';
+import { Logo } from '../../atoms';
import { SiteHeader } from './site-header';
const nav = [
@@ -9,8 +11,6 @@ const nav = [
{ id: 'contact-link', href: '#', label: 'Contact' },
];
-const photo = 'http://placeimg.com/640/480/nightlife';
-
const title = 'Assumenda quis quod';
describe('SiteHeader', () => {
@@ -19,9 +19,17 @@ describe('SiteHeader', () => {
<SiteHeader
ackeeStorageKey="ackee-tracking"
isHome={true}
+ logo={<Logo />}
motionStorageKey="reduced-motion"
nav={nav}
- photo={photo}
+ photo={
+ <NextImage
+ alt="A photo"
+ height={200}
+ src="https://picsum.photos/200"
+ width={200}
+ />
+ }
searchPage="#"
title={title}
/>
@@ -35,9 +43,17 @@ describe('SiteHeader', () => {
render(
<SiteHeader
ackeeStorageKey="ackee-tracking"
+ logo={<Logo />}
motionStorageKey="reduced-motion"
nav={nav}
- photo={photo}
+ photo={
+ <NextImage
+ alt="A photo"
+ height={200}
+ src="https://picsum.photos/200"
+ width={200}
+ />
+ }
searchPage="#"
title={title}
/>
diff --git a/src/components/organisms/toolbar/search.tsx b/src/components/organisms/toolbar/search.tsx
index a09bdae..6a33aff 100644
--- a/src/components/organisms/toolbar/search.tsx
+++ b/src/components/organisms/toolbar/search.tsx
@@ -62,13 +62,12 @@ const SearchWithRef: ForwardRefRenderFunction<HTMLDivElement, SearchProps> = (
value="open"
/>
<FlippingLabel
- aria-label={label}
className={sharedStyles.label}
htmlFor="search-button"
+ icon={<Icon aria-hidden={true} shape="magnifying-glass" size="lg" />}
isActive={isActive}
- >
- <Icon aria-hidden={true} shape="magnifying-glass" size="lg" />
- </FlippingLabel>
+ label={label}
+ />
<SearchModal
className={`${sharedStyles.modal} ${searchStyles.modal} ${className}`}
ref={searchInputRef}
diff --git a/src/components/organisms/toolbar/settings.tsx b/src/components/organisms/toolbar/settings.tsx
index 53634d8..b7625aa 100644
--- a/src/components/organisms/toolbar/settings.tsx
+++ b/src/components/organisms/toolbar/settings.tsx
@@ -54,13 +54,12 @@ const SettingsWithRef: ForwardRefRenderFunction<
value="open"
/>
<FlippingLabel
- aria-label={label}
className={styles.label}
htmlFor="settings-button"
+ icon={<Icon aria-hidden={true} shape="cog" size="lg" />}
isActive={isActive}
- >
- <Icon aria-hidden={true} shape="cog" size="lg" />
- </FlippingLabel>
+ label={label}
+ />
<SettingsModal
ackeeStorageKey={ackeeStorageKey}
className={`${styles.modal} ${className}`}
diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx
index 444b170..3e1eb63 100644
--- a/src/components/templates/layout/layout.tsx
+++ b/src/components/templates/layout/layout.tsx
@@ -1,4 +1,5 @@
/* eslint-disable max-statements */
+import NextImage from 'next/image';
import Script from 'next/script';
import {
type FC,
@@ -16,7 +17,7 @@ import {
useScrollPosition,
useSettings,
} from '../../../utils/hooks';
-import { ButtonLink, Icon, Main } from '../../atoms';
+import { ButtonLink, Icon, Logo, Main } from '../../atoms';
import {
SiteFooter,
type SiteFooterProps,
@@ -112,6 +113,22 @@ export const Layout: FC<LayoutProps> = ({
description: 'Layout: main nav - contact link',
id: 'AE4kCD',
});
+ const photoAltText = intl.formatMessage(
+ {
+ defaultMessage: '{website} picture',
+ description: 'Layout: photo alternative text',
+ id: '8jjY1X',
+ },
+ { website: name }
+ );
+ const logoTitle = intl.formatMessage(
+ {
+ defaultMessage: '{website} logo',
+ description: 'Layout: logo title',
+ id: '52H2HA',
+ },
+ { website: name }
+ );
const mainNav: SiteHeaderProps['nav'] = [
{
@@ -240,11 +257,19 @@ export const Layout: FC<LayoutProps> = ({
baseline={baseline}
className={styles.header}
isHome={isHome}
+ logo={<Logo heading={logoTitle} />}
// eslint-disable-next-line react/jsx-no-literals -- Storage key allowed
motionStorageKey="reduced-motion"
nav={mainNav}
- // eslint-disable-next-line react/jsx-no-literals -- Photo allowed
- photo="/armand-philippot.jpg"
+ photo={
+ <NextImage
+ alt={photoAltText}
+ height={100}
+ // eslint-disable-next-line react/jsx-no-literals -- Photo allowed
+ src="/armand-philippot.jpg"
+ width={100}
+ />
+ }
searchPage={ROUTES.SEARCH}
title={name}
withLink={true}
diff --git a/src/styles/base/_animations.scss b/src/styles/base/_animations.scss
index 21e1b47..f8d93e7 100644
--- a/src/styles/base/_animations.scss
+++ b/src/styles/base/_animations.scss
@@ -1,7 +1,7 @@
@use "../abstracts/functions" as fun;
@keyframes pulse {
- from {
+ 0% {
transform: scale(1);
}
@@ -9,44 +9,57 @@
transform: scale(0.8);
}
- to {
+ 100% {
transform: scale(1);
}
}
@keyframes draw-borders {
0% {
- background-position: top left, top right, bottom right, bottom left;
- background-size: 0% var(--draw-border-thickness, fun.convert-px(3)),
+ background-position:
+ top left,
+ top right,
+ bottom right,
+ bottom left;
+ background-size:
+ 0% var(--draw-border-thickness, fun.convert-px(3)),
var(--draw-border-thickness, fun.convert-px(3)) 0%,
0% var(--draw-border-thickness, fun.convert-px(3)),
var(--draw-border-thickness, fun.convert-px(3)) 0%;
}
25% {
- background-size: 0% var(--draw-border-thickness, fun.convert-px(3)),
+ background-size:
+ 0% var(--draw-border-thickness, fun.convert-px(3)),
var(--draw-border-thickness, fun.convert-px(3)) 0%,
100% var(--draw-border-thickness, fun.convert-px(3)),
var(--draw-border-thickness, fun.convert-px(3)) 0%;
}
50% {
- background-size: 0% var(--draw-border-thickness, fun.convert-px(3)),
+ background-size:
+ 0% var(--draw-border-thickness, fun.convert-px(3)),
var(--draw-border-thickness, fun.convert-px(3)) 0%,
100% var(--draw-border-thickness, fun.convert-px(3)),
var(--draw-border-thickness, fun.convert-px(3)) 100%;
}
75% {
- background-size: 100% var(--draw-border-thickness, fun.convert-px(3)),
+ background-size:
+ 100% var(--draw-border-thickness, fun.convert-px(3)),
var(--draw-border-thickness, fun.convert-px(3)) 0%,
100% var(--draw-border-thickness, fun.convert-px(3)),
var(--draw-border-thickness, fun.convert-px(3)) 100%;
}
100% {
- background-position: top left, top right, bottom right, bottom left;
- background-size: 100% var(--draw-border-thickness, fun.convert-px(3)),
+ background-position:
+ top left,
+ top right,
+ bottom right,
+ bottom left;
+ background-size:
+ 100% var(--draw-border-thickness, fun.convert-px(3)),
var(--draw-border-thickness, fun.convert-px(3)) 100%,
100% var(--draw-border-thickness, fun.convert-px(3)),
var(--draw-border-thickness, fun.convert-px(3)) 100%;
@@ -73,17 +86,6 @@
}
}
-@keyframes flip-logo {
- 0%,
- 90% {
- transform: rotateY(180deg);
- }
-
- 100% {
- transform: rotateY(0deg);
- }
-}
-
@keyframes hide-text {
0%,
100% {