-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #30098 from storybookjs/norbert/fix-confetti-bundling
Onboarding: Replace `react-confetti` with `@neoconfetti/react`
- Loading branch information
Showing
10 changed files
with
118 additions
and
230 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 26 additions & 123 deletions
149
code/addons/onboarding/src/components/Confetti/Confetti.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,131 +1,34 @@ | ||
import React, { useEffect } from 'react'; | ||
import { useState } from 'react'; | ||
import { createPortal } from 'react-dom'; | ||
import React, { type ComponentProps } from 'react'; | ||
|
||
import { styled } from 'storybook/internal/theming'; | ||
|
||
import ReactConfetti from 'react-confetti'; | ||
import { Confetti as ReactConfetti } from '@neoconfetti/react'; | ||
|
||
interface ConfettiProps extends Omit<React.ComponentProps<typeof ReactConfetti>, 'drawShape'> { | ||
top?: number; | ||
left?: number; | ||
width?: number; | ||
height?: number; | ||
numberOfPieces?: number; | ||
recycle?: boolean; | ||
colors?: string[]; | ||
} | ||
const Wrapper = styled.div({ | ||
zIndex: 9999, | ||
position: 'fixed', | ||
top: 0, | ||
left: '50%', | ||
width: '50%', | ||
height: '100%', | ||
}); | ||
|
||
const Wrapper = styled.div<{ | ||
width: number; | ||
height: number; | ||
top: number; | ||
left: number; | ||
}>(({ width, height, left, top }) => ({ | ||
width: `${width}px`, | ||
height: `${height}px`, | ||
left: `${left}px`, | ||
top: `${top}px`, | ||
position: 'relative', | ||
overflow: 'hidden', | ||
})); | ||
|
||
export function Confetti({ | ||
top = 0, | ||
left = 0, | ||
width = window.innerWidth, | ||
height = window.innerHeight, | ||
export const Confetti = React.memo(function Confetti({ | ||
timeToFade = 5000, | ||
colors = ['#CA90FF', '#FC521F', '#66BF3C', '#FF4785', '#FFAE00', '#1EA7FD'], | ||
...confettiProps | ||
}: ConfettiProps): React.ReactPortal { | ||
const [confettiContainer] = useState(() => { | ||
const container = document.createElement('div'); | ||
container.setAttribute('id', 'confetti-container'); | ||
container.setAttribute( | ||
'style', | ||
'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;' | ||
); | ||
|
||
return container; | ||
}); | ||
|
||
useEffect(() => { | ||
document.body.appendChild(confettiContainer); | ||
|
||
return () => { | ||
document.body.removeChild(confettiContainer); | ||
}; | ||
}, []); | ||
|
||
return createPortal( | ||
<Wrapper top={top} left={left} width={width} height={height}> | ||
<ReactConfetti colors={colors} drawShape={draw} {...confettiProps} /> | ||
</Wrapper>, | ||
confettiContainer | ||
}: ComponentProps<typeof ReactConfetti> & { timeToFade?: number }) { | ||
return ( | ||
<Wrapper> | ||
<ReactConfetti | ||
colors={colors} | ||
particleCount={200} | ||
duration={5000} | ||
stageHeight={window.innerHeight} | ||
stageWidth={window.innerWidth} | ||
destroyAfterDone | ||
{...confettiProps} | ||
/> | ||
</Wrapper> | ||
); | ||
} | ||
|
||
enum ParticleShape { | ||
Circle = 1, | ||
Square = 2, | ||
TShape = 3, | ||
LShape = 4, | ||
Triangle = 5, | ||
QuarterCircle = 6, | ||
} | ||
|
||
function getRandomInt(min: number, max: number) { | ||
return Math.floor(Math.random() * (max - min)) + min; | ||
} | ||
|
||
function draw(this: any, context: CanvasRenderingContext2D) { | ||
this.shape = this.shape || getRandomInt(1, 6); | ||
|
||
switch (this.shape) { | ||
case ParticleShape.Square: { | ||
const cornerRadius = 2; | ||
const width = this.w / 2; | ||
const height = this.h / 2; | ||
|
||
context.moveTo(-width + cornerRadius, -height); | ||
context.lineTo(width - cornerRadius, -height); | ||
context.arcTo(width, -height, width, -height + cornerRadius, cornerRadius); | ||
context.lineTo(width, height - cornerRadius); | ||
context.arcTo(width, height, width - cornerRadius, height, cornerRadius); | ||
context.lineTo(-width + cornerRadius, height); | ||
context.arcTo(-width, height, -width, height - cornerRadius, cornerRadius); | ||
context.lineTo(-width, -height + cornerRadius); | ||
context.arcTo(-width, -height, -width + cornerRadius, -height, cornerRadius); | ||
|
||
break; | ||
} | ||
case ParticleShape.TShape: { | ||
context.rect(-4, -4, 8, 16); | ||
context.rect(-12, -4, 24, 8); | ||
break; | ||
} | ||
case ParticleShape.LShape: { | ||
context.rect(-4, -4, 8, 16); | ||
context.rect(-4, -4, 24, 8); | ||
break; | ||
} | ||
case ParticleShape.Circle: { | ||
context.arc(0, 0, this.radius, 0, 2 * Math.PI); | ||
break; | ||
} | ||
case ParticleShape.Triangle: { | ||
context.moveTo(16, 4); | ||
context.lineTo(4, 24); | ||
context.lineTo(24, 24); | ||
break; | ||
} | ||
case ParticleShape.QuarterCircle: { | ||
context.arc(4, -4, 4, -Math.PI / 2, 0); | ||
context.lineTo(4, 0); | ||
break; | ||
} | ||
} | ||
|
||
context.closePath(); | ||
context.fill(); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,4 @@ | |
"jsx": "react", | ||
"jsxImportSource": "react" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { expect, test } from '@playwright/test'; | ||
import process from 'process'; | ||
|
||
import { SbPage } from './util'; | ||
|
||
const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001'; | ||
const templateName = process.env.STORYBOOK_TEMPLATE_NAME || ''; | ||
const type = process.env.STORYBOOK_TYPE || 'dev'; | ||
|
||
const supportsOnboarding = | ||
templateName.includes('react') || | ||
templateName.includes('vue3') || | ||
templateName.includes('angular') || | ||
templateName.includes('next'); | ||
|
||
test.describe('addon-onboarding', () => { | ||
test.skip(type === 'build', `Skipping addon tests for production Storybooks`); | ||
test.skip( | ||
!supportsOnboarding, | ||
`Skipping ${templateName}, which does not have addon-onboarding set up.` | ||
); | ||
test('the onboarding flow', async ({ page }) => { | ||
await page.goto(`${storybookUrl}/?path=/onboarding`); | ||
const sbPage = new SbPage(page, expect); | ||
await sbPage.waitUntilLoaded(); | ||
|
||
await expect(page.getByRole('heading', { name: 'Meet your new frontend' })).toBeVisible(); | ||
await page.locator('#storybook-addon-onboarding').getByRole('button').click(); | ||
|
||
await expect(page.getByText('Interactive story playground')).toBeVisible(); | ||
await page.getByLabel('Next').click(); | ||
|
||
await expect(page.getByText('Save your changes as a new')).toBeVisible(); | ||
await page.getByLabel('Next').click(); | ||
|
||
await expect(page.getByRole('heading', { name: 'Create new story' })).toBeVisible(); | ||
await page.getByPlaceholder('Story export name').click(); | ||
|
||
// this is needed because the e2e test will generate a new file in the system | ||
// which we don't know of its location (it runs in different sandboxes) | ||
// so we just create a random id to make it easier to run tests | ||
const id = Math.random().toString(36).substring(7); | ||
await page.getByPlaceholder('Story export name').fill('Test-' + id); | ||
await page.getByRole('button', { name: 'Create' }).click(); | ||
|
||
await expect(page.getByText('You just added your first')).toBeVisible(); | ||
await page.getByLabel('Last').click(); | ||
|
||
await expect( | ||
sbPage.previewIframe().getByRole('heading', { name: 'Configure your project' }) | ||
).toBeVisible(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.