Skip to content

Commit dbd52f6

Browse files
committed
chore: Refactor card into separate internal component
1 parent c02fc0d commit dbd52f6

File tree

6 files changed

+255
-134
lines changed

6 files changed

+255
-134
lines changed

src/cards/index.tsx

Lines changed: 52 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { InternalContainerAsSubstep } from '../container/internal';
1212
import { useInternalI18n } from '../i18n/context';
1313
import { AnalyticsFunnelSubStep } from '../internal/analytics/components/analytics-funnel';
1414
import { getBaseProps } from '../internal/base-component';
15+
import Card from '../internal/card';
1516
import { CollectionLabelContext } from '../internal/context/collection-label-context';
1617
import { LinkDefaultVariantContext } from '../internal/context/link-default-variant-context';
1718
import useBaseComponent from '../internal/hooks/use-base-component';
@@ -269,7 +270,6 @@ const CardsList = <T,>({
269270
}) => {
270271
const selectable = !!selectionType;
271272
const canClickEntireCard = selectable && entireCardClickable;
272-
const isRefresh = useVisualRefresh();
273273

274274
const { moveFocusDown, moveFocusUp } = useSelectionFocusMove(selectionType, items.length);
275275

@@ -315,60 +315,61 @@ const CardsList = <T,>({
315315
},
316316
};
317317
return (
318-
<li
319-
className={clsx(styles.card, {
320-
[styles['card-selectable']]: selectable,
321-
[styles['card-selected']]: selectable && selected,
322-
})}
323-
key={key}
318+
<Card
319+
action={
320+
selectionProps && (
321+
<div
322+
className={styles['selection-control']}
323+
{...(!canClickEntireCard && !disabled
324+
? getAnalyticsMetadataAttribute(selectionAnalyticsMetadata)
325+
: {})}
326+
>
327+
<SelectionControl onFocusDown={moveFocusDown} onFocusUp={moveFocusUp} {...selectionProps} />
328+
</div>
329+
)
330+
}
331+
active={selectable && selected}
332+
className={styles.card}
333+
header={
334+
<div className={clsx(styles['card-header'], analyticsSelectors['card-header'])}>
335+
{cardDefinition.header ? cardDefinition.header(item) : ''}
336+
</div>
337+
}
338+
innerMetadataAttributes={
339+
entireCardClickable && !disabled ? getAnalyticsMetadataAttribute(selectionAnalyticsMetadata) : {}
340+
}
341+
key={index}
342+
metadataAttributes={{
343+
...getAnalyticsMetadataAttribute({
344+
component: {
345+
innerContext: {
346+
position: `${index + 1}`,
347+
item: `${key}`,
348+
},
349+
},
350+
}),
351+
...focusMarkers.item,
352+
}}
353+
onClick={
354+
canClickEntireCard
355+
? event => {
356+
selectionProps?.onChange();
357+
// Manually move focus to the native input (checkbox or radio button)
358+
event.currentTarget.querySelector('input')?.focus();
359+
}
360+
: undefined
361+
}
324362
onFocus={onFocus}
325-
{...(focusMarkers && focusMarkers.item)}
326363
role={listItemRole}
327-
{...getAnalyticsMetadataAttribute({
328-
component: {
329-
innerContext: {
330-
position: `${index + 1}`,
331-
item: `${key}`,
332-
},
333-
},
334-
})}
364+
TagName="li"
335365
>
336-
<div
337-
className={clsx(styles['card-inner'], isRefresh && styles.refresh)}
338-
{...(canClickEntireCard && !disabled ? getAnalyticsMetadataAttribute(selectionAnalyticsMetadata) : {})}
339-
onClick={
340-
canClickEntireCard
341-
? event => {
342-
selectionProps?.onChange();
343-
// Manually move focus to the native input (checkbox or radio button)
344-
event.currentTarget.querySelector('input')?.focus();
345-
}
346-
: undefined
347-
}
348-
>
349-
<div className={styles['card-header']}>
350-
<div className={clsx(styles['card-header-inner'], analyticsSelectors['card-header'])}>
351-
{cardDefinition.header ? cardDefinition.header(item) : ''}
352-
</div>
353-
{selectionProps && (
354-
<div
355-
className={styles['selection-control']}
356-
{...(!canClickEntireCard && !disabled
357-
? getAnalyticsMetadataAttribute(selectionAnalyticsMetadata)
358-
: {})}
359-
>
360-
<SelectionControl onFocusDown={moveFocusDown} onFocusUp={moveFocusUp} {...selectionProps} />
361-
</div>
362-
)}
366+
{visibleSectionsDefinition.map(({ width = 100, header, content, id }, index) => (
367+
<div key={id || index} className={styles.section} style={{ width: `${width}%` }}>
368+
{header ? <div className={styles['section-header']}>{header}</div> : ''}
369+
{content ? <div className={styles['section-content']}>{content(item)}</div> : ''}
363370
</div>
364-
{visibleSectionsDefinition.map(({ width = 100, header, content, id }, index) => (
365-
<div key={id || index} className={styles.section} style={{ width: `${width}%` }}>
366-
{header ? <div className={styles['section-header']}>{header}</div> : ''}
367-
{content ? <div className={styles['section-content']}>{content(item)}</div> : ''}
368-
</div>
369-
))}
370-
</div>
371-
</li>
371+
))}
372+
</Card>
372373
);
373374
})}
374375
</ol>

