Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(PageFeatures): Allow the PageFeatures component to render in a grid #1312

Merged
merged 2 commits into from
Jan 27, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
64 changes: 39 additions & 25 deletions packages/gamut-labs/src/landingPage/Feature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,38 +36,60 @@ const FeatureBlock = styled.div`
}
`;

type FeaturedMediaProps = {
featuresMedia?: 'image' | 'icon' | 'stat' | 'none';
type FeaturedImageProps = {
featuresMedia: 'image';
imgSrc: string;
imgAlt?: string;
statText?: string;
imgAlt: string;
};

const FeaturedMedia: React.FC<FeaturedMediaProps> = ({
featuresMedia = 'none',
...rest
}) => {
if (featuresMedia === 'image') {
type FeaturedIconProps = {
featuresMedia: 'icon';
imgSrc: string;
imgAlt: string;
};

type FeaturedStatProps = {
featuresMedia: 'stat';
statText: string;
};

type FeaturedNoMediaProps = {
featuresMedia: 'none';
};

type FeaturedMediaProps =
| FeaturedImageProps
| FeaturedIconProps
| FeaturedStatProps
| FeaturedNoMediaProps;

const FeaturedMedia: React.FC<FeaturedMediaProps> = (props) => {
if (props.featuresMedia === 'image') {
return (
<Image src={rest.imgSrc} alt={rest.imgAlt} data-testid="feature-image" />
<Image
src={props.imgSrc}
alt={props.imgAlt}
data-testid="feature-image"
/>
);
}

if (featuresMedia === 'icon') {
if (props.featuresMedia === 'icon') {
return (
<Icon src={rest.imgSrc} alt={rest.imgAlt} data-testid="feature-icon" />
<Icon src={props.imgSrc} alt={props.imgAlt} data-testid="feature-icon" />
);
}

if (featuresMedia === 'stat') {
if (props.featuresMedia === 'stat') {
return (
<Text
as="div"
marginTop={48}
fontSize={{ xs: 44, lg: 64 }}
fontWeight="title"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction in font-weight.

data-testid="feature-stat"
>
{rest.statText}
{props.statText}
</Text>
);
}
Expand All @@ -82,24 +104,16 @@ export type FeatureProps = Pick<
FeaturedMediaProps;

export const Feature: React.FC<FeatureProps> = ({
featuresMedia,
imgSrc,
imgAlt = '',
statText,
title,
desc,
onAnchorClick,
testId,
...featuredMediaProps
}) => (
<FeatureBlock data-testid={testId}>
<FeaturedMedia
featuresMedia={featuresMedia}
imgSrc={imgSrc}
imgAlt={imgAlt}
statText={statText}
/>
<FeaturedMedia {...featuredMediaProps} />
{title && (
<Text as="h3" fontSize={{ xs: 22, lg: 26 }}>
<Text as="h3" fontSize={{ xs: 22, lg: 26 }} fontWeight="title">
{title}
</Text>
)}
Expand Down
85 changes: 71 additions & 14 deletions packages/gamut-labs/src/landingPage/PageFeatures.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Container } from '@codecademy/gamut';
import {
Column,
ColumnSizes,
Container,
LayoutGrid,
ResponsiveProperty,
} from '@codecademy/gamut';
import { mediaQueries } from '@codecademy/gamut-styles';
import styled from '@emotion/styled';
import React from 'react';
import React, { ReactNode } from 'react';

import { CTA, Description, Feature, FeatureProps, Title } from './';
import { BaseProps } from './types';
Expand All @@ -13,6 +19,8 @@ const FlexContainer = styled(Container)`
`;

export type PageFeaturesProps = BaseProps & {
maxCols?: 1 | 2 | 3 | 4;

/**
* Array of features, which consist of image, image alt, title, and description
*/
Expand All @@ -29,10 +37,56 @@ export type PageFeaturesProps = BaseProps & {
featuresMedia?: 'image' | 'icon' | 'stat' | 'none';
};

const rowRenderEach = (
items: FeatureProps[],
itemRenderer: (item: FeatureProps) => ReactNode
): ReactNode => (
<FlexContainer nowrap column>
{items.map(itemRenderer)}
</FlexContainer>
);

const gridRenderEach = (
maxCols: NonNullable<PageFeaturesProps['maxCols']>,
items: FeatureProps[],
itemRenderer: (item: FeatureProps) => ReactNode
): ReactNode => {
const size = { xs: 12, sm: 12 / maxCols } as ResponsiveProperty<ColumnSizes>;
/* eslint-disable react/no-array-index-key */
return (
<LayoutGrid
columnGap={{ lg: 'lg', xs: 'sm' }}
rowGap={{ lg: 'lg', xs: 'sm' }}
>
{items.map((item, i) => (
<Column key={i} size={size}>
{itemRenderer(item)}
</Column>
))}
</LayoutGrid>
);
/* eslint-enable react/no-array-index-key */
};

const renderEach = (
maxCols: PageFeaturesProps['maxCols'],
items: FeatureProps[],
itemRenderer: (item: FeatureProps) => ReactNode
): ReactNode => {
if (maxCols === undefined) {
return rowRenderEach(items, itemRenderer);
}
if (maxCols > 0 && maxCols <= 4) {
return gridRenderEach(maxCols, items, itemRenderer);
}
return null;
};

export const PageFeatures: React.FC<PageFeaturesProps> = ({
title,
desc,
cta,
maxCols,
features,
featuresMedia,
isIcon,
Expand All @@ -49,17 +103,20 @@ export const PageFeatures: React.FC<PageFeaturesProps> = ({
</CTA>
)}
</div>
<FlexContainer nowrap column>
{features.map((feature) => (
<Feature
key={feature.title}
{...feature}
featuresMedia={
featuresMedia ? featuresMedia : isIcon ? 'icon' : 'image'
}
onAnchorClick={onAnchorClick}
/>
))}
</FlexContainer>
{renderEach(
maxCols,
features.map((feature) => ({
...feature,
featuresMedia: featuresMedia
? featuresMedia
: isIcon
? 'icon'
: 'image',
onAnchorClick,
})) as FeatureProps[],
(feature) => (
<Feature key={feature.title} {...feature} />
)
)}
</div>
);
43 changes: 27 additions & 16 deletions packages/gamut-labs/src/landingPage/__tests__/Feature-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,38 @@ import React from 'react';

import { Feature, FeatureProps } from '..';

const renderComponent = (overrides: Partial<FeatureProps> = {}) => {
const props: FeatureProps = {
featuresMedia: 'image',
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
imgAlt: 'Codey Boba Tea',
...overrides,
};

return mount(<Feature {...props} />);
};
const renderComponent = (props: FeatureProps) => mount(<Feature {...props} />);

describe('Feature', () => {
it('renders a title when title prop is provided', () => {
const wrapper = renderComponent({ title: 'Test Title' });
const wrapper = renderComponent({
featuresMedia: 'none',
title: 'Test Title',
});
expect(wrapper.find('h3').text()).toEqual('Test Title');
});

it('does not render a title when title prop is not provided', () => {
const wrapper = renderComponent();
const wrapper = renderComponent({
featuresMedia: 'none',
desc: 'Test Description',
});
expect(wrapper.find('h3')).toHaveLength(0);
});

it('renders a description when desc prop is provided', () => {
const wrapper = renderComponent({ desc: 'Test Description' });
const wrapper = renderComponent({
featuresMedia: 'none',
desc: 'Test Description',
});
expect(wrapper.find('p').text()).toEqual('Test Description');
});

it('does not render a description when desc prop is not provided', () => {
const wrapper = renderComponent();
const wrapper = renderComponent({
featuresMedia: 'none',
title: 'Test Title',
});
expect(wrapper.find('p')).toHaveLength(0);
});

Expand All @@ -43,14 +46,22 @@ describe('Feature', () => {
});

it('renders an image', () => {
const wrapper = renderComponent();
const wrapper = renderComponent({
featuresMedia: 'image',
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
imgAlt: 'Codey Boba Tea',
});
expect(wrapper.find('img[data-testid="feature-image"]')).toHaveLength(1);
expect(wrapper.find('img[data-testid="feature-icon"]')).toHaveLength(0);
expect(wrapper.find('div[data-testid="feature-stat"]')).toHaveLength(0);
});

it('renders an icon', () => {
const wrapper = renderComponent({ featuresMedia: 'icon' });
const wrapper = renderComponent({
featuresMedia: 'icon',
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
imgAlt: 'Codey Boba Tea',
});
expect(wrapper.find('img[data-testid="feature-image"]')).toHaveLength(0);
expect(wrapper.find('img[data-testid="feature-icon"]')).toHaveLength(1);
expect(wrapper.find('div[data-testid="feature-stat"]')).toHaveLength(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,10 @@ describe('PageFeatures', () => {
const wrapper = renderComponent({
features: [
{
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
imgAlt: 'Codey boba tea',
title: 'Software Engineer',
desc: '**Software Engineer**. Example link [here](#).',
},
{
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
imgAlt: 'Codey boba tea',
title: 'Data Scientist',
desc: '**Data Scientist**. Example link [here](#).',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,51 @@ Promote a single stat by setting the `featuresMedia` prop to `stat` and providin
{(args) => <PageFeatures {...args} />}
</Story>
</Canvas>

## Wrap the features to create a grid

If you specify a `maxCols` the features at desktop will wrap at that number of columns

<Canvas>
<Story
name="Features Grid"
args={{
features: [
{
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
imgAlt: 'Codey boba tea',
title: 'Software Engineer',
desc: '**Software Engineer**. Example link [here](#).',
},
{
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
imgAlt: 'Codey boba tea',
title: 'Data Scientist',
desc: '**Data Scientist**. Example link [here](#).',
},
{
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
imgAlt: 'Codey boba tea',
title: 'Product Manager',
desc: '**Product Manager**. Example link [here](#).',
},
{
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
imgAlt: 'Codey boba tea',
title: 'Product Designer',
desc: '**Product Designer**. Example link [here](#).',
},
{
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
imgAlt: 'Codey boba tea',
title: 'Product Designer',
desc: '**Product Designer**. Example link [here](#).',
},
],
featuresMedia: 'icon',
maxCols: 3,
}}
>
{(args) => <PageFeatures {...args} />}
</Story>
</Canvas>