Skip to content

Commit c4013da

Browse files
committed
Action card
1 parent 06dbb81 commit c4013da

File tree

7 files changed

+184
-46
lines changed

7 files changed

+184
-46
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React, { useState } from 'react';
4+
5+
import Card from '~components/internal/components/card';
6+
7+
import { CardPage } from './common';
8+
9+
export default function ButtonsScenario() {
10+
const [isActive, setActive] = useState(false);
11+
return (
12+
<CardPage title="Action card: action in chat">
13+
<Card header="Give EC2 access to S3" variant="action" active={isActive} onClick={() => setActive(true)}>
14+
A more detailed description of the action.
15+
</Card>
16+
</CardPage>
17+
);
18+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React, { useState } from 'react';
4+
5+
import Box from '~components/box';
6+
import Container from '~components/container';
7+
import Header from '~components/header';
8+
import Card from '~components/internal/components/card';
9+
10+
import ScreenshotArea from '../utils/screenshot-area';
11+
12+
const getRandomId = () => Math.floor(Math.random() * 1e12).toString();
13+
14+
const accounts = [
15+
{
16+
accountAlias: 'Account alias',
17+
accountId: getRandomId(),
18+
role: 'Dev',
19+
20+
lastLogin: '1 minute ago',
21+
},
22+
{
23+
accountId: getRandomId(),
24+
role: 'ReadOnly',
25+
26+
lastLogin: '10 minute ago',
27+
},
28+
{
29+
accountId: getRandomId(),
30+
role: 'Root',
31+
lastLogin: '2 hours ago',
32+
},
33+
{
34+
accountAlias: 'acme-staging-infra',
35+
accountId: getRandomId(),
36+
role: 'Admin',
37+
38+
lastLogin: '3 hours ago',
39+
},
40+
{
41+
accountAlias: 'acme-prod-monitoring',
42+
accountId: getRandomId(),
43+
role: 'PowerUser',
44+
45+
lastLogin: '10 hours ago',
46+
},
47+
];
48+
49+
export default function ButtonsScenario() {
50+
const [activeId, setActiveId] = useState<string>();
51+
return (
52+
<article>
53+
<h1>Action card: list selection</h1>
54+
<ScreenshotArea>
55+
<div style={{ inlineSize: 550, marginInline: 'auto' }}>
56+
<Container header={<Header variant="h2">Choose an active session</Header>}>
57+
<ol style={{ display: 'flex', flexDirection: 'column', gap: 16, margin: 0, padding: 0 }}>
58+
{accounts.map(({ accountAlias, accountId, role, email, lastLogin }) => (
59+
<Card
60+
tagName="li"
61+
header={accountAlias ? `${accountAlias} (${accountId})` : `Account ID: ${accountId}`}
62+
description={[role, email].filter(Boolean).join('/')}
63+
key={accountId}
64+
variant="action"
65+
active={activeId === accountId}
66+
onClick={() => setActiveId(accountId)}
67+
>
68+
<Box color="text-body-secondary" fontSize="body-s">{`Logged in ${lastLogin}`}</Box>
69+
</Card>
70+
))}
71+
</ol>
72+
</Container>
73+
</div>
74+
</ScreenshotArea>
75+
</article>
76+
);
77+
}

pages/card/action-card.page.tsx

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/cards/styles.scss

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,12 @@
7474
}
7575

7676
.card {
77-
/* Used in test utils */
77+
margin-block-start: 0;
78+
margin-block-end: awsui.$space-grid-gutter;
79+
margin-inline-start: awsui.$space-grid-gutter;
80+
margin-inline-end: 0;
81+
inline-size: 100%;
82+
min-inline-size: 0;
7883
}
7984

