Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .nx/version-plans/version-plan-1791834654983.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
gamut: patch
---

Fix Card border radius to align to theme values
42 changes: 36 additions & 6 deletions packages/gamut/src/Card/__tests__/Card.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { theme } from '@codecademy/gamut-styles';
import {
coreTheme,
GamutProvider,
lxStudioTheme,
percipioTheme,
theme,
} from '@codecademy/gamut-styles';
import { MockGamutProvider, setupRtl } from '@codecademy/gamut-tests';
import { renderHook } from '@testing-library/react';

Expand Down Expand Up @@ -37,7 +43,7 @@ describe('Card', () => {

describe('useCardElevation', () => {
it('maps the theme elevation tokens onto the motion variants', () => {
const { animate, initial } = renderElevation('none');
const { animate, initial } = renderElevation('none', 'md');

expect(initial).toMatchObject({
boxShadow: theme.elevation['rest-shadow'],
Expand All @@ -50,7 +56,7 @@ describe('useCardElevation', () => {
});

it('uses the mirrored hover tokens for patternRight shadows', () => {
const { animate } = renderElevation('patternRight');
const { animate } = renderElevation('patternRight', 'md');

expect(animate).toMatchObject({
boxShadow: theme.elevation['hoverMirrored-shadow'],
Expand All @@ -59,7 +65,7 @@ describe('useCardElevation', () => {
});

it('applies outline shadows but shares the elevation transforms', () => {
const { animateOutline, initialOutline } = renderElevation('outline');
const { animateOutline, initialOutline } = renderElevation('outline', 'md');

expect(initialOutline.boxShadow).not.toBe(theme.elevation['rest-shadow']);
expect(initialOutline.transform).toBe(theme.elevation['rest-transform']);
Expand All @@ -68,10 +74,34 @@ describe('useCardElevation', () => {
});

it('passes borderRadius through to every variant', () => {
const variants = renderElevation('none', '8px');
const variants = renderElevation('none', 'lg');

Object.values(variants).forEach((variant) => {
expect(variant.borderRadius).toBe('8px');
expect(variant.borderRadius).toBe(theme.borderRadii.lg);
});
});

it.each([
['coreTheme', coreTheme],
['percipioTheme', percipioTheme],
['lxStudioTheme', lxStudioTheme],
])('resolves borderRadius from %s', (_name, activeTheme) => {
(['sm', 'md', 'lg'] as const).forEach((token) => {
const { result } = renderHook(() => useCardElevation('none', token), {
wrapper: ({ children }) => (
<GamutProvider
theme={activeTheme}
useCache={false}
useGlobals={false}
>
{children}
</GamutProvider>
),
});

expect(result.current.initial.borderRadius).toBe(
activeTheme.borderRadii[token]
);
});
});
});
11 changes: 3 additions & 8 deletions packages/gamut/src/Card/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { CheckerDense } from '@codecademy/gamut-patterns';
import { borderRadii, Colors } from '@codecademy/gamut-styles';
import { Colors } from '@codecademy/gamut-styles';
import * as React from 'react';

import { DynamicCardWrapper, MotionBox, StaticCardWrapper } from './elements';
import { patternFadeInOut, useCardElevation } from './styles';
import { CardProps } from './types';

type BorderRadiusToken = keyof typeof borderRadii;

export const Card: React.FC<CardProps> = ({
children,
variant = 'default',
Expand All @@ -19,18 +17,15 @@ export const Card: React.FC<CardProps> = ({
height = '100%',
...rest
}) => {
const defaultBorderRadius: BorderRadiusToken = isInteractive ? 'md' : 'none';
const trueBorderRadius = borderRadius ?? defaultBorderRadius;
const resolvedBorderRadius =
borderRadii[trueBorderRadius as BorderRadiusToken];
const trueBorderRadius = borderRadius ?? (isInteractive ? 'md' : 'none');

const SelectedWrapper =
variant === 'default' ? DynamicCardWrapper : StaticCardWrapper;

const hasPattern = shadow === 'patternLeft' || shadow === 'patternRight';
const isOutline = shadow === 'outline';

const setHoverShadow = useCardElevation(shadow, resolvedBorderRadius);
const setHoverShadow = useCardElevation(shadow, trueBorderRadius);

const initialVariant = isOutline ? 'initialOutline' : 'initial';
const animateVariant = isOutline ? 'animateOutline' : 'animate';
Expand Down
21 changes: 15 additions & 6 deletions packages/gamut/src/Card/styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
import { StyleProps } from '@codecademy/variance';
import { Theme, useTheme } from '@emotion/react';

import type { CardWrapperProps } from './types';

/**
* The theme's elevation scale is flattened to dashcase tokens (`rest-shadow`,
* `hoverMirrored-transform`, ...), so per-state groups don't exist on the
Expand Down Expand Up @@ -94,34 +96,41 @@ export const patternFadeInOut = {

export const useCardElevation = (
shadow: StyleProps<typeof shadowVariants>['shadow'],
borderRadius?: string
borderRadius: CardWrapperProps['borderRadius']
) => {
const { elevation } = useTheme();
const { borderRadii, elevation } = useTheme();
const hoverState: ElevationState =
shadow === 'patternRight' ? 'hoverMirrored' : 'hover';

// framer-motion can only animate a concrete value, so responsive or
// function-valued radii are not resolved
const resolvedBorderRadius =
typeof borderRadius === 'string'
? borderRadii[borderRadius as keyof Theme['borderRadii']] ?? borderRadius
: undefined;

return {
initial: {
...getElevationStyles(elevation, 'rest'),
borderRadius,
borderRadius: resolvedBorderRadius,
transition: REST_TRANSITION,
},
animate: {
...getElevationStyles(elevation, hoverState),
borderRadius,
borderRadius: resolvedBorderRadius,
transition: HOVER_TRANSITION,
},
// Outline styles are specific to Codecademy and will not be included in a theme
initialOutline: {
...getElevationStyles(elevation, 'rest'),
boxShadow: `-${SHADOW_OFFSET_INITIAL}px ${SHADOW_OFFSET_INITIAL}px 0 0px ${theme.colors['background-current']}, -${SHADOW_OFFSET_INITIAL}px ${SHADOW_OFFSET_INITIAL}px 0 1px ${theme.colors['border-primary']}`,
borderRadius,
borderRadius: resolvedBorderRadius,
transition: REST_TRANSITION,
},
animateOutline: {
...getElevationStyles(elevation, 'hover'),
boxShadow: `-${SHADOW_OFFSET}px ${SHADOW_OFFSET}px 0 0px ${theme.colors['shadow-primary']}, -${SHADOW_OFFSET}px ${SHADOW_OFFSET}px 0 1px ${theme.colors['shadow-primary']}`,
borderRadius,
borderRadius: resolvedBorderRadius,
transition: HOVER_TRANSITION,
},
};
Expand Down
Loading