Skip to content

Commit 11f1d20

Browse files
committed
chore: Adds internal style api v2 to item card
1 parent ab62434 commit 11f1d20

9 files changed

Lines changed: 307 additions & 22 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React from 'react';
4+
5+
import { ItemCard, ItemCardProps, SpaceBetween } from '~components';
6+
7+
import { SimplePage } from '../app/templates';
8+
9+
import styles from './style-api-v2.scss';
10+
11+
interface StyledItemCardProps extends ItemCardProps {
12+
rootStyled?: boolean;
13+
headerStyled?: boolean | 'background';
14+
footerStyled?: boolean | 'background';
15+
withContent?: boolean;
16+
}
17+
18+
function StyledItemCard({ rootStyled, headerStyled, footerStyled, withContent = true, ...props }: StyledItemCardProps) {
19+
return (
20+
<ItemCard
21+
header="Card header"
22+
description="Card description"
23+
footer="Card footer"
24+
{...props}
25+
{...{
26+
styleClassNames: {
27+
root: rootStyled ? styles.root : undefined,
28+
header:
29+
headerStyled === 'background' ? styles['header-background'] : headerStyled ? styles.header : undefined,
30+
footer:
31+
footerStyled === 'background' ? styles['footer-background'] : footerStyled ? styles.footer : undefined,
32+
},
33+
}}
34+
>
35+
{withContent ? 'Card content' : null}
36+
</ItemCard>
37+
);
38+
}
39+
40+
export default function () {
41+
return (
42+
<SimplePage title="Item card - Style API v2" screenshotArea={{}}>
43+
<SpaceBetween size="m" direction="horizontal">
44+
<div className={styles.cell}>
45+
<StyledItemCard rootStyled={true} headerStyled={true} footerStyled={true} variant="default" />
46+
</div>
47+
<div className={styles.cell}>
48+
<StyledItemCard rootStyled={true} headerStyled={true} footerStyled={true} variant="embedded" />
49+
</div>
50+
</SpaceBetween>
51+
<SpaceBetween size="m" direction="horizontal">
52+
<div className={styles.cell}>
53+
<StyledItemCard rootStyled={true} />
54+
</div>
55+
<div className={styles.cell}>
56+
<StyledItemCard headerStyled={true} />
57+
</div>
58+
<div className={styles.cell}>
59+
<StyledItemCard footerStyled={true} />
60+
</div>
61+
</SpaceBetween>
62+
<SpaceBetween size="m" direction="horizontal">
63+
<div className={styles.cell}>
64+
<StyledItemCard headerStyled="background" withContent={false} footer={undefined} />
65+
</div>
66+
<div className={styles.cell}>
67+
<StyledItemCard footerStyled="background" withContent={false} header={undefined} description={undefined} />
68+
</div>
69+
</SpaceBetween>
70+
</SimplePage>
71+
);
72+
}

‎pages/item-card/style-api-v2.scss‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
@use '~components/styling/item-card';
7+
@use '../style-api/palette' as palette;
8+
9+
.cell {
10+
inline-size: 260px;
11+
}
12+
13+
.root {
14+
@include item-card.root(
15+
$background: palette.$surface,
16+
$box-shadow: 0 4px 12px palette.$subtle-active,
17+
$border-color: palette.$border-accent,
18+
$border-width: 2px,
19+
$border-radius: 24px
20+
);
21+
}
22+
23+
// Only the block-end side of the header border is visible: it is the divider between the header and the content.
24+
.header {
25+
@include item-card.header($background: palette.$genai-surface, $border-color: palette.$genai, $border-width: 2px);
26+
}
27+
28+
// Only the block-start side of the footer border is visible: it is the divider between the content and the footer.
29+
.footer {
30+
@include item-card.footer($background: palette.$info-surface, $border-color: palette.$info, $border-width: 3px);
31+
}
32+
33+
// Background only, for the header-only and footer-only cards: the background shape shows the corner radii without a divider on the card edge.
34+
.header-background {
35+
@include item-card.header($background: palette.$genai-surface);
36+
}
37+
38+
.footer-background {
39+
@include item-card.footer($background: palette.$info-surface);
40+
}

