Skip to content

Commit 0465254

Browse files
authored
Merge pull request #13 from machielsdev/feature/cards
Added base cards
2 parents bca35c4 + 8326ab4 commit 0465254

File tree

14 files changed

+313
-34
lines changed

14 files changed

+313
-34
lines changed

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"import/no-dynamic-require": "off",
2424
"global-require": "off",
2525
"quotes": ["error", "single", { "allowTemplateLiterals": true }],
26-
"@typescript-eslint/indent": ["error", 4]
26+
"@typescript-eslint/indent": ["error", 4],
27+
"@typescript-eslint/ban-ts-comment": "off"
2728
}
2829
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
# testing
99
/coverage
10+
/www/src
1011

1112
# production
1213
/build

__tests__/Card.test.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { render } from 'enzyme';
2+
import React from 'react';
3+
import Card from '@/components/Card/Card';
4+
import CardContent from '@/components/Card/Content';
5+
import CardHeader from '@/components/Card/Header';
6+
import { Variant } from '@/components';
7+
8+
describe('Card test', () => {
9+
it('should render card', () => {
10+
const container = render(
11+
<div>
12+
<Card />
13+
</div>
14+
);
15+
16+
expect(container.find('.card').length).toBe(1);
17+
});
18+
19+
it('should render card header', () => {
20+
const container = render(
21+
<div>
22+
<Card>
23+
<Card.Header title="Foo" />
24+
<CardHeader title="Foo" />
25+
Hello world
26+
</Card>
27+
</div>
28+
);
29+
30+
expect(container.find('.card .card-header').length).toBe(2);
31+
});
32+
33+
it('should render card header with actions', () => {
34+
const container = render(
35+
<div>
36+
<Card>
37+
<Card.Header title="Foo" actions={[<div key="1" className="foo"/>, null]} />
38+
Hello world
39+
</Card>
40+
</div>
41+
);
42+
43+
expect(container.find('.card .card-header .card-header-action').length).toBe(1);
44+
});
45+
46+
it('should render card content', () => {
47+
const container = render(
48+
<div>
49+
<Card>
50+
<Card.Content>Content</Card.Content>
51+
<CardContent>Content</CardContent>
52+
</Card>
53+
</div>
54+
);
55+
56+
expect(container.find('.card .card-content').text()).toBe('ContentContent');
57+
});
58+
59+
it('should render variant card ', () => {
60+
const container = render(
61+
<div>
62+
<Card variant={Variant.PRIMARY} />
63+
</div>
64+
);
65+
66+
expect(container.find('.card.card-primary').length).toBe(1);
67+
});
68+
});

src/components/Card/Card.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as React from 'react';
2+
import clsx from 'clsx';
3+
import PropTypes from 'prop-types';
4+
import CardContent from '@/components/Card/Content';
5+
import { ForwardComponentWithStatics } from '@/components/utils/ForwardComponentWithStatics';
6+
import CardHeader from '@/components/Card/Header';
7+
import { Variant } from '@/components';
8+
9+
export type CardStatics = {
10+
Content: typeof CardContent;
11+
Header: typeof CardHeader;
12+
}
13+
14+
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
15+
variant?: Variant | string;
16+
}
17+
18+
// Static properties are not given yet, when declaring the card const. Therefore, the error saying
19+
// that Card is missing above CardStatics properties. These will defined after the card component
20+
// is defined.
21+
// @ts-ignore
22+
const Card: ForwardComponentWithStatics<HTMLDivElement, CardProps, CardStatics> = React.forwardRef((
23+
{
24+
children,
25+
className,
26+
variant
27+
},
28+
ref
29+
): React.ReactElement => (
30+
<div
31+
className={clsx(
32+
'card',
33+
variant && `card-${variant}`,
34+
className
35+
)}
36+
ref={ref}
37+
>
38+
{children}
39+
</div>
40+
));
41+
42+
Card.displayName = 'Card';
43+
Card.propTypes = {
44+
className: PropTypes.string,
45+
children: PropTypes.node,
46+
variant: PropTypes.string
47+
};
48+
49+
Card.Content = CardContent;
50+
Card.Header = CardHeader;
51+
52+
export default Card;

src/components/Card/Content.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as React from 'react';
2+
import clsx from 'clsx';
3+
import PropTypes from 'prop-types';
4+
5+
type CardContentProps = React.HTMLAttributes<HTMLDivElement>;
6+
7+
const CardContent = React.forwardRef<HTMLDivElement, CardContentProps>((
8+
{
9+
children,
10+
className
11+
},
12+
ref
13+
): React.ReactElement => (
14+
<div
15+
ref={ref}
16+
className={clsx(
17+
'card-content',
18+
className
19+
)}
20+
>
21+
{children}
22+
</div>
23+
));
24+
25+
CardContent.displayName = 'CardContent';
26+
CardContent.propTypes = {
27+
children: PropTypes.node,
28+
className: PropTypes.string
29+
};
30+
31+
export default CardContent;