src/cards/styles.scss

Lines changed: 2 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -7,47 +7,9 @@
77

88
@use '../internal/styles' as styles;
99
@use '../internal/styles/tokens' as awsui;
10-
@use '../container/shared' as container;
11-
@use './motion';
12-
13-
@mixin card-style {
14-
border-start-start-radius: awsui.$border-radius-container;
15-
border-start-end-radius: awsui.$border-radius-container;
16-
border-end-start-radius: awsui.$border-radius-container;
17-
border-end-end-radius: awsui.$border-radius-container;
18-
box-sizing: border-box;
19-
20-
&::before {
21-
@include styles.base-pseudo-element;
22-
// Reset border color to prevent it from flashing black during card selection animation
23-
border-color: transparent;
24-
border-block-start: awsui.$border-container-top-width solid awsui.$color-border-container-top;
25-
border-start-start-radius: awsui.$border-radius-container;
26-
border-start-end-radius: awsui.$border-radius-container;
27-
border-end-start-radius: awsui.$border-radius-container;
28-
border-end-end-radius: awsui.$border-radius-container;
29-
z-index: 1;
30-
}
31-
32-
&::after {
33-
@include styles.base-pseudo-element;
34-
border-start-start-radius: awsui.$border-radius-container;
35-
border-start-end-radius: awsui.$border-radius-container;
36-
border-end-start-radius: awsui.$border-radius-container;
37-
border-end-end-radius: awsui.$border-radius-container;
38-
}
39-
&:not(.refresh)::after {
40-
box-shadow: awsui.$shadow-container;
41-
}
42-
&.refresh::after {
43-
border-block: solid awsui.$border-divider-section-width awsui.$color-border-divider-default;
44-
border-inline: solid awsui.$border-divider-section-width awsui.$color-border-divider-default;
45-
}
46-
}
4710

4811
.root {
4912
@include styles.styles-reset();
50-
@include styles.default-text-style;
5113
}
5214

5315
.header {
@@ -111,49 +73,8 @@
11173
}
11274
}
11375