8085
.card-header-inner {

src/internal/components/card/index.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import React from 'react';
44
import clsx from 'clsx';
55

6+
import InternalIcon from '../../../icon/internal';
67
import { useVisualRefresh } from '../../hooks/use-visual-mode';
78
import { InternalCardProps } from './interfaces';
89

@@ -21,34 +22,47 @@ export default function Card({
2122
onFocus,
2223
role,
2324
tagName: TagName = 'div',
25+
variant = 'default',
2426
disableContentPaddings,
2527
}: InternalCardProps) {
2628
const isRefresh = useVisualRefresh();
2729

30+
const hasActions = !!actions || variant === 'action';
31+
32+
const InnerTagName = variant === 'action' ? 'button' : 'div';
33+
2834
return (
2935
<TagName
3036
className={clsx(className, styles.root, {
31-
[styles['with-actions']]: !!actions,
32-
[styles.active]: active,
37+
[styles['with-actions']]: !!hasActions,
3338
})}
3439
onFocus={onFocus}
3540
role={role}
3641
{...metadataAttributes}
3742
>
38-
<div
39-
className={clsx(styles['card-inner'], isRefresh && styles.refresh)}
43+
<InnerTagName
44+
className={clsx(
45+
styles['card-inner'],
46+
styles[`variant-${variant}`],
47+
isRefresh && styles.refresh,
48+
active && [styles.active]
49+
)}
4050
{...innerMetadataAttributes}
4151
onClick={onClick}
4252
>
4353
<div className={styles.header}>
4454
<div className={styles['header-top-row']}>
4555
<div className={styles['header-inner']}>{header}</div>
46-
{actions && <div className={styles.actions}>{actions}</div>}
56+
{hasActions && (
57+
<div className={styles.actions}>
58+
<div className={styles['actions-inner']}>{actions || <InternalIcon name="angle-right" />}</div>
59+
</div>
60+
)}
4761
</div>
4862
{description && <div className={styles.description}>{description}</div>}
4963
</div>
5064
<div className={clsx(styles.body, disableContentPaddings && styles['no-padding'])}>{children}</div>
51-
</div>
65+
</InnerTagName>
5266
</TagName>
5367
);
5468
}

src/internal/components/card/interfaces.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,13 @@ export interface InternalCardProps extends BaseComponentProps {
5353

5454
role?: string;
5555

56-
tagName?: 'li' | 'div';
56+
tagName?: 'li' | 'div' | 'button';
5757

5858
disableContentPaddings?: boolean;
5959

6060
metadataAttributes?: Record<string, string | undefined>;
6161

6262
innerMetadataAttributes?: Record<string, string | undefined>;
63+
64+
variant?: 'action' | 'default';
6365
}

src/internal/components/card/styles.scss

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,25 @@
55

66
@use 'sass:math';
77

8+
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
89
@use '../../styles' as styles;
910
@use '../../styles/tokens' as awsui;
1011
@use './motion';
1112

1213
@mixin card-style {
14+
@include styles.styles-reset();
15+
1316
border-start-start-radius: awsui.$border-radius-container;
1417
border-start-end-radius: awsui.$border-radius-container;
1518
border-end-start-radius: awsui.$border-radius-container;
1619
border-end-end-radius: awsui.$border-radius-container;
1720
box-sizing: border-box;
1821

22+
// Override button styles for action cards
23+
padding-inline: 0;
24+
border-block: 0 none;
25+
border-inline: 0 none;
26+
1927
&::before {
2028
@include styles.base-pseudo-element;
2129
// Reset border color to prevent it from flashing black during card selection animation
@@ -39,13 +47,16 @@
3947
box-shadow: awsui.$shadow-container;
4048
}
4149
&.refresh::after {
42-
border-block: solid awsui.$border-divider-section-width awsui.$color-border-divider-default;
43-
border-inline: solid awsui.$border-divider-section-width awsui.$color-border-divider-default;
50+
border-block: solid awsui.$border-divider-section-width;
51+
border-inline: solid awsui.$border-divider-section-width;
52+
}
53+
&:not(.variant-action)::after {
54+
border-color: awsui.$color-border-divider-default;
4455
}
4556
}
4657

4758
.root {
48-
@include styles.styles-reset();
59+
background-color: transparent;
4960
display: flex;
5061
overflow-wrap: break-word;
5162
word-wrap: break-word;
@@ -56,16 +67,58 @@
5667
list-style: none;
5768
}
5869

70+
.actions-inner {
71+
flex-shrink: 0;
72+
}
73+
5974
.card-inner {
6075
position: relative;
6176
background-color: awsui.$color-background-container-content;
62-
margin-block-start: 0;
63-
margin-block-end: awsui.$space-grid-gutter;
64-
margin-inline-start: awsui.$space-grid-gutter;
65-
margin-inline-end: 0;
6677
inline-size: 100%;
6778
min-inline-size: 0;
6879
@include card-style;
80+
/* stylelint-disable-next-line selector-combinator-disallowed-list */
81+
&:not(.active):not(:hover) .actions-inner {
82+
color: awsui.$color-border-button-normal-default;
83+
}
84+
&.active {
85+
&::before {
86+
border-block: awsui.$border-item-width solid;
87+
border-inline: awsui.$border-item-width solid;
88+
}
89+
&:not(.variant-action) {
90+
background-color: awsui.$color-background-item-selected;
91+
&::before {
92+
border-color: awsui.$color-border-item-selected;
93+
}
94+
}
95+
&.variant-action::before {
96+
border-color: awsui.$color-border-button-normal-hover;
97+
}
98+
}
99+
&.variant-action {
100+
cursor: pointer;
101+
102+
&:not(:hover) {
103+
&::after {
104+
border-color: awsui.$color-border-button-normal-default;
105+
}
106+
}
107+
&:hover::after {
108+
border-color: awsui.$color-border-button-normal-hover;
109+
}
110+
@include focus-visible.when-visible {
111+
&::before {
112+
border-block-width: 1px;
113+
border-inline-width: 1px;
114+
}
115+
@include styles.focus-highlight(
116+
$gutter: 0,
117+
$border-radius: awsui.$border-radius-container,
118+
$box-shadow: styles.$box-shadow-focused-light
119+
);
120+
}
121+
}
69122
}
70123

71124
.header {
@@ -104,20 +157,6 @@
104157
}
105158
}
106159

107-
.active {
108-
> .card-inner {
109-
background-color: awsui.$color-background-item-selected;
110-
&::before {
111-
border-block: awsui.$border-item-width solid awsui.$color-border-item-selected;
112-
border-inline: awsui.$border-item-width solid awsui.$color-border-item-selected;
113-
}
114-
}
115-
}
116-
117-
.actions {
118-
flex-shrink: 0;
119-
}
120-
121160
.description {
122161
color: awsui.$color-text-heading-secondary;
123162
}

0 commit comments

Comments
 (0)