From d75b9a1e150ab211c1052fb49bede9bd16320aca Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Sat, 7 Oct 2023 18:44:14 +0200 Subject: 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. --- src/components/atoms/flip/flip-side.tsx | 35 ++++++++++ src/components/atoms/flip/flip.module.scss | 49 ++++++++++++++ src/components/atoms/flip/flip.stories.tsx | 61 ++++++++++++++++++ src/components/atoms/flip/flip.test.tsx | 72 +++++++++++++++++++++ src/components/atoms/flip/flip.tsx | 50 +++++++++++++++ src/components/atoms/flip/index.ts | 2 + src/components/atoms/index.ts | 1 + .../flipping-label/flipping-label.module.scss | 58 ++--------------- .../flipping-label/flipping-label.stories.tsx | 12 ++-- .../forms/flipping-label/flipping-label.test.tsx | 15 +++-- .../forms/flipping-label/flipping-label.tsx | 45 +++++++++---- .../molecules/images/flipping-logo.module.scss | 59 ----------------- .../molecules/images/flipping-logo.stories.tsx | 72 --------------------- .../molecules/images/flipping-logo.test.tsx | 26 -------- src/components/molecules/images/flipping-logo.tsx | 63 ------------------ src/components/molecules/images/index.ts | 1 - .../molecules/layout/branding.module.scss | 32 ++++++++- .../molecules/layout/branding.stories.tsx | 26 ++++++-- src/components/molecules/layout/branding.test.tsx | 75 ++++++++++++++++++---- src/components/molecules/layout/branding.tsx | 54 ++++++---------- .../organisms/layout/site-header.stories.tsx | 12 +++- .../organisms/layout/site-header.test.tsx | 24 +++++-- src/components/organisms/toolbar/search.tsx | 7 +- src/components/organisms/toolbar/settings.tsx | 7 +- src/components/templates/layout/layout.tsx | 31 ++++++++- src/styles/base/_animations.scss | 42 ++++++------ 26 files changed, 543 insertions(+), 388 deletions(-) create mode 100644 src/components/atoms/flip/flip-side.tsx create mode 100644 src/components/atoms/flip/flip.module.scss create mode 100644 src/components/atoms/flip/flip.stories.tsx create mode 100644 src/components/atoms/flip/flip.test.tsx create mode 100644 src/components/atoms/flip/flip.tsx create mode 100644 src/components/atoms/flip/index.ts delete mode 100644 src/components/molecules/images/flipping-logo.module.scss delete mode 100644 src/components/molecules/images/flipping-logo.stories.tsx delete mode 100644 src/components/molecules/images/flipping-logo.test.tsx delete mode 100644 src/components/molecules/images/flipping-logo.tsx (limited to 'src') 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, '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 ( +
+ {children} +
+ ); +}; + +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; + +const Template: ComponentStory = (args) => ( + +); + +/** + * Images Stories - Horizontal Flipping + */ +export const Horizontal = Template.bind({}); +Horizontal.args = { + children: ( + <> + + Consequatur natus possimus quia consequatur placeat consectetur. Quia + vel magnam. Dolorem in quas non inventore aut sapiente. Consequuntur est + cum et. + + + 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. + + + ), +}; + +/** + * Images Stories - Vertical Flipping + */ +export const Vertical = Template.bind({}); +Vertical.args = { + children: ( + <> + + Consequatur natus possimus quia consequatur placeat consectetur. Quia + vel magnam. Dolorem in quas non inventore aut sapiente. Consequuntur est + cum et. + + + 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. + + + ), + 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( + + {front} + {back} + + ); + + 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( + + {front} + {back} + + ); + + expect(rtlScreen.getByText(front).parentElement).toHaveClass( + 'wrapper--horizontal' + ); + }); + + it('can be animated vertically', () => { + const front = 'quis et id'; + const back = 'quis est itaque'; + + render( + + {front} + {back} + + ); + + expect(rtlScreen.getByText(front).parentElement).toHaveClass( + 'wrapper--vertical' + ); + }); + + it('can be animated manually', () => { + const front = 'quis et id'; + const back = 'quis est itaque'; + + render( + + {front} + {back} + + ); + + 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, '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 = ( + { 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 ( +
+ {children} +
+ ); +}; + +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 = ({ const updateState = useCallback(() => setActive((prev) => !prev), []); return ( - + ); }; export const Active = Template.bind({}); Active.args = { - children: , + icon: , isActive: true, + label: 'Close the search', }; export const Inactive = Template.bind({}); Inactive.args = { - children: , + icon: , 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( - - <>Test - + } + 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 = ({ - children, className = '', + icon, isActive, + label, ...props }) => { - const wrapperModifier = isActive ? 'wrapper--active' : 'wrapper--inactive'; + const wrapperClass = `${styles.wrapper} ${className}`; return ( -