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 /src/components/molecules | |
| 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.
Diffstat (limited to 'src/components/molecules')
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} |
