Skip to content
Permalink
Browse files

feat(card): card component (#145)

  • Loading branch information
GiantRobots authored and SiTaggart committed Nov 14, 2019
1 parent 50aef76 commit 9049c5f8953b8ebee68596fedbcd58f6054ef166
@@ -0,0 +1,147 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Card it should render CardFooter 1`] = `
.emotion-1 {
font-family: 'Whitney SSm A','Whitney SSm B','Helvetica Neue',Helvetica,Arial,sans-serif;
font-size: 0.875rem;
line-height: 1.5rem;
color: #282a2b;
font-weight: 400;
}

.emotion-1 *,
.emotion-1 *::after,
.emotion-1 *::before {
box-sizing: border-box;
}

.emotion-0 {
box-sizing: border-box;
min-width: 0;
border-top-width: 1px;
border-bottom-width: 0;
border-left-width: 0;
border-right-width: 0;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-style: solid;
}

<div
className="emotion-1"
>
<article
className="emotion-0"
>
Just | footer | things
</article>
</div>
`;

exports[`Card it should render children 1`] = `
.emotion-1 {
font-family: 'Whitney SSm A','Whitney SSm B','Helvetica Neue',Helvetica,Arial,sans-serif;
font-size: 0.875rem;
line-height: 1.5rem;
color: #282a2b;
font-weight: 400;
}

.emotion-1 *,
.emotion-1 *::after,
.emotion-1 *::before {
box-sizing: border-box;
}

.emotion-0 {
box-sizing: border-box;
min-width: 0;
border-width: 2px;
border-color: #c8cccf;
border-style: solid;
border-radius: 4px;
padding: 1.25rem;
background-color: #ffffff;
}

<div
className="emotion-1"
>
<article
className="emotion-0"
>
I AM A JEDI!!!!
</article>
</div>
`;

exports[`Card it should render default values 1`] = `
.emotion-1 {
font-family: 'Whitney SSm A','Whitney SSm B','Helvetica Neue',Helvetica,Arial,sans-serif;
font-size: 0.875rem;
line-height: 1.5rem;
color: #282a2b;
font-weight: 400;
}

.emotion-1 *,
.emotion-1 *::after,
.emotion-1 *::before {
box-sizing: border-box;
}

.emotion-0 {
box-sizing: border-box;
min-width: 0;
border-width: 2px;
border-color: #c8cccf;
border-style: solid;
border-radius: 4px;
padding: 1.25rem;
background-color: #ffffff;
}

<div
className="emotion-1"
>
<article
className="emotion-0"
/>
</div>
`;

exports[`Card it should render default values unless overridden by the component 1`] = `
.emotion-1 {
font-family: 'Whitney SSm A','Whitney SSm B','Helvetica Neue',Helvetica,Arial,sans-serif;
font-size: 0.875rem;
line-height: 1.5rem;
color: #282a2b;
font-weight: 400;
}

.emotion-1 *,
.emotion-1 *::after,
.emotion-1 *::before {
box-sizing: border-box;
}

.emotion-0 {
box-sizing: border-box;
min-width: 0;
border-width: 2px;
border-color: #c8cccf;
border-style: solid;
border-radius: 4px;
padding: 1.25rem;
background-color: #ffffff;
padding-bottom: 4.75rem;
}

<div
className="emotion-1"
>
<article
className="emotion-0"
/>
</div>
`;
@@ -0,0 +1,58 @@
import * as React from 'react';
import {Theme} from '@twilio-paste/theme';
import renderer from 'react-test-renderer';
import {Card, CardFooter} from '../src';

describe('Card', () => {
it('it should render default values', (): void => {
const tree = renderer
.create(
<Theme.Provider>
<Card />
</Theme.Provider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

it('it should error with bad propTypes', (): void => {
expect(() => {
renderer.create(
<Theme.Provider>
<Card error="blowitup" />
</Theme.Provider>
);
}).toThrow();
});

it('it should render default values unless overridden by the component', (): void => {
const tree = renderer
.create(
<Theme.Provider>
<Card paddingBottom="space200" />
</Theme.Provider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('it should render children', (): void => {
const tree = renderer
.create(
<Theme.Provider>
<Card>I AM A JEDI!!!!</Card>
</Theme.Provider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('it should render CardFooter', (): void => {
const tree = renderer
.create(
<Theme.Provider>
<CardFooter>Just | footer | things</CardFooter>
</Theme.Provider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
});
@@ -0,0 +1,19 @@
import {cardProps} from '../src';
import {errorOnBadProps} from '../src/utilities';

describe('errorOnBadProps', () => {
it('it should error when incorrect props are passed in', () => {
expect(() => {
errorOnBadProps({stuff: 1}, cardProps);
}).toThrow();
});
it('it should allow aria props', () => {
expect(errorOnBadProps({'aria-nagrande': 1, 'aria-coolstuff': 1}, cardProps)).toEqual(undefined);
});
it('it should allow data props', () => {
expect(errorOnBadProps({'aria-nagrande': 1, 'data-qahook': 1, 'aria-paddingTop': 1}, cardProps)).toEqual(undefined);
});
it('it should allow spacing props', () => {
expect(errorOnBadProps({'aria-nagrande': 1, 'aria-paddingTop': 1}, cardProps)).toEqual(undefined);
});
});
@@ -2,7 +2,7 @@
"name": "@twilio-paste/card",
"version": "1.0.0",
"category": "data display",
"status": "backlog",
"status": "alpha",
"private": true,
"description": "",
"author": "Twilio Inc.",
@@ -26,9 +26,13 @@
},
"peerDependencies": {
"react": ">= 16.0.0",
"react-dom": ">= 16.0.0"
"react-dom": ">= 16.0.0",
"@twilio-paste/box": "^2.0.1",
"@twilio-paste/types": "^2.0.1"
},
"devDependencies": {
"@twilio-paste/box": "^2.0.1",
"@twilio-paste/types": "^2.0.1",
"rollup": "^1.16.2",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-commonjs": "^10.0.1",
@@ -1,25 +1,52 @@
import * as React from 'react';
import {Box} from '@twilio-paste/box';
import {PaddingProps} from '@twilio-paste/types';
import {StaticDiv} from '@twilio-paste/types/src/DomTypes';
import {errorOnBadProps, paddingProps} from './utilities';

interface CardTitleProps {
children: React.ReactNode;
}
const CardTitle: React.FunctionComponent<CardTitleProps> = ({children}) => <h4>{children}</h4>;
interface CardFooterProps extends PaddingProps, StaticDiv {}
const cardFooterProps = paddingProps;

interface CardBodyProps {
children: React.ReactNode;
}
const CardBody: React.FunctionComponent<CardBodyProps> = ({children}) => <div>{children}</div>;
const CardFooter: React.FunctionComponent<CardFooterProps> = ({children, ...attributes}) => {
errorOnBadProps(attributes, cardFooterProps);

interface CardProps {
onClick: () => void;
children: React.ReactNode;
}
return (
<Box
as="article"
borderTopWidth="borderWidth10"
borderBottomWidth="borderWidth0"
borderLeftWidth="borderWidth0"
borderRightWidth="borderWidth0"
marginTop="space40"
paddingTop="space40"
borderStyle="solid"
{...attributes}
>
{children}
</Box>
);
};

const Card: React.FunctionComponent<CardProps> = ({children, onClick}) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-tabindex
<aside onClick={onClick} tabIndex={onClick != null ? 0 : undefined} role={onClick != null ? 'button' : undefined}>
{children}
</aside>
);
interface CardProps extends PaddingProps, StaticDiv {}
const cardProps = paddingProps;

export {Card, CardBody, CardTitle};
const Card: React.FunctionComponent<CardProps> = ({children, ...attributes}) => {
errorOnBadProps(attributes, cardProps);

return (
<Box
as="article"
borderWidth="borderWidth20"
borderColor="colorBorder"
borderStyle="solid"
borderRadius="borderRadius20"
padding="space60"
backgroundColor="colorBackgroundBody"
{...attributes}
>
{children}
</Box>
);
};

export {Card, CardFooter, cardProps, cardFooterProps};
@@ -0,0 +1,14 @@
const paddingProps = ['padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'];

const errorOnBadProps = (props: {[key: string]: any}, propWhitelist: string[]): void => {
// Object.keys might be slow in the hot path
const badProps = Object.keys(props).filter(
prop => !propWhitelist.includes(prop) && !prop.startsWith('aria-') && !prop.startsWith('data-')
);

if (badProps.length > 0) {
throw new Error(`${badProps.join(', ')} are invalid for the card component`);
}
};

export {paddingProps, errorOnBadProps};
@@ -1,11 +1,41 @@
import * as React from 'react';
import {storiesOf} from '@storybook/react';
import {action} from '@storybook/addon-actions';
import {Card, CardTitle, CardBody} from '../src';
import {Heading} from '@twilio-paste/heading';
import {withKnobs, select} from '@storybook/addon-knobs';
import {Text} from '@twilio-paste/text';
import {Padding} from '@twilio-paste/types';
import {DefaultTheme} from '@twilio-paste/theme-tokens';

storiesOf('Components|Card', module).add('Default', () => (
<Card onClick={action('clicked')}>
<CardTitle>Title</CardTitle>
<CardBody>Body</CardBody>
</Card>
));
import {Card, CardFooter} from '../src';

const spaceOptions = Object.keys(DefaultTheme.space);

storiesOf('Components|Card', module)
.addDecorator(withKnobs)
.add('Default', () => (
<Card padding={select('padding', spaceOptions, 'space10') as Padding}>
<Heading as="h2">With a heading</Heading>
<Text>Body</Text>
<CardFooter>
<Text>I&apos;m | The | Footer</Text>
</CardFooter>
</Card>
))
.add('Padding', () => (
<Card padding="space200">
<Heading as="h2">With a heading</Heading>
<Text>Body</Text>
</Card>
))
.add('No Padding', () => (
<Card padding="space0">
<Heading as="h2">With a heading</Heading>
<Text>Body</Text>
</Card>
))
.add('Prop Passthrough', () => (
<Card aria-busy aria-atomic data-qahook="ZeCard!">
<Heading as="h2">With a heading</Heading>
<Text>Body</Text>
</Card>
));

1 comment on commit 9049c5f

@now

This comment has been minimized.

Please sign in to comment.
You can’t perform that action at this time.