aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/flip
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-10-07 18:44:14 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:14:41 +0100
commitd75b9a1e150ab211c1052fb49bede9bd16320aca (patch)
treee5bb221d2b8dc83151697fe646e9194f921b5807 /src/components/atoms/flip
parent12a03a9a72f7895d571dbaeeb245d92aa277a610 (diff)
feat(components): add a generic Flip component
The flipping animation is used at several places so it makes sense to use a single component to handle the animation. It will avoid styles duplication.
Diffstat (limited to 'src/components/atoms/flip')
-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
6 files changed, 269 insertions, 0 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';