114-
.card {
115-
display: flex;
116-
overflow-wrap: break-word;
117-
word-wrap: break-word;
118-
margin-block: 0;
119-
margin-inline: 0;
120-
padding-block: 0;
121-
padding-inline: 0;
122-
list-style: none;
123-
&-inner {
124-
position: relative;
125-
background-color: awsui.$color-background-container-content;
126-
margin-block-start: 0;
127-
margin-block-end: awsui.$space-grid-gutter;
128-
margin-inline-start: awsui.$space-grid-gutter;
129-
margin-inline-end: 0;
130-
padding-block: awsui.$space-card-vertical;
131-
padding-inline: awsui.$space-card-horizontal;
132-
inline-size: 100%;
133-
min-inline-size: 0;
134-
@include card-style;
135-
}
136-
&-header {
137-
@include styles.font-heading-m;
138-
&-inner {
139-
inline-size: 100%;
140-
display: inline-block;
141-
}
142-
}
143-
&-selectable {
144-
> .card-inner > .card-header {
145-
inline-size: 90%;
146-
}
147-
}
148-
&-selected {
149-
> .card-inner {
150-
background-color: awsui.$color-background-item-selected;
151-
&::before {
152-
border-block: awsui.$border-item-width solid awsui.$color-border-item-selected;
153-
border-inline: awsui.$border-item-width solid awsui.$color-border-item-selected;
154-
}
155-
}
156-
}
76+
.card-header {
77+
@include styles.font-heading-m;
15778
}
15879

15980
.section {

src/internal/card/index.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 clsx from 'clsx';
5+
6+
import { useVisualRefresh } from '../hooks/use-visual-mode';
7+
import { InternalCardProps } from './interfaces';
8+
9+
import styles from './styles.css.js';
10+
11+
export default function Card({
12+
action,
13+
active,
14+
children,
15+
className,
16+
header,
17+
innerMetadataAttributes,
18+
metadataAttributes,
19+
onClick,
20+
onFocus,
21+
role,
22+
TagName = 'div',
23+
}: InternalCardProps) {
24+
const isRefresh = useVisualRefresh();
25+
26+
return (
27+
<TagName
28+
className={clsx(className, styles.card, styles.root, {
29+
[styles['card-with-action']]: !!action,
30+
[styles['card-active']]: active,
31+
})}
32+
onFocus={onFocus}
33+
role={role}
34+
{...metadataAttributes}
35+
>
36+
<div
37+
className={clsx(styles['card-inner'], isRefresh && styles.refresh)}
38+
{...innerMetadataAttributes}
39+
onClick={onClick}
40+
>
41+
<div className={styles['card-header']}>
42+
{header}
43+
{action && <div className={styles.action}>{action}</div>}
44+
</div>
45+
{children}
46+
</div>
47+
</TagName>
48+
);
49+
}

src/internal/card/interfaces.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React, { FocusEventHandler } from 'react';
4+
5+
import { BaseComponentProps } from '../base-component';
6+
7+
export interface InternalCardProps extends BaseComponentProps {
8+
/**
9+
* Specifies an action for the card.
10+
* It is recommended to use a button with inline-icon variant.
11+
*/
12+
action?: React.ReactNode;
13+
14+
/**
15+
* Specifies whether the card is in active state.
16+
*/
17+
active?: boolean;
18+
19+
/**
20+
* Optional URL for an image which will be displayed cropped as a background of the card.
21+
* When this property is used, a dark gradient is overlayed and the text above defaults to bright colors.
22+
* Make sure that any content you place on the card has sufficient contrast with the overlayed image behind.
23+
*/
24+
imageUrl?: string;
25+
26+
/**
27+
* Primary content displayed in the card.
28+
*/
29+
children?: React.ReactNode;
30+
31+
/**
32+
* Heading text.
33+
*/
34+
header?: React.ReactNode;
35+
36+
/**
37+
* Icon which will be displayed at the top of the card,
38+
* inline at the start of the content.
39+
*/
40+
icon?: React.ReactNode;
41+
42+
/**
43+
* Called when the user clicks on the card.
44+
*/
45+
onClick?: React.MouseEventHandler<HTMLElement>;
46+
47+
onFocus?: FocusEventHandler<HTMLElement>;
48+
49+
role?: string;
50+
51+
TagName?: 'li' | 'div';
52+
53+
metadataAttributes: Record<string, string | undefined>;
54+
55+
innerMetadataAttributes: Record<string, string | undefined>;
56+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
@use '../internal/styles' as styles;
7-
@use '../internal/styles/tokens' as awsui;
6+
@use '../styles' as styles;
7+
@use '../styles/tokens' as awsui;
88

99
.card-inner {
1010
@include styles.with-motion {

0 commit comments

Comments
 (0)