Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
72 changes: 72 additions & 0 deletions pages/item-card/style-api-v2.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import { ItemCard, ItemCardProps, SpaceBetween } from '~components';

import { SimplePage } from '../app/templates';

import styles from './style-api-v2.scss';

interface StyledItemCardProps extends ItemCardProps {
rootStyled?: boolean;
headerStyled?: boolean | 'background';
footerStyled?: boolean | 'background';
withContent?: boolean;
}

function StyledItemCard({ rootStyled, headerStyled, footerStyled, withContent = true, ...props }: StyledItemCardProps) {
return (
<ItemCard
header="Card header"
description="Card description"
footer="Card footer"
{...props}
{...{
styleClassNames: {
root: rootStyled ? styles.root : undefined,
header:
headerStyled === 'background' ? styles['header-background'] : headerStyled ? styles.header : undefined,
footer:
footerStyled === 'background' ? styles['footer-background'] : footerStyled ? styles.footer : undefined,
},
}}
>
{withContent ? 'Card content' : null}
</ItemCard>
);
}

export default function () {
return (
<SimplePage title="Item card - Style API v2" screenshotArea={{}}>
<SpaceBetween size="m" direction="horizontal">
<div className={styles.cell}>
<StyledItemCard rootStyled={true} headerStyled={true} footerStyled={true} variant="default" />
</div>
<div className={styles.cell}>
<StyledItemCard rootStyled={true} headerStyled={true} footerStyled={true} variant="embedded" />
</div>
</SpaceBetween>
<SpaceBetween size="m" direction="horizontal">
<div className={styles.cell}>
<StyledItemCard rootStyled={true} />
</div>
<div className={styles.cell}>
<StyledItemCard headerStyled={true} />
</div>
<div className={styles.cell}>
<StyledItemCard footerStyled={true} />
</div>
</SpaceBetween>
<SpaceBetween size="m" direction="horizontal">
<div className={styles.cell}>
<StyledItemCard headerStyled="background" withContent={false} footer={undefined} />
</div>
<div className={styles.cell}>
<StyledItemCard footerStyled="background" withContent={false} header={undefined} description={undefined} />
</div>
</SpaceBetween>
</SimplePage>
);
}
37 changes: 37 additions & 0 deletions pages/item-card/style-api-v2.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

@use '~components/styling/item-card';
@use '../style-api/palette' as palette;

.cell {
inline-size: 260px;
}

.root {
@include item-card.root(
$background: palette.$surface,
$box-shadow: 0 4px 12px palette.$subtle-active,
$border-color: palette.$border-accent,
$border-width: 2px,
$border-radius: 24px
);
}

.header {
@include item-card.header($background: palette.$genai-surface, $border-color: palette.$genai, $border-width: 2px);
}

.footer {
@include item-card.footer($background: palette.$info-surface, $border-color: palette.$info, $border-width: 3px);
}

.header-background {
@include item-card.header($background: palette.$genai-surface);
}

.footer-background {
@include item-card.footer($background: palette.$info-surface);
}
44 changes: 44 additions & 0 deletions src/item-card/__tests__/style-api.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { render } from '@testing-library/react';

import ItemCard, { ItemCardProps } from '../../../lib/components/item-card';
import createWrapper from '../../../lib/components/test-utils/dom';

import styles from '../../../lib/components/item-card/styles.selectors.js';

function renderItemCard(props: ItemCardProps & { styleClassNames?: Record<string, string> } = {}) {
const { container } = render(<ItemCard {...props}>Content</ItemCard>);
return { container, wrapper: createWrapper(container).findItemCard()! };
}

const styleClassNames = { root: 'a', header: 'b', footer: 'c', unknown: 'd' };

