-
Notifications
You must be signed in to change notification settings - Fork 191
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
769e84b
commit 185905e
Showing
16 changed files
with
1,109 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
{ | ||
"tabWidth": 2, | ||
"singleQuote": true | ||
"singleQuote": true, | ||
"trailingComma": "all" | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { render, screen } from '@testing-library/react'; | ||
import { describe, expect, it, vi } from 'vitest'; | ||
import { cbwSvg } from '../../svg/cbwSvg'; | ||
import { QrCodeSvg } from './QrCodeSvg'; | ||
|
||
vi.mock('../../../core-react/internal/hooks/useTheme', () => ({ | ||
useTheme: vi.fn(() => 'default'), | ||
})); | ||
|
||
describe('QRCodeSVG', () => { | ||
it('renders nothing when no value is provided', () => { | ||
render(<QrCodeSvg value="" />); | ||
expect(screen.queryByTitle('QR Code')).toBeNull(); | ||
}); | ||
|
||
it('renders SVG with default props', () => { | ||
render(<QrCodeSvg value="test" />); | ||
|
||
const svg = screen.getByTitle('QR Code'); | ||
|
||
expect(svg).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders with logo', () => { | ||
render(<QrCodeSvg value="test" logo={cbwSvg} />); | ||
|
||
expect(screen.getByTestId('qr-code-logo')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders with linear gradient', () => { | ||
render(<QrCodeSvg value="test" gradientType="linear" />); | ||
|
||
expect(screen.queryByTestId('linearGrad')).toBeInTheDocument(); | ||
expect(screen.queryByTestId('radialGrad')).toBeNull(); | ||
}); | ||
|
||
it('renders with radial gradient', () => { | ||
render(<QrCodeSvg value="test" gradientType="radial" />); | ||
|
||
expect(screen.queryByTestId('radialGrad')).toBeInTheDocument(); | ||
expect(screen.queryByTestId('linearGrad')).not.toBeInTheDocument(); | ||
}); | ||
}); |
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,183 @@ | ||
import { useId, useMemo } from 'react'; | ||
import { useTheme } from '../../../core-react/internal/hooks/useTheme'; | ||
import { | ||
GRADIENT_END_COORDINATES, | ||
GRADIENT_START_COORDINATES, | ||
QR_CODE_SIZE, | ||
QR_LOGO_BACKGROUND_COLOR, | ||
QR_LOGO_RADIUS, | ||
QR_LOGO_SIZE, | ||
linearGradientStops, | ||
ockThemeToLinearGradientColorMap, | ||
ockThemeToRadiamGradientColorMap, | ||
presetGradients, | ||
} from './gradientConstants'; | ||
import { useCorners } from './useCorners'; | ||
import { useDotsPath } from './useDotsPath'; | ||
import { useLogo } from './useLogo'; | ||
import { useMatrix } from './useMatrix'; | ||
|
||
function coordinateAsPercentage(coordinate: number) { | ||
return `${coordinate * 100}%`; | ||
} | ||
|
||
export type QRCodeSVGProps = { | ||
value: string; | ||
size?: number; | ||
backgroundColor?: string; | ||
logo?: React.ReactNode; | ||
logoSize?: number; | ||
logoBackgroundColor?: string; | ||
logoMargin?: number; | ||
logoBorderRadius?: number; | ||
quietZone?: number; | ||
quietZoneBorderRadius?: number; | ||
ecl?: 'L' | 'M' | 'Q' | 'H'; | ||
gradientType?: 'radial' | 'linear'; | ||
}; | ||
|
||
export function QrCodeSvg({ | ||
value, | ||
size = QR_CODE_SIZE, | ||
backgroundColor = '#ffffff', | ||
logo, | ||
logoSize = QR_LOGO_SIZE, | ||
logoBackgroundColor = QR_LOGO_BACKGROUND_COLOR, | ||
logoMargin = 5, | ||
logoBorderRadius = QR_LOGO_RADIUS, | ||
quietZone = 12, | ||
quietZoneBorderRadius = 10, | ||
ecl = 'Q', | ||
gradientType = 'radial', | ||
}: QRCodeSVGProps) { | ||
const gradientRadius = size * 0.55; | ||
const gradientCenterPoint = size / 2; | ||
const uid = useId(); | ||
|
||
const theme = useTheme(); | ||
const themeName = theme.split('-')[0]; | ||
|
||
const isRadialGradient = gradientType === 'radial'; | ||
const fillColor = isRadialGradient ? `url(#radialGrad-${uid})` : '#000000'; | ||
const bgColor = isRadialGradient | ||
? backgroundColor | ||
: `url(#linearGrad-${uid})`; | ||
|
||
const linearGradientColor = | ||
ockThemeToLinearGradientColorMap[ | ||
themeName as keyof typeof ockThemeToLinearGradientColorMap | ||
]; | ||
const linearColors = [ | ||
linearGradientStops[linearGradientColor].startColor, | ||
linearGradientStops[linearGradientColor].endColor, | ||
]; | ||
|
||
const presetGradientForColor = | ||
presetGradients[ | ||
ockThemeToRadiamGradientColorMap[ | ||
themeName as keyof typeof ockThemeToLinearGradientColorMap | ||
] as keyof typeof presetGradients | ||
]; | ||
|
||
const matrix = useMatrix(value, ecl); | ||
const corners = useCorners(size, matrix.length, bgColor, fillColor, uid); | ||
const { x: x1, y: y1 } = GRADIENT_START_COORDINATES; | ||
const { x: x2, y: y2 } = GRADIENT_END_COORDINATES; | ||
|
||
const viewBox = useMemo(() => { | ||
return [ | ||
-quietZone, | ||
-quietZone, | ||
size + quietZone * 2, | ||
size + quietZone * 2, | ||
].join(' '); | ||
}, [quietZone, size]); | ||
|
||
const svgLogo = useLogo({ | ||
size, | ||
logo, | ||
logoSize, | ||
logoBackgroundColor, | ||
logoMargin, | ||
logoBorderRadius, | ||
}); | ||
|
||
const path = useDotsPath({ | ||
matrix, | ||
size, | ||
logoSize, | ||
logoMargin, | ||
logoBorderRadius, | ||
hasLogo: !!logo, | ||
}); | ||
|
||
if (!path || !value) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<svg viewBox={viewBox} width={size} height={size}> | ||
<title>QR Code</title> | ||
<defs> | ||
{isRadialGradient ? ( | ||
<radialGradient | ||
id={`radialGrad-${uid}`} | ||
data-testid="radialGrad" | ||
rx={gradientRadius} | ||
ry={gradientRadius} | ||
cx={gradientCenterPoint} | ||
cy={gradientCenterPoint} | ||
gradientUnits="userSpaceOnUse" | ||
> | ||
{presetGradientForColor.map(([gradientColor, offset]) => ( | ||
<stop | ||
key={`${gradientColor}${offset}`} | ||
offset={offset} | ||
stopColor={gradientColor} | ||
stopOpacity={1} | ||
/> | ||
))} | ||
</radialGradient> | ||
) : ( | ||
<linearGradient | ||
id={`linearGrad-${uid}`} | ||
data-testid="linearGrad" | ||
x1={coordinateAsPercentage(x1)} | ||
y1={coordinateAsPercentage(y1)} | ||
x2={coordinateAsPercentage(x2)} | ||
y2={coordinateAsPercentage(y2)} | ||
gradientUnits="userSpaceOnUse" | ||
> | ||
<stop offset="0" stopColor={linearColors[0]} /> | ||
<stop offset="1" stopColor={linearColors[1]} /> | ||
</linearGradient> | ||
)} | ||
</defs> | ||
<g> | ||
<rect | ||
rx={quietZoneBorderRadius} | ||
ry={quietZoneBorderRadius} | ||
x={-quietZone} | ||
y={-quietZone} | ||
width={size + quietZone * 2} | ||
height={size + quietZone * 2} | ||
fill={backgroundColor} | ||
stroke={bgColor} | ||
strokeWidth={2} | ||
/> | ||
</g> | ||
<g> | ||
<path | ||
d={path} | ||
fill={fillColor} | ||
strokeLinecap="butt" | ||
stroke={fillColor} | ||
strokeWidth={0} | ||
opacity={0.6} | ||
/> | ||
{corners} | ||
{svgLogo} | ||
</g> | ||
</svg> | ||
); | ||
} |
115 changes: 115 additions & 0 deletions
115
src/internal/components/QrCode/gradientConstants.test.ts
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,115 @@ | ||
import { describe, expect, it } from 'vitest'; | ||
import { | ||
GRADIENT_END_COORDINATES, | ||
GRADIENT_END_STYLE, | ||
GRADIENT_START_COORDINATES, | ||
QR_CODE_SIZE, | ||
QR_LOGO_BACKGROUND_COLOR, | ||
QR_LOGO_RADIUS, | ||
QR_LOGO_SIZE, | ||
linearGradientStops, | ||
ockThemeToLinearGradientColorMap, | ||
ockThemeToRadiamGradientColorMap, | ||
presetGradients, | ||
} from './gradientConstants'; | ||
|
||
describe('QR Code Constants', () => { | ||
it('should have correct size constants', () => { | ||
expect(QR_CODE_SIZE).toBe(237); | ||
expect(QR_LOGO_SIZE).toBe(50); | ||
expect(QR_LOGO_RADIUS).toBe(10); | ||
}); | ||
|
||
it('should have correct logo background color', () => { | ||
expect(QR_LOGO_BACKGROUND_COLOR).toBe('#ffffff'); | ||
}); | ||
|
||
it('should have correct gradient coordinates', () => { | ||
expect(GRADIENT_START_COORDINATES).toEqual({ x: 0, y: 0 }); | ||
expect(GRADIENT_END_COORDINATES).toEqual({ x: 1, y: 0 }); | ||
}); | ||
|
||
it('should have correct gradient end style', () => { | ||
expect(GRADIENT_END_STYLE).toEqual({ borderRadius: 32 }); | ||
}); | ||
}); | ||
|
||
describe('Theme Maps', () => { | ||
it('should have correct linear gradient theme mappings', () => { | ||
expect(ockThemeToLinearGradientColorMap).toEqual({ | ||
default: 'blue', | ||
base: 'baseBlue', | ||
cyberpunk: 'pink', | ||
hacker: 'black', | ||
}); | ||
}); | ||
|
||
it('should have correct radial gradient theme mappings', () => { | ||
expect(ockThemeToRadiamGradientColorMap).toEqual({ | ||
default: 'default', | ||
base: 'blue', | ||
cyberpunk: 'magenta', | ||
hacker: 'black', | ||
}); | ||
}); | ||
}); | ||
|
||
describe('Linear Gradient Stops', () => { | ||
it('should have correct blue gradient colors', () => { | ||
expect(linearGradientStops.blue).toEqual({ | ||
startColor: '#266EFF', | ||
endColor: '#45E1E5', | ||
}); | ||
}); | ||
|
||
it('should have correct pink gradient colors', () => { | ||
expect(linearGradientStops.pink).toEqual({ | ||
startColor: '#EE5A67', | ||
endColor: '#CE46BD', | ||
}); | ||
}); | ||
|
||
it('should have all required gradient themes', () => { | ||
const expectedThemes = ['blue', 'pink', 'black', 'baseBlue']; | ||
|
||
for (const theme of expectedThemes) { | ||
expect(linearGradientStops[theme]).toBeDefined(); | ||
expect(linearGradientStops[theme].startColor).toBeDefined(); | ||
expect(linearGradientStops[theme].endColor).toBeDefined(); | ||
} | ||
}); | ||
}); | ||
|
||
describe('Preset Gradients', () => { | ||
it('should have correct default gradient stops', () => { | ||
expect(presetGradients.default).toEqual([ | ||
['#0F27FF', '39.06%'], | ||
['#6100FF', '76.56%'], | ||
['#201F1D', '100%'], | ||
]); | ||
}); | ||
|
||
it('should have all required preset themes', () => { | ||
const expectedPresets = ['default', 'blue', 'magenta', 'black']; | ||
|
||
for (const preset of expectedPresets) { | ||
expect( | ||
presetGradients[preset as keyof typeof presetGradients], | ||
).toBeDefined(); | ||
expect( | ||
Array.isArray(presetGradients[preset as keyof typeof presetGradients]), | ||
).toBe(true); | ||
expect( | ||
presetGradients[preset as keyof typeof presetGradients].length, | ||
).toBe(3); | ||
} | ||
}); | ||
|
||
it('should have valid percentage formats for all gradients', () => { | ||
for (const gradient of Object.values(presetGradients)) { | ||
for (const [_, percentage] of gradient) { | ||
expect(percentage).toMatch(/^\d+(\.\d+)?%$/); | ||
} | ||
} | ||
}); | ||
}); |
Oops, something went wrong.