src/components/Card/Header.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as React from 'react';
2+
import PropTypes from 'prop-types';
3+
import clsx from 'clsx';
4+
5+
export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
6+
actions?: Array<React.ReactNode> | React.ReactNode | null;
7+
title?: string;
8+
}
9+
10+
const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>((
11+
{
12+
actions,
13+
children,
14+
className,
15+
title
16+
},
17+
ref
18+
): React.ReactElement => {
19+
actions = React.Children.map<React.ReactNode, React.ReactNode>(
20+
actions,
21+
(child: React.ReactNode) => {
22+
if (React.isValidElement(child)) {
23+
return React.cloneElement(child, {
24+
...child.props,
25+
className: clsx(child.props.className, 'card-header-action')
26+
})
27+
}
28+
29+
return undefined;
30+
}
31+
);
32+
33+
return (
34+
<div
35+
ref={ref}
36+
className={clsx(
37+
'card-header',
38+
className
39+
)}
40+
>
41+
{title && (
42+
<div
43+
className={clsx(
44+
'card-title'
45+
)}
46+
>
47+
{title}
48+
</div>
49+
)}
50+
{actions && (
51+
<div className="card-header-actions">
52+
{actions}
53+
</div>
54+
)}
55+
{children}
56+
</div>
57+
);
58+
});
59+
60+
CardHeader.displayName = 'CardHeader';
61+
CardHeader.propTypes = {
62+
actions: PropTypes.arrayOf(PropTypes.node),
63+
children: PropTypes.node,
64+
className: PropTypes.string,
65+
title: PropTypes.string
66+
}
67+
68+
export default CardHeader;

src/components/Card/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default as Card } from './Card';
2+
export { default as CardContent } from './Content';
3+
export { default as CardHeader } from './Header';

src/components/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
export * from './utils';
1+
export * from './Card';
22
export * from './Page';
33
export * from './Panel';
4+
export * from './utils';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React from 'react';
2+
3+
export type StaticsMap = Record<string, React.ReactNode>;
4+
5+
export type ForwardComponentWithStatics<T, P, U extends StaticsMap> =
6+
React.ForwardRefExoticComponent<React.PropsWithoutRef<P>
7+
& React.RefAttributes<T>>
8+
& U;

src/style/base/_variables.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ $base-gutter: 1rem;
3030
$primary-color: mixins.color('deepPurple', 600);
3131
$primary-color-hover: mixins.color('deepPurple', 700);
3232
$primary-color-active: mixins.color('deepPurple', 800);
33+
$primary-color-light-background: mixins.color('deepPurple', 50);
3334

3435
$secondary-color: mixins.color('red', 600);
3536
$secondary-color-hover: mixins.color('red', 700);
@@ -38,11 +39,13 @@ $secondary-color-active: mixins.color('red', 800);
3839
$danger-color: mixins.color('red', 700);
3940
$danger-color-hover: mixins.color('red', 800);
4041
$danger-color-active: mixins.color('red', 900);
42+
$danger-color-light-background: mixins.color('red', 50);
4143

4244
:root {
4345
--primary-color: #{$primary-color};
4446
--primary-color-hover: #{$primary-color-hover};
4547
--primary-color-active: #{$primary-color-active};
48+
--primary-color-light-background: #{$primary-color-light-background};
4649

4750
--secondary-color: #{$secondary-color};
4851
--secondary-color-hover: #{$secondary-color-hover};
@@ -51,6 +54,7 @@ $danger-color-active: mixins.color('red', 900);
5154
--danger-color: #{$danger-color};
5255
--danger-color-hover: #{$danger-color-hover};
5356
--danger-color-active: #{$danger-color-active};
57+
--danger-color-light-background: #{$danger-color-light-background}
5458
}
5559

5660
/**
@@ -123,3 +127,14 @@ $danger-button-ghost-active-background: mixins.color('red', 75);
123127
--danger-button-ghost-hover-background: #{$danger-button-ghost-hover-background};
124128
--danger-button-ghost-active-background: #{$danger-button-ghost-active-background};
125129
}
130+
131+
/**
132+
* 6. Cards
133+
*/
134+
$card-padding: 1rem;
135+
$card-background: #fff;
136+
137+
:root {
138+
--card-padding: #{$card-padding};
139+
--card-background: #{$card-background};
140+
}

src/style/components/_card.scss

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
@use "elevation";
2+
3+
.card {
4+
@extend .elevated-1;
5+
background: var(--card-background);
6+
border: solid 1px var(--base-border-color);
7+
border-radius: var(--base-border-radius);
8+
9+
.card-content {
10+
padding: var(--card-padding);
11+
}
12+
13+
.card-header {
14+
padding: var(--card-padding);
15+
display: flex;
16+
align-items: center;
17+
18+
.card-header-actions {
19+
margin-left: auto;
20+
21+
> .card-header-action {
22+
margin-left: var(--card-padding);
23+
}
24+
}
25+
26+
& + .card-content {
27+
padding-top: 0;
28+
}
29+
}
30+
31+
.card-title {
32+
font: {
33+
weight: 600;
34+
size: 1.1rem;
35+
}
36+
}
37+
38+
&.card-primary {
39+
border-color: var(--primary-color);
40+
background: var(--primary-color-light-background);
41+
}
42+
43+
&.card-primary-ghost {
44+
border-color: var(--primary-color);
45+
}
46+
47+
&.card-danger {
48+
border-color: var(--danger-color);
49+
background: var(--danger-color-light-background);
50+
}
51+
52+
&.card-danger-ghost {
53+
border-color: var(--danger-color);
54+
}
55+
}

src/style/components/_elevation.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@use "../base/mixins";
2+
3+
@for $i from 1 through 5 {
4+
.elevated-#{$i} {
5+
box-shadow: 0 2px #{$i * 2 + 3}px 0px hsla(hue(mixins.color('gray', 500)), saturation(mixins.color('gray', 500)), lightness(mixins.color('gray', 500)), 0.2);
6+
}
7+
}

src/style/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
@use "components/button";
1414
@use "components/page";
1515
@use "components/panel";
16+
@use "components/card";

0 commit comments

Comments
 (0)