Skip to content

Commit

Permalink
feat(avatar): add loading state to avatar and AIConversation (#5777)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbanksdesign authored Sep 11, 2024
1 parent 1e2d285 commit 0ebf8b3
Show file tree
Hide file tree
Showing 13 changed files with 143 additions and 2 deletions.
12 changes: 12 additions & 0 deletions .changeset/olive-bats-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@aws-amplify/ui-react-ai": minor
"@aws-amplify/ui-react": minor
"@aws-amplify/ui": minor
---

feat(avatar): add loading state to avatar and AIConversation


```jsx
<Avatar isLoading />
```
7 changes: 7 additions & 0 deletions docs/__tests__/__snapshots__/props-table.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,13 @@ exports[`Props Table 1`] = `
"category": "BaseAvatarProps",
"isOptional": true
},
"isLoading": {
"name": "isLoading",
"type": "boolean | undefined",
"description": "The isLoading property will display a loader around the avatar",
"category": "BaseAvatarProps",
"isOptional": true
},
"isDisabled": {
"name": "isDisabled",
"type": "boolean | undefined",
Expand Down
5 changes: 5 additions & 0 deletions docs/src/components/ComponentsMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ export const ComponentsMetadata: ComponentClassNameItems = {
components: ['Avatar'],
description: 'Class applied to the icon element',
},
AvatarLoader: {
className: ComponentClassName.AvatarLoader,
components: ['Avatar'],
description: 'Class applied to the loader element',
},
Badge: {
className: ComponentClassName.Badge,
components: ['Badge'],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Avatar, Flex } from '@aws-amplify/ui-react';

export default function AvatarLoadingExample() {
return (
<Flex>
<Avatar isLoading />
<Avatar isLoading colorTheme="info" />
<Avatar isLoading variation="outlined" colorTheme="success" />
</Flex>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { default as AvatarVariationExample } from './AvatarVariationExample';
export { default as AvatarStyleExample } from './AvatarStyleExample';
export { default as AvatarThemeExample } from './AvatarThemeExample';
export { default as AvatarAccessibilityExample } from './AvatarAccessibilityExample';
export { default as AvatarLoadingExample } from './AvatarLoadingExample';
18 changes: 18 additions & 0 deletions docs/src/pages/[platform]/components/avatar/react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
AvatarStyleExample,
AvatarThemeExample,
AvatarAccessibilityExample,
AvatarLoadingExample,
} from './examples';

## Demo
Expand Down Expand Up @@ -89,6 +90,23 @@ Import the Avatar primitive and styles.
</ExampleCode>
</Example>


### Loading

<Example>
<View>
<AvatarLoadingExample />
</View>

<ExampleCode>

```jsx file=./examples/AvatarLoadingExample.tsx

```

</ExampleCode>
</Example>

### Changing the default icon

You can use the `<IconsProvider>` to change the default icon for all Avatars in your application.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
classNameModifier,
classNames,
} from '@aws-amplify/ui';
import { LoadingContext } from '../../context/LoadingContext';

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

const LoadingMessage = () => {
const avatars = React.useContext(AvatarsContext);
const variant = React.useContext(MessageVariantContext);
const avatar = avatars?.ai;

return (
<View
className={classNames(
ComponentClassName.AIConversationMessage,
classNameModifier(ComponentClassName.AIConversationMessage, variant),
classNameModifier(ComponentClassName.AIConversationMessage, 'assistant')
)}
>
<View className={ComponentClassName.AIConversationMessageAvatar}>
<Avatar isLoading>{avatar?.avatar}</Avatar>
</View>
<View className={ComponentClassName.AIConversationMessageBody}>
<View className={ComponentClassName.AIConversationMessageSender}>
<Text
className={ComponentClassName.AIConversationMessageSenderUsername}
>
{avatar?.username}
</Text>
</View>
</View>
</View>
);
};

const Message = ({ message }: { message: ConversationMessage }) => {
const avatars = React.useContext(AvatarsContext);
const variant = React.useContext(MessageVariantContext);
Expand Down Expand Up @@ -68,11 +98,13 @@ const Message = ({ message }: { message: ConversationMessage }) => {
export const MessageList: ControlsContextProps['MessageList'] = ({
messages,
}) => {
const isLoading = React.useContext(LoadingContext);
return (
<View className={ComponentClassName.AIConversationMessageList}>
{messages.map((message, i) => (
<Message key={`message-${i}`} message={message} />
))}
{isLoading ? <LoadingMessage /> : null}
</View>
);
};
3 changes: 3 additions & 0 deletions packages/react/__tests__/__snapshots__/exports.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,9 @@ exports[`primitive catalog should match primitives catalog snapshot 1`] = `
"isDisabled": {
"type": "boolean",
},
"isLoading": {
"type": "boolean",
},
"left": {
"type": "string",
},
Expand Down
16 changes: 15 additions & 1 deletion packages/react/src/primitives/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,20 @@ import { View } from '../View';
import { IconUser, useIcons } from '../Icon';
import { Image } from '../Image';
import { AvatarProps, BaseAvatarProps } from './types';
import { Loader } from '../Loader';

const AvatarPrimitive: Primitive<AvatarProps, 'span'> = (
{ className, children, variation, colorTheme, size, src, alt, ...rest },
{
className,
children,
variation,
colorTheme,
size,
src,
alt,
isLoading,
...rest
},
ref
) => {
const icons = useIcons('avatar');
Expand Down Expand Up @@ -40,6 +51,9 @@ const AvatarPrimitive: Primitive<AvatarProps, 'span'> = (
</View>
)
)}
{isLoading ? (
<Loader className={ComponentClassName.AvatarLoader} />
) : null}
</View>
);
};
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/primitives/Avatar/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export interface BaseAvatarProps extends BaseViewProps {
* The size property will affect the size of the avatar.
*/
size?: AvatarSizes;

/**
* @description
* The isLoading property will display a loader around the avatar
*/
isLoading?: boolean;
}

