Skip to content

Commit 0ebf8b3

Browse files
authored
feat(avatar): add loading state to avatar and AIConversation (#5777)
1 parent 1e2d285 commit 0ebf8b3

File tree

13 files changed

+143
-2
lines changed

13 files changed

+143
-2
lines changed

.changeset/olive-bats-obey.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@aws-amplify/ui-react-ai": minor
3+
"@aws-amplify/ui-react": minor
4+
"@aws-amplify/ui": minor
5+
---
6+
7+
feat(avatar): add loading state to avatar and AIConversation
8+
9+
10+
```jsx
11+
<Avatar isLoading />
12+
```

docs/__tests__/__snapshots__/props-table.test.ts.snap

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,13 @@ exports[`Props Table 1`] = `
11231123
"category": "BaseAvatarProps",
11241124
"isOptional": true
11251125
},
1126+
"isLoading": {
1127+
"name": "isLoading",
1128+
"type": "boolean | undefined",
1129+
"description": "The isLoading property will display a loader around the avatar",
1130+
"category": "BaseAvatarProps",
1131+
"isOptional": true
1132+
},
11261133
"isDisabled": {
11271134
"name": "isDisabled",
11281135
"type": "boolean | undefined",

docs/src/components/ComponentsMetadata.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ export const ComponentsMetadata: ComponentClassNameItems = {
285285
components: ['Avatar'],
286286
description: 'Class applied to the icon element',
287287
},
288+
AvatarLoader: {
289+
className: ComponentClassName.AvatarLoader,
290+
components: ['Avatar'],
291+
description: 'Class applied to the loader element',
292+
},
288293
Badge: {
289294
className: ComponentClassName.Badge,
290295
components: ['Badge'],
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Avatar, Flex } from '@aws-amplify/ui-react';
2+
3+
export default function AvatarLoadingExample() {
4+
return (
5+
<Flex>
6+
<Avatar isLoading />
7+
<Avatar isLoading colorTheme="info" />
8+
<Avatar isLoading variation="outlined" colorTheme="success" />
9+
</Flex>
10+
);
11+
}

docs/src/pages/[platform]/components/avatar/examples/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export { default as AvatarVariationExample } from './AvatarVariationExample';
66
export { default as AvatarStyleExample } from './AvatarStyleExample';
77
export { default as AvatarThemeExample } from './AvatarThemeExample';
88
export { default as AvatarAccessibilityExample } from './AvatarAccessibilityExample';
9+
export { default as AvatarLoadingExample } from './AvatarLoadingExample';

docs/src/pages/[platform]/components/avatar/react.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
AvatarStyleExample,
1616
AvatarThemeExample,
1717
AvatarAccessibilityExample,
18+
AvatarLoadingExample,
1819
} from './examples';
1920

2021
## Demo
@@ -89,6 +90,23 @@ Import the Avatar primitive and styles.
8990
</ExampleCode>
9091
</Example>
9192

93+
94+
### Loading
95+
96+
<Example>
97+
<View>
98+
<AvatarLoadingExample />
99+
</View>
100+
101+
<ExampleCode>
102+
103+
```jsx file=./examples/AvatarLoadingExample.tsx
104+
105+
```
106+
107+
</ExampleCode>
108+
</Example>
109+
92110
### Changing the default icon
93111

94112
You can use the `<IconsProvider>` to change the default icon for all Avatars in your application.

packages/react-ai/src/components/AIConversation/views/default/MessageList.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
classNameModifier,
1515
classNames,
1616
} from '@aws-amplify/ui';
17+
import { LoadingContext } from '../../context/LoadingContext';
1718

1819
const MessageMeta = ({ message }: { message: ConversationMessage }) => {
1920
// need to pass this in as props in order for it to be overridable
@@ -34,6 +35,35 @@ const MessageMeta = ({ message }: { message: ConversationMessage }) => {
3435
);
3536
};
3637

38+
const LoadingMessage = () => {
39+
const avatars = React.useContext(AvatarsContext);
40+
const variant = React.useContext(MessageVariantContext);
41+
const avatar = avatars?.ai;
42+
43+
return (
44+
<View
45+
className={classNames(
46+
ComponentClassName.AIConversationMessage,
47+
classNameModifier(ComponentClassName.AIConversationMessage, variant),
48+
classNameModifier(ComponentClassName.AIConversationMessage, 'assistant')
49+
)}
50+
>
51+
<View className={ComponentClassName.AIConversationMessageAvatar}>
52+
<Avatar isLoading>{avatar?.avatar}</Avatar>
53+
</View>
54+
<View className={ComponentClassName.AIConversationMessageBody}>
55+
<View className={ComponentClassName.AIConversationMessageSender}>
56+
<Text
57+
className={ComponentClassName.AIConversationMessageSenderUsername}
58+
>
59+
{avatar?.username}
60+
</Text>
61+
</View>
62+
</View>
63+
</View>
64+
);
65+
};
66+
3767
const Message = ({ message }: { message: ConversationMessage }) => {
3868
const avatars = React.useContext(AvatarsContext);
3969
const variant = React.useContext(MessageVariantContext);
@@ -68,11 +98,13 @@ const Message = ({ message }: { message: ConversationMessage }) => {
6898
export const MessageList: ControlsContextProps['MessageList'] = ({
6999
messages,
70100
}) => {
101+
const isLoading = React.useContext(LoadingContext);
71102
return (
72103
<View className={ComponentClassName.AIConversationMessageList}>
73104
{messages.map((message, i) => (
74105
<Message key={`message-${i}`} message={message} />
75106
))}
107+
{isLoading ? <LoadingMessage /> : null}
76108
</View>
77109
);
78110
};

packages/react/__tests__/__snapshots__/exports.ts.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,9 @@ exports[`primitive catalog should match primitives catalog snapshot 1`] = `
11291129
"isDisabled": {
11301130
"type": "boolean",
11311131
},
1132+
"isLoading": {
1133+
"type": "boolean",
1134+
},
11321135
"left": {
11331136
"type": "string",
11341137
},

packages/react/src/primitives/Avatar/Avatar.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,20 @@ import { View } from '../View';
1010
import { IconUser, useIcons } from '../Icon';
1111
import { Image } from '../Image';
1212
import { AvatarProps, BaseAvatarProps } from './types';
13+
import { Loader } from '../Loader';
1314

1415
const AvatarPrimitive: Primitive<AvatarProps, 'span'> = (
15-
{ className, children, variation, colorTheme, size, src, alt, ...rest },
16+
{
17+
className,
18+
children,
19+
variation,
20+
colorTheme,
21+
size,
22+
src,
23+
alt,
24+
isLoading,
25+
...rest
26+
},
1627
ref
1728
) => {
1829
const icons = useIcons('avatar');
@@ -40,6 +51,9 @@ const AvatarPrimitive: Primitive<AvatarProps, 'span'> = (
4051
</View>
4152
)
4253
)}
54+
{isLoading ? (
55+
<Loader className={ComponentClassName.AvatarLoader} />
56+
) : null}
4357
</View>
4458
);
4559
};

