diff --git a/.nx/version-plans/version-plan-1791834654983.md b/.nx/version-plans/version-plan-1791834654983.md new file mode 100644 index 00000000000..91f7abe2b34 --- /dev/null +++ b/.nx/version-plans/version-plan-1791834654983.md @@ -0,0 +1,5 @@ +--- +gamut: patch +--- + +Fix Card border radius to align to theme values diff --git a/packages/gamut/src/Card/__tests__/Card.test.tsx b/packages/gamut/src/Card/__tests__/Card.test.tsx index 2016e097d01..a6242008ef0 100644 --- a/packages/gamut/src/Card/__tests__/Card.test.tsx +++ b/packages/gamut/src/Card/__tests__/Card.test.tsx @@ -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'; @@ -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'], @@ -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'], @@ -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']); @@ -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 }) => ( + + {children} + + ), + }); + + expect(result.current.initial.borderRadius).toBe( + activeTheme.borderRadii[token] + ); }); }); }); diff --git a/packages/gamut/src/Card/index.tsx b/packages/gamut/src/Card/index.tsx index 413d80b269e..880ffd08bea 100644 --- a/packages/gamut/src/Card/index.tsx +++ b/packages/gamut/src/Card/index.tsx @@ -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 = ({ children, variant = 'default', @@ -19,10 +17,7 @@ export const Card: React.FC = ({ 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; @@ -30,7 +25,7 @@ export const Card: React.FC = ({ 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'; diff --git a/packages/gamut/src/Card/styles.tsx b/packages/gamut/src/Card/styles.tsx index 2a32935f8ab..32c7f6fc7b9 100644 --- a/packages/gamut/src/Card/styles.tsx +++ b/packages/gamut/src/Card/styles.tsx @@ -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 @@ -94,34 +96,41 @@ export const patternFadeInOut = { export const useCardElevation = ( shadow: StyleProps['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, }, };