export type AvatarProps<Element extends ElementType = 'span'> = PrimitiveProps<
Expand Down
9 changes: 8 additions & 1 deletion packages/ui/src/theme/components/avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,11 @@ import {

export type AvatarTheme<Required extends boolean = false> = ComponentStyles &
Modifiers<Size | ColorTheme | 'filled' | 'outlined', Required> &
Elements<{ icon?: ComponentStyles; image?: ComponentStyles }, Required>;
Elements<
{
icon?: ComponentStyles;
image?: ComponentStyles;
loader?: ComponentStyles;
},
Required
>;
24 changes: 24 additions & 0 deletions packages/ui/src/theme/css/component/avatar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

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

position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
Expand Down Expand Up @@ -66,6 +67,9 @@
--avatar-filled-color: var(
--amplify-components-avatar-warning-background-color
);
--amplify-components-loader-stroke-filled: var(
--amplify-components-avatar-warning-color
);
}

&--error {
Expand All @@ -80,6 +84,9 @@
--avatar-filled-color: var(
--amplify-components-avatar-error-background-color
);
--amplify-components-loader-stroke-filled: var(
--amplify-components-avatar-error-color
);
}

&--info {
Expand All @@ -94,6 +101,10 @@
--avatar-filled-color: var(
--amplify-components-avatar-info-background-color
);

--amplify-components-loader-stroke-filled: var(
--amplify-components-avatar-info-color
);
}

&--success {
Expand All @@ -110,6 +121,10 @@
--avatar-filled-color: var(
--amplify-components-avatar-success-background-color
);

--amplify-components-loader-stroke-filled: var(
--amplify-components-avatar-success-color
);
}

// elements
Expand All @@ -126,4 +141,13 @@
object-fit: cover;
display: block;
}

&__loader {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
// This will make the empty part of the loader not show up
stroke: transparent;
}
}
1 change: 1 addition & 0 deletions packages/ui/src/types/primitives/componentClassName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const ComponentClassName = {
Avatar: 'amplify-avatar',
AvatarIcon: 'amplify-avatar__icon',
AvatarImage: 'amplify-avatar__image',
AvatarLoader: 'amplify-avatar__loader',
AIConversation: 'amplify-ai-conversation',
AIConversationAttachment: 'amplify-ai-conversation__attachment',
AIConversationAttachmentList: 'amplify-ai-conversation__attachment__list',
Expand Down

0 comments on commit 0ebf8b3

Please sign in to comment.