packages/react/src/primitives/Avatar/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export interface BaseAvatarProps extends BaseViewProps {
3737
* The size property will affect the size of the avatar.
3838
*/
3939
size?: AvatarSizes;
40+
41+
/**
42+
* @description
43+
* The isLoading property will display a loader around the avatar
44+
*/
45+
isLoading?: boolean;
4046
}
4147

4248
export type AvatarProps<Element extends ElementType = 'span'> = PrimitiveProps<

packages/ui/src/theme/components/avatar.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,11 @@ import {
88

99
export type AvatarTheme<Required extends boolean = false> = ComponentStyles &
1010
Modifiers<Size | ColorTheme | 'filled' | 'outlined', Required> &
11-
Elements<{ icon?: ComponentStyles; image?: ComponentStyles }, Required>;
11+
Elements<
12+
{
13+
icon?: ComponentStyles;
14+
image?: ComponentStyles;
15+
loader?: ComponentStyles;
16+
},
17+
Required
18+
>;

packages/ui/src/theme/css/component/avatar.scss

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
--amplify-components-icon-height: 100%;
1212

13+
position: relative;
1314
display: inline-flex;
1415
align-items: center;
1516
justify-content: center;
@@ -66,6 +67,9 @@
6667
--avatar-filled-color: var(
6768
--amplify-components-avatar-warning-background-color
6869
);
70+
--amplify-components-loader-stroke-filled: var(
71+
--amplify-components-avatar-warning-color
72+
);
6973
}
7074

7175
&--error {
@@ -80,6 +84,9 @@
8084
--avatar-filled-color: var(
8185
--amplify-components-avatar-error-background-color
8286
);
87+
--amplify-components-loader-stroke-filled: var(
88+
--amplify-components-avatar-error-color
89+
);
8390
}
8491

8592
&--info {
@@ -94,6 +101,10 @@
94101
--avatar-filled-color: var(
95102
--amplify-components-avatar-info-background-color
96103
);
104+
105+
--amplify-components-loader-stroke-filled: var(
106+
--amplify-components-avatar-info-color
107+
);
97108
}
98109

99110
&--success {
@@ -110,6 +121,10 @@
110121
--avatar-filled-color: var(
111122
--amplify-components-avatar-success-background-color
112123
);
124+
125+
--amplify-components-loader-stroke-filled: var(
126+
--amplify-components-avatar-success-color
127+
);
113128
}
114129

115130
// elements
@@ -126,4 +141,13 @@
126141
object-fit: cover;
127142
display: block;
128143
}
144+
145+
&__loader {
146+
position: absolute;
147+
inset: 0;
148+
width: 100%;
149+
height: 100%;
150+
// This will make the empty part of the loader not show up
151+
stroke: transparent;
152+
}
129153
}

packages/ui/src/types/primitives/componentClassName.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const ComponentClassName = {
2020
Avatar: 'amplify-avatar',
2121
AvatarIcon: 'amplify-avatar__icon',
2222
AvatarImage: 'amplify-avatar__image',
23+
AvatarLoader: 'amplify-avatar__loader',
2324
AIConversation: 'amplify-ai-conversation',
2425
AIConversationAttachment: 'amplify-ai-conversation__attachment',
2526
AIConversationAttachmentList: 'amplify-ai-conversation__attachment__list',

0 commit comments

Comments
 (0)