Skip to content

Commit 16f2af8

Browse files
authored
✨ Confetti (#1206)
* ✨ Confetti component and hook
1 parent 6adfe59 commit 16f2af8

19 files changed

Lines changed: 1334 additions & 0 deletions

src/atoms/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ export { useThemeProvider } from 'src/providers/ThemeProvider/ThemeProvider';
1717
export { usePrefetchRichTextImages } from './usePrefetchRichTextImages';
1818
export { useFaqsInApplication } from './useFaqsInApplication';
1919
export { useToasts } from 'src/providers/ToastProvider/ToastProvider';
20+
export { useConfetti } from 'src/providers/ConfettiProvider/ConfettiProvider';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ParticleShape } from './Confetti.types';
2+
import { colors } from 'src/atoms';
3+
4+
export const CONFETTI_DEFAULT_COLORS = [
5+
colors.dataviz.lightpurple.darker,
6+
colors.dataviz.lightblue.darker,
7+
colors.dataviz.lightgreen.default,
8+
colors.interactive.warning__resting.rgba,
9+
colors.interactive.danger__resting.rgba,
10+
colors.interactive.success__resting.rgba,
11+
];
12+
13+
export const CONFETTI_DEFAULT_SHAPES: ParticleShape[] = [
14+
'star',
15+
'circle',
16+
'square',
17+
'spiral',
18+
];
19+
20+
export const STAR_POINTS = 5;
21+
export const DEG_TO_RAD = Math.PI / 180;
22+
export const FRICTION = 0.9;
23+
export const GRAVITY = 0.5;
24+
25+
export const IMPERATIVE_STYLING: React.CSSProperties = {
26+
position: 'fixed',
27+
top: 0,
28+
left: 0,
29+
width: '100vw',
30+
height: '100vh',
31+
pointerEvents: 'none',
32+
};
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { Typography } from '@equinor/eds-core-react';
2+
import { Meta, StoryFn } from '@storybook/react-vite';
3+
4+
import { Button } from '../Button/Button';
5+
import { SeasonalColorsMap } from './utils/seasonalColors';
6+
import { Confetti } from './Confetti';
7+
import {
8+
CONFETTI_DEFAULT_COLORS,
9+
CONFETTI_DEFAULT_SHAPES,
10+
} from './Confetti.constants';
11+
import { ConfettiProps } from './Confetti.types';
12+
import { spacings } from 'src/atoms';
13+
import {
14+
ConfettiProvider,
15+
useConfetti,
16+
} from 'src/providers/ConfettiProvider/ConfettiProvider';
17+
18+
import styled from 'styled-components';
19+
20+
const meta: Meta<typeof Confetti> = {
21+
title: 'Molecules/Confetti',
22+
component: Confetti,
23+
parameters: {
24+
layout: 'fullscreen',
25+
},
26+
decorators: [
27+
(Story) => (
28+
<ConfettiProvider>
29+
<Story />
30+
</ConfettiProvider>
31+
),
32+
],
33+
argTypes: {
34+
mode: {
35+
control: { type: 'radio' },
36+
options: ['shower', 'boom'],
37+
},
38+
shapes: {
39+
control: { type: 'check' },
40+
options: CONFETTI_DEFAULT_SHAPES,
41+
},
42+
colors: {
43+
control: { type: 'check' },
44+
options: CONFETTI_DEFAULT_COLORS,
45+
},
46+
effectCount: {
47+
if: { arg: 'mode', eq: 'boom' },
48+
},
49+
duration: {
50+
if: { arg: 'mode', eq: 'shower' },
51+
control: { type: 'number' },
52+
},
53+
},
54+
args: {
55+
shapeSize: 12,
56+
mode: 'boom',
57+
shapes: CONFETTI_DEFAULT_SHAPES,
58+
colors: CONFETTI_DEFAULT_COLORS,
59+
effectCount: Infinity,
60+
},
61+
};
62+
63+
export default meta;
64+
65+
const Container = styled.div`
66+
width: 980px;
67+
height: 480px;
68+
position: relative;
69+
overflow: hidden;
70+
`;
71+
72+
export const Boom: StoryFn = (props: ConfettiProps) => {
73+
return (
74+
<Container>
75+
<Confetti {...props} mode="boom" />
76+
</Container>
77+
);
78+
};
79+
80+
export const Shower: StoryFn = (props: ConfettiProps) => {
81+
return (
82+
<Container>
83+
<Confetti {...props} mode="shower" />
84+
</Container>
85+
);
86+
};
87+
88+
const ButtonTriggeredContainer = styled.div`
89+
width: 100%;
90+
padding: ${spacings.medium};
91+
display: flex;
92+
flex-direction: column;
93+
align-items: center;
94+
gap: ${spacings.medium};
95+
`;
96+
97+
export const TriggerConfetti: StoryFn = () => {
98+
const { boom, shower } = useConfetti();
99+
100+
return (
101+
<ButtonTriggeredContainer>
102+
<Typography>
103+
Click the buttons below to trigger the boom and shower confetti effects
104+
</Typography>
105+
<div style={{ display: 'flex', gap: spacings.medium }}>
106+
<Button onClick={() => boom()}>Boom 🎉</Button>
107+
<Button onClick={() => shower()}>Shower 🌧️</Button>
108+
</div>
109+
</ButtonTriggeredContainer>
110+
);
111+
};
112+
113+
export const SeasonalColorsExample: StoryFn = () => {
114+
const christmasColors = SeasonalColorsMap['christmas'].colors;
115+
116+
return (
117+
<Container>
118+
<Confetti mode="shower" colors={christmasColors} />
119+
</Container>
120+
);
121+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import styled from 'styled-components';
2+
3+
export const Canvas = styled.canvas`
4+
width: 100%;
5+
height: 100%;
6+
pointer-events: none;
7+
`;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { useEffect } from 'react';
2+
3+
import { faker } from '@faker-js/faker';
4+
5+
import { Confetti } from './Confetti';
6+
import { useConfetti } from 'src/atoms';
7+
import { ConfettiProvider } from 'src/providers';
8+
import { render, screen } from 'src/tests/browsertest-utils';
9+
10+
test('Renders confetti canvas', () => {
11+
render(<Confetti mode="shower" duration={1000} />);
12+
const canvas = screen.getByTestId('canvas-confetti');
13+
14+
expect(canvas).toBeInTheDocument();
15+
});
16+
17+
test('renders confetti on boom', () => {
18+
const Test = () => {
19+
const { boom } = useConfetti();
20+
useEffect(() => {
21+
boom();
22+
}, [boom]);
23+
return null;
24+
};
25+
26+
render(
27+
<ConfettiProvider>
28+
<Test />
29+
</ConfettiProvider>
30+
);
31+
32+
expect(screen.getByTestId('canvas-confetti')).toBeInTheDocument();
33+
});
34+
35+
test("Adds styling to canvas when 'style' prop is provided", () => {
36+
const customStyle = { border: '2px solid red' };
37+
render(<Confetti style={customStyle} />);
38+
const canvas = screen.getByTestId('canvas-confetti');
39+
40+
expect(canvas).toHaveStyle('border: 2px solid red');
41+
});
42+
43+
test('Adds className to canvas when provided', () => {
44+
const customClassName = 'my-custom-confetti';
45+
render(<Confetti className={customClassName} />);
46+
const canvas = screen.getByTestId('canvas-confetti');
47+
48+
expect(canvas).toHaveClass(customClassName);
49+
});
50+
51+
test('Throws error on negative duration', async () => {
52+
const duration = faker.number.int({ min: Number.MIN_SAFE_INTEGER, max: 0 });
53+
expect(() => render(<Confetti mode="shower" duration={duration} />)).toThrow(
54+
'Duration must be a non-negative number'
55+
);
56+
});
57+
58+
test('Throws error on effect count under 1', async () => {
59+
const effectCount = faker.number.int({
60+
min: Number.MIN_SAFE_INTEGER,
61+
max: 0,
62+
});
63+
expect(() =>
64+
render(<Confetti mode="boom" effectCount={effectCount} />)
65+
).toThrow('Effect count must be at least 1');
66+
});

0 commit comments

Comments
 (0)