Skip to content

Commit 97a6e44

Browse files
committed
init Flashcard component
1 parent 29bd844 commit 97a6e44

File tree

9 files changed

+448
-0
lines changed

9 files changed

+448
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "pcln-design-system",
5+
"comment": "add Flashcard component",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "pcln-design-system"
10+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { render, screen } from '@testing-library/react'
2+
import React from 'react'
3+
import { Flashcard } from '..'
4+
5+
const frontside = 'Front'
6+
const backside = 'Back'
7+
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
8+
9+
describe('Flashcard', () => {
10+
it('renders the front content', () => {
11+
render(<Flashcard backside={backside}>{frontside}</Flashcard>)
12+
const front = screen.getByText(frontside)
13+
const back = screen.queryByText(backside)
14+
expect(front).toBeInTheDocument()
15+
expect(back).not.toBeInTheDocument()
16+
})
17+
18+
it('renders the back content when flipped', async () => {
19+
render(<Flashcard backside={backside}>{frontside}</Flashcard>)
20+
21+
const front = screen.getByText(frontside)
22+
23+
front.click()
24+
await wait(1000)
25+
26+
const front2 = screen.queryByText(frontside)
27+
const back = screen.getByText(backside)
28+
expect(front2).not.toBeInTheDocument()
29+
expect(back).toBeInTheDocument()
30+
})
31+
32+
it('renders the front content when dismissed', async () => {
33+
const handleChange = jest.fn()
34+
35+
render(
36+
<>
37+
<span>outside</span>
38+
<Flashcard defaultOpen backside={backside} onOpenChange={handleChange}>
39+
{frontside}
40+
</Flashcard>
41+
</>
42+
)
43+
44+
const outside = screen.getByText('outside')
45+
outside.click()
46+
await wait(1000)
47+
48+
const front = screen.getByText(frontside)
49+
const back = screen.queryByText(backside)
50+
expect(front).toBeInTheDocument()
51+
expect(back).not.toBeInTheDocument()
52+
expect(handleChange).toHaveBeenCalled()
53+
})
54+
55+
it('handles controlled state when open', async () => {
56+
render(
57+
<Flashcard backside={backside} open={false}>
58+
{frontside}
59+
</Flashcard>
60+
)
61+
62+
const front = screen.getByText(frontside)
63+
front.click()
64+
await wait(1000)
65+
66+
const back = screen.queryByText(backside)
67+
expect(back).not.toBeInTheDocument()
68+
})
69+
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { IFlashcardProps } from '..'
2+
import { borderRadii, Grid, paletteColors, shadows } from '..'
3+
4+
import type { ArgTypes } from '@storybook/react'
5+
import React from 'react'
6+
import { flashCardRotations } from './Flashcard.styled'
7+
8+
export const argTypes: Partial<ArgTypes<IFlashcardProps>> = {
9+
backside: { table: { disable: true } },
10+
backsideBg: { control: { type: 'select' }, options: paletteColors },
11+
bg: { control: { type: 'select' }, options: paletteColors },
12+
borderRadius: { control: { type: 'select' }, options: Object.keys(borderRadii) },
13+
boxShadowSize: { control: { type: 'select' }, options: Object.keys(shadows) },
14+
children: { table: { disable: true } },
15+
duration: { control: { type: 'number' } },
16+
open: { control: { type: 'boolean' } },
17+
perspective: { control: { type: 'number' } },
18+
rotation: { control: { type: 'select' }, options: flashCardRotations },
19+
}
20+
21+
export const defaultArgs: Partial<IFlashcardProps> = {
22+
backside: <Grid p={5}>Back</Grid>,
23+
backsideBg: 'secondary.light',
24+
bg: 'primary.light',
25+
borderRadius: 'xl',
26+
children: <Grid p={5}>Front</Grid>,
27+
duration: 0.5,
28+
perspective: 200,
29+
rotation: 'y',
30+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import type { Meta, StoryObj } from '@storybook/react'
2+
import { ArrowLeft } from 'pcln-icons'
3+
import React, { useState } from 'react'
4+
5+
import type { IFlashcardProps, IGridProps } from '..'
6+
import { Button, Flashcard, Grid, IconButton, Text } from '..'
7+
import { argTypes, defaultArgs } from './Flashcard.stories.args'
8+
9+
type FlashcardStory = StoryObj<IFlashcardProps>
10+
11+
const ExampleImage = () => (
12+
<img
13+
src='https://s1.pclncdn.com/design-assets/hero/beach.jpg?opto&optimize=medium&auto=jpg&width=600&height=450&fit=crop'
14+
alt='an example'
15+
style={{ width: '100%', display: 'block' }}
16+
/>
17+
)
18+
19+
export const Playground: FlashcardStory = {
20+
render: (args) => (
21+
<Grid width='fit-content'>
22+
<Flashcard {...args} />,
23+
</Grid>
24+
),
25+
}
26+
27+
export const Multiple: FlashcardStory = {
28+
render: (args) => (
29+
<Grid gap={4} templateColumns='1fr 1fr' width='fit-content'>
30+
<Flashcard {...args} />
31+
<Flashcard {...args} />
32+
<Flashcard {...args} />
33+
<Flashcard {...args} />
34+
</Grid>
35+
),
36+
}
37+
38+
export const DifferentSizes: FlashcardStory = {
39+
render: (args) => (
40+
<Grid width={200} height={200} placeItems='center'>
41+
<Flashcard {...args} backside={<Grid p={4}>Back</Grid>}>
42+
<Grid p={5}>Front</Grid>
43+
</Flashcard>
44+
</Grid>
45+
),
46+
}
47+
48+
const GridCell = (props: IGridProps) => <Grid placeItems='center' p={3} background='white' {...props} />
49+
export const ContentShift: FlashcardStory = {
50+
render: (args) => (
51+
<Grid templateColumns='repeat(3, auto)' gap={2} background='black' border='8px solid'>
52+
<GridCell>Top Left</GridCell>
53+
<GridCell>Top</GridCell>
54+
<GridCell>Top Right</GridCell>
55+
<GridCell>Left</GridCell>
56+
<Grid background='white'>
57+
<Flashcard
58+
{...args}
59+
backside={
60+
<Grid p={5} placeItems='center'>
61+
Back
62+
<br />
63+
<br />
64+
Lorem ipsum dolor sit amet consectetur adipisicing elit.
65+
</Grid>
66+
}
67+
>
68+
<Grid p={4} placeItems='center'>
69+
Front
70+
</Grid>
71+
</Flashcard>
72+
</Grid>
73+
<GridCell>Right</GridCell>
74+
<GridCell>Bottom Left</GridCell>
75+
<GridCell>Bottom</GridCell>
76+
<GridCell>Bottom Right</GridCell>
77+
</Grid>
78+
),
79+
}
80+
81+
export const OverflowContent: FlashcardStory = {
82+
...Playground,
83+
args: {
84+
...defaultArgs,
85+
children: (
86+
<Grid overflow='hidden' borderRadius={16}>
87+
<Grid p={4} gap={3} overflow='auto' maxHeight={200} maxWidth={300}>
88+
<Text textStyle='heading3'>Front</Text>
89+
<Text textStyle='paragraph'>
90+
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore accusamus mollitia ipsa aliquid,
91+
placeat sint consequatur inventore doloribus in culpa dolorum excepturi possimus ab. Deleniti
92+
repellat quo aspernatur nihil. Maiores.
93+
</Text>
94+
</Grid>
95+
</Grid>
96+
),
97+
backside: (
98+
<Grid overflow='hidden' borderRadius={16}>
99+
<Grid p={4} gap={3} overflow='auto' maxHeight={200} maxWidth={300}>
100+
<Text textStyle='heading3'>Back</Text>
101+
<Text textStyle='paragraph'>
102+
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore accusamus mollitia ipsa aliquid,
103+
placeat sint consequatur inventore doloribus in culpa dolorum excepturi possimus ab. Deleniti
104+
repellat quo aspernatur nihil. Maiores.
105+
</Text>
106+
</Grid>
107+
</Grid>
108+
),
109+
},
110+
}
111+
112+
export const Image: FlashcardStory = {
113+
...Playground,
114+
args: {
115+
...defaultArgs,
116+
perspective: 400,
117+
children: (
118+
<Grid width={400} height={300} placeItems='center' overflow='hidden' borderRadius={16}>
119+
<ExampleImage />
120+
</Grid>
121+
),
122+
backside: (
123+
<Grid position='relative' width={400} height={300} p={5} gap={3} overflow='hidden' borderRadius={16}>
124+
<Grid
125+
position='absolute'
126+
zIndex={-1}
127+
style={{ inset: 0, filter: 'blur(.5rem)', transform: 'rotateY(180deg)' }}
128+
>
129+
<ExampleImage />
130+
</Grid>
131+
<Grid position='absolute' zIndex={-1} style={{ inset: 0, opacity: 0.75 }} background='white' />
132+
<Text textStyle='heading3'>Beach</Text>
133+
<Text textStyle='paragraph'>
134+
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore accusamus mollitia ipsa aliquid,
135+
placeat sint consequatur inventore doloribus in culpa dolorum excepturi possimus ab. Deleniti
136+
repellat quo aspernatur nihil. Maiores.
137+
</Text>
138+
</Grid>
139+
),
140+
},
141+
}
142+
143+
export const Controlled: FlashcardStory = {
144+
render: (args) => {
145+
const [open, setOpen] = useState(false)
146+
147+
const flipButton = (
148+
<IconButton
149+
icon={<ArrowLeft />}
150+
style={{ position: 'absolute', top: '1rem', left: '1rem', border: '2px solid currentcolor' }}
151+
onClick={() => setOpen(!open)}
152+
borderRadius='full'
153+
/>
154+
)
155+
156+
return (
157+
<Grid gap={5} placeItems='center'>
158+
<Button onClick={() => setOpen(!open)}>Toggle the Flashcard</Button>
159+
<Flashcard {...args} open={open} backside={<Grid p={6}> {flipButton} Back </Grid>}>
160+
<Grid p={6}> {flipButton} Front </Grid>
161+
</Flashcard>
162+
</Grid>
163+
)
164+
},
165+
argTypes: { open: { table: { disable: true } } },
166+
args: { perspective: 300 },
167+
}
168+
169+
const insideProps = { width: 200, height: 200, placeItems: 'center', p: 3 }
170+
const outsideProps = { width: 300, height: 300, placeItems: 'center', p: 3 }
171+
export const Nested: FlashcardStory = {
172+
render: (args) => (
173+
<Grid width='fit-content'>
174+
<Flashcard {...args} backside={<Grid {...outsideProps}> Outside Back </Grid>}>
175+
<Grid gap={3} {...outsideProps}>
176+
<Text>Outside Front</Text>
177+
<Flashcard {...args} backside={<Grid {...insideProps}> Inside Back </Grid>}>
178+
<Grid {...insideProps}>Inside Front</Grid>
179+
</Flashcard>
180+
</Grid>
181+
</Flashcard>
182+
</Grid>
183+
),
184+
args: {
185+
...defaultArgs,
186+
boxShadowSize: 'md',
187+
},
188+
}
189+
190+
const meta: Meta<typeof Flashcard> = {
191+
title: 'Flashcard',
192+
component: Flashcard,
193+
args: defaultArgs,
194+
argTypes: argTypes,
195+
}
196+
197+
export default meta
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { IFlashcardProps } from '..'
2+
3+
import themeGet from '@styled-system/theme-get'
4+
import type { ForwardRefComponent, HTMLMotionProps, TargetAndTransition } from 'framer-motion'
5+
import { motion } from 'framer-motion'
6+
import styled from 'styled-components'
7+
8+
export const flashCardRotations = ['x', 'y', 'x-reverse', 'y-reverse'] as const
9+
export type FlashcardRotation = (typeof flashCardRotations)[number]
10+
11+
export const flashcardRotations: Record<FlashcardRotation | 'reset', TargetAndTransition> = {
12+
x: { rotateX: 180 },
13+
y: { rotateY: 180 },
14+
'x-reverse': { rotateX: -180 },
15+
'y-reverse': { rotateY: -180 },
16+
reset: { rotateX: 0, rotateY: 0 },
17+
} as const
18+
19+
export type FlashcardMotionProps = Partial<IFlashcardProps> & HTMLMotionProps<'div'>
20+
21+
export type FlashcardContainerProps = HTMLMotionProps<'div'> &
22+
Partial<Omit<IFlashcardProps, 'borderRadius' | 'children'>>
23+
24+
export const CardContainer: (props: FlashcardContainerProps) => JSX.Element = styled(motion.div)`
25+
perspective: ${(props: FlashcardContainerProps) => props.perspective}px;
26+
`
27+
28+
export const OuterCardMotion: ForwardRefComponent<HTMLDivElement, FlashcardMotionProps> = styled(motion.div)``
29+
30+
export const InnerCardMotion: ForwardRefComponent<HTMLDivElement, FlashcardMotionProps> = styled(motion.div)`
31+
background-color: ${(props: FlashcardMotionProps) => themeGet(`palette.${props.bg}`)(props)};
32+
border-radius: ${(props: FlashcardMotionProps) => themeGet(`borderRadii.${props.borderRadius}`)(props)};
33+
box-shadow: ${(props: FlashcardMotionProps) => themeGet(`shadows.${props.boxShadowSize}`)(props)};
34+
&:hover {
35+
box-shadow: ${(props: FlashcardMotionProps) => themeGet(`shadows.2xl`)(props)};
36+
}
37+
`

0 commit comments

Comments
 (0)