Skip to content

Commit 1b2b263

Browse files
committed
Added base cards
1 parent bca35c4 commit 1b2b263

File tree

11 files changed

+293
-19
lines changed

11 files changed

+293
-19
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
}

__tests__/Card.test.tsx

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

src/components/Card/Card.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
8+
export type CardStatics = {
9+
Content: typeof CardContent;
10+
Header: typeof CardHeader;
11+
}
12+
13+
type CardProps = React.HTMLAttributes<HTMLDivElement>;
14+
15+
// Static properties are not given yet, when declaring the card const. Therefore, the error saying
16+
// that Card is missing above CardStatics properties. These will defined after the card component
17+
// is defined.
18+
// @ts-ignore
19+
const Card: ForwardComponentWithStatics<HTMLDivElement, CardProps, CardStatics> = React.forwardRef((
20+
{
21+
children,
22+
className,
23+
},
24+
ref
25+
): React.ReactElement => (
26+
<div
27+
className={clsx(
28+
'card',
29+
className
30+
)}
31+
ref={ref}
32+
>
33+
{children}
34+
</div>
35+
));
36+
37+
Card.displayName = 'Card';
38+
Card.propTypes = {
39+
className: PropTypes.string,
40+
children: PropTypes.node
41+
};
42+
43+
Card.Content = CardContent;
44+
Card.Header = CardHeader;
45+
46+
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;
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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,14 @@ $danger-button-ghost-active-background: mixins.color('red', 75);
123123
--danger-button-ghost-hover-background: #{$danger-button-ghost-hover-background};
124124
--danger-button-ghost-active-background: #{$danger-button-ghost-active-background};
125125
}
126+
127+
/**
128+
* 6. Cards
129+
*/
130+
$card-padding: 1rem;
131+
$card-background: #fff;
132+
133+
:root {
134+
--card-padding: #{$card-padding};
135+
--card-background: #{$card-background};
136+
}

src/style/components/_card.scss

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
27+
.card-title {
28+
font: {
29+
weight: 600;
30+
size: 1.1rem;
31+
}
32+
}
33+
}

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)