diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-07 18:44:14 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:14:41 +0100 |
| commit | d75b9a1e150ab211c1052fb49bede9bd16320aca (patch) | |
| tree | e5bb221d2b8dc83151697fe646e9194f921b5807 | |
| parent | 12a03a9a72f7895d571dbaeeb245d92aa277a610 (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.
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% { |