‎src/item-card/__tests__/__snapshots__/styles.test.tsx.snap‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,5 @@ exports[`getRootStyles handles all possible style configurations 3`] = `
4747
"--awsui-style-item-card-border-radius-8i8exb": "8px",
4848
"--awsui-style-item-card-border-width-default-8i8exb": "1px",
4949
"--awsui-style-item-card-box-shadow-default-8i8exb": "0 1px 3px rgba(0,0,0,0.1)",
50-
"borderRadius": "8px",
5150
}
5251
`;

‎src/item-card/__tests__/card.test.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from 'react';
44
import { fireEvent, render } from '@testing-library/react';
55

66
import Icon from '../../../lib/components/icon';
7+
import customCssProps from '../../../lib/components/internal/generated/custom-css-properties';
78
import ItemCard from '../../../lib/components/item-card';
89
import InternalItemCard from '../../../lib/components/item-card/internal';
910
import createWrapper from '../../../lib/components/test-utils/dom';
@@ -211,7 +212,9 @@ describe('Style API', () => {
211212
</ItemCard>
212213
);
213214

214-
expect(getComputedStyle(itemCard.getElement()).getPropertyValue('border-radius')).toBe('20px');
215+
expect(getComputedStyle(itemCard.getElement()).getPropertyValue(customCssProps.styleItemCardBorderRadius)).toBe(
216+
'20px'
217+
);
215218

216219
const body = itemCard.findContent()!;
217220
expect(getComputedStyle(body.getElement()).getPropertyValue('padding-block')).toBe('30px');
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React from 'react';
4+
import { render } from '@testing-library/react';
5+
6+
import ItemCard, { ItemCardProps } from '../../../lib/components/item-card';
7+
import createWrapper from '../../../lib/components/test-utils/dom';
8+
9+
import styles from '../../../lib/components/item-card/styles.selectors.js';
10+
11+
function renderItemCard(props: ItemCardProps & { styleClassNames?: Record<string, string> } = {}) {
12+
const { container } = render(<ItemCard {...props}>Content</ItemCard>);
13+
return { container, wrapper: createWrapper(container).findItemCard()! };
14+
}
15+
16+
const styleClassNames = { root: 'a', header: 'b', footer: 'c', unknown: 'd' };
17+
18+
describe('ItemCard Style API v2', () => {
19+
test('applies each slot class to its element and ignores unknown keys', () => {
20+
const { container, wrapper } = renderItemCard({ header: 'Header', footer: 'Footer', styleClassNames });
21+
expect(wrapper.getElement()).toHaveClass('a');
22+
expect(wrapper.findByClassName(styles.header)!.getElement()).toHaveClass('b');
23+
expect(wrapper.findFooter()!.getElement()).toHaveClass('c');
24+
expect(container.querySelectorAll('.a, .b, .c')).toHaveLength(3);
25+
expect(container.querySelector('.d')).toBeNull();
26+
});
27+
28+
test.each(['default', 'embedded'] as const)('applies the header class in the %s variant', variant => {
29+
const { container, wrapper } = renderItemCard({ variant, description: 'Description', styleClassNames });
30+
expect(wrapper.findByClassName(styles.header)!.getElement()).toHaveClass('b');
31+
expect(container.querySelectorAll('.b')).toHaveLength(1);
32+
});
33+
34+
test('does not render the header class when the header row is empty', () => {
35+
const { container, wrapper } = renderItemCard({ footer: 'Footer', styleClassNames });
36+
expect(wrapper.findByClassName(styles.header)).toBeNull();
37+
expect(container.querySelector('.b')).toBeNull();
38+
});
39+
40+
test('does not leak the styleClassNames prop to the DOM', () => {
41+
const { container } = renderItemCard({ header: 'Header', footer: 'Footer', styleClassNames });
42+
expect(container.querySelector('[styleClassNames]')).toBeNull();
43+
});
44+
});

‎src/item-card/__tests__/styles.test.tsx‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ describe('getFooterStyles', () => {
117117
expect(getFooterStyles(allStyles)).toMatchSnapshot();
118118
});
119119

120+
test('draws a visible divider when only one of its values is set', () => {
121+
expect(getFooterStyles({ footer: { divider: { borderWidth: '2px' } } })).toEqual({
122+
borderBlockStartStyle: 'solid',
123+
borderBlockStartWidth: '2px',
124+
borderBlockStartColor: 'currentcolor',
125+
});
126+
expect(getFooterStyles({ footer: { divider: { borderColor: 'red' } } })).toEqual({
127+
borderBlockStartStyle: 'solid',
128+
borderBlockStartWidth: 'medium',
129+
borderBlockStartColor: 'red',
130+
});
131+
});
132+
120133
test('returns empty object when SYSTEM is not core', async () => {
121134
jest.resetModules();
122135
jest.doMock('../../internal/environment', () => ({

‎src/item-card/internal.tsx‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ import { getContentStyles, getFooterStyles, getHeaderStyles, getRootStyles } fro
1515
import styles from './styles.css.js';
1616
import testStyles from './test-classes/styles.css.js';
1717

18+
// Style API v2
19+
export interface StyleClassNames {
20+
root?: string;
21+
header?: string;
22+
footer?: string;
23+
}
24+
25+
interface InternalItemCardWithStyleProps extends InternalItemCardProps {
26+
styleClassNames?: StyleClassNames;
27+
}
28+
1829
export default function InternalItemCard({
1930
actions,
2031
highlighted,
@@ -34,8 +45,9 @@ export default function InternalItemCard({
3445
fullHeight,
3546
variant = 'default',
3647
__internalRootRef,
48+
styleClassNames,
3749
...restProps
38-
}: InternalItemCardProps) {
50+
}: InternalItemCardWithStyleProps) {
3951
const baseProps = getBaseProps(restProps);
4052
const isRefresh = useVisualRefresh();
4153
const rootRef = useMergeRefs(__internalRootRef, nativeAttributes?.ref);
@@ -46,6 +58,7 @@ export default function InternalItemCard({
4658
{
4759
className: clsx(
4860
className,
61+
styleClassNames?.root,
4962
styles.root,
5063
highlighted && styles.highlighted,
5164
fullHeight && styles['full-height'],
@@ -66,6 +79,7 @@ export default function InternalItemCard({
6679
{!headerRowEmpty && (
6780
<div
6881
className={clsx(
82+
styleClassNames?.header,
6983
styles.header,
7084
disableHeaderPaddings && styles['no-padding'],
7185
!!actions && styles['with-actions']
@@ -94,7 +108,12 @@ export default function InternalItemCard({
94108
)}
95109
{footer && (
96110
<div
97-
className={clsx(styles.footer, testStyles.footer, disableFooterPaddings && styles['no-padding'])}
111+
className={clsx(
112+
styleClassNames?.footer,
113+
styles.footer,
114+
testStyles.footer,
115+
disableFooterPaddings && styles['no-padding']
116+
)}
98117
style={getFooterStyles(style)}
99118
>
100119
{footer}

‎src/item-card/style.tsx‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ export function getRootStyles(style: ItemCardProps.Style | undefined) {
1111

1212
return {
1313
...(style?.root?.background && { [customCssProps.styleItemCardBackgroundDefault]: style.root.background }),
14-
...(style?.root?.borderRadius && {
15-
[customCssProps.styleItemCardBorderRadius]: style.root.borderRadius,
16-
borderRadius: style.root.borderRadius,
17-
}),
14+
...(style?.root?.borderRadius && { [customCssProps.styleItemCardBorderRadius]: style.root.borderRadius }),
1815
...(style?.root?.borderColor && { [customCssProps.styleItemCardBorderColorDefault]: style.root.borderColor }),
1916
...(style?.root?.borderWidth && { [customCssProps.styleItemCardBorderWidthDefault]: style.root.borderWidth }),
2017
...(style?.root?.boxShadow && { [customCssProps.styleItemCardBoxShadowDefault]: style.root.boxShadow }),
@@ -51,7 +48,12 @@ export function getFooterStyles(style: ItemCardProps.Style | undefined) {
5148
const hasDivider = style?.footer?.divider?.borderColor || style?.footer?.divider?.borderWidth;
5249

5350
return {
54-
...(hasDivider && { borderBlockStartStyle: 'solid' as const }),
51+
// All three longhands, because the footer presets a zero-width transparent border for the Style API divider.
52+
...(hasDivider && {
53+
borderBlockStartStyle: 'solid' as const,
54+
borderBlockStartWidth: 'medium',
55+
borderBlockStartColor: 'currentcolor',
56+
}),
5557
...(style?.footer?.divider?.borderColor && { borderBlockStartColor: style.footer.divider.borderColor }),
5658
...(style?.footer?.divider?.borderWidth && { borderBlockStartWidth: style.footer.divider.borderWidth }),
5759
...(style?.footer?.root?.paddingBlock && { paddingBlock: style.footer.root.paddingBlock }),

0 commit comments

Comments
 (0)