describe('ItemCard Style API v2', () => {
test('applies each slot class to its element and ignores unknown keys', () => {
const { container, wrapper } = renderItemCard({ header: 'Header', footer: 'Footer', styleClassNames });
expect(wrapper.getElement()).toHaveClass('a');
expect(wrapper.findByClassName(styles.header)!.getElement()).toHaveClass('b');
expect(wrapper.findFooter()!.getElement()).toHaveClass('c');
expect(container.querySelectorAll('.a, .b, .c')).toHaveLength(3);
expect(container.querySelector('.d')).toBeNull();
});

test.each(['default', 'embedded'] as const)('applies the header class in the %s variant', variant => {
const { container, wrapper } = renderItemCard({ variant, description: 'Description', styleClassNames });
expect(wrapper.findByClassName(styles.header)!.getElement()).toHaveClass('b');
expect(container.querySelectorAll('.b')).toHaveLength(1);
});

test('does not render the header class when the header row is empty', () => {
const { container, wrapper } = renderItemCard({ footer: 'Footer', styleClassNames });
expect(wrapper.findByClassName(styles.header)).toBeNull();
expect(container.querySelector('.b')).toBeNull();
});

test('does not leak the styleClassNames prop to the DOM', () => {
const { container } = renderItemCard({ header: 'Header', footer: 'Footer', styleClassNames });
expect(container.querySelector('[styleClassNames]')).toBeNull();
});
});
13 changes: 13 additions & 0 deletions src/item-card/__tests__/styles.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,19 @@ describe('getFooterStyles', () => {
expect(getFooterStyles(allStyles)).toMatchSnapshot();
});

test('draws a visible divider when only one of its values is set', () => {
expect(getFooterStyles({ footer: { divider: { borderWidth: '2px' } } })).toEqual({
borderBlockStartStyle: 'solid',
borderBlockStartWidth: '2px',
borderBlockStartColor: 'currentcolor',
});
expect(getFooterStyles({ footer: { divider: { borderColor: 'red' } } })).toEqual({
borderBlockStartStyle: 'solid',
borderBlockStartWidth: 'medium',
borderBlockStartColor: 'red',
});
});

test('returns empty object when SYSTEM is not core', async () => {
jest.resetModules();
jest.doMock('../../internal/environment', () => ({
Expand Down
23 changes: 21 additions & 2 deletions src/item-card/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ import { getContentStyles, getFooterStyles, getHeaderStyles, getRootStyles } fro
import styles from './styles.css.js';
import testStyles from './test-classes/styles.css.js';

// Style API v2
export interface StyleClassNames {
root?: string;
header?: string;
footer?: string;
}

interface InternalItemCardWithStyleProps extends InternalItemCardProps {
styleClassNames?: StyleClassNames;
}

export default function InternalItemCard({
actions,
highlighted,
Expand All @@ -34,8 +45,9 @@ export default function InternalItemCard({
fullHeight,
variant = 'default',
__internalRootRef,
styleClassNames,
...restProps
}: InternalItemCardProps) {
}: InternalItemCardWithStyleProps) {
const baseProps = getBaseProps(restProps);
const isRefresh = useVisualRefresh();
const rootRef = useMergeRefs(__internalRootRef, nativeAttributes?.ref);
Expand All @@ -46,6 +58,7 @@ export default function InternalItemCard({
{
className: clsx(
className,
styleClassNames?.root,
styles.root,
highlighted && styles.highlighted,
fullHeight && styles['full-height'],
Expand All @@ -66,6 +79,7 @@ export default function InternalItemCard({
{!headerRowEmpty && (
<div
className={clsx(
styleClassNames?.header,
styles.header,
disableHeaderPaddings && styles['no-padding'],
!!actions && styles['with-actions']
Expand Down Expand Up @@ -94,7 +108,12 @@ export default function InternalItemCard({
)}
{footer && (
<div
className={clsx(styles.footer, testStyles.footer, disableFooterPaddings && styles['no-padding'])}
className={clsx(
styleClassNames?.footer,
styles.footer,
testStyles.footer,
disableFooterPaddings && styles['no-padding']
)}
style={getFooterStyles(style)}
>
{footer}
Expand Down
7 changes: 6 additions & 1 deletion src/item-card/style.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ export function getFooterStyles(style: ItemCardProps.Style | undefined) {
const hasDivider = style?.footer?.divider?.borderColor || style?.footer?.divider?.borderWidth;

return {
...(hasDivider && { borderBlockStartStyle: 'solid' as const }),
// All three longhands: the footer presets a zero-width transparent border.
...(hasDivider && {
borderBlockStartStyle: 'solid' as const,
borderBlockStartWidth: 'medium',
borderBlockStartColor: 'currentcolor',
}),
...(style?.footer?.divider?.borderColor && { borderBlockStartColor: style.footer.divider.borderColor }),
...(style?.footer?.divider?.borderWidth && { borderBlockStartWidth: style.footer.divider.borderWidth }),
...(style?.footer?.root?.paddingBlock && { paddingBlock: style.footer.root.paddingBlock }),
Expand Down
Loading
Loading