From 0ebf8b346bc744cd73e1e7891eafc07538d6419d Mon Sep 17 00:00:00 2001 From: Danny Banks Date: Wed, 11 Sep 2024 13:07:15 -0700 Subject: [PATCH] feat(avatar): add loading state to avatar and AIConversation (#5777) --- .changeset/olive-bats-obey.md | 12 +++++++ .../__snapshots__/props-table.test.ts.snap | 7 ++++ docs/src/components/ComponentsMetadata.ts | 5 +++ .../avatar/examples/AvatarLoadingExample.tsx | 11 +++++++ .../components/avatar/examples/index.ts | 1 + .../[platform]/components/avatar/react.mdx | 18 +++++++++++ .../views/default/MessageList.tsx | 32 +++++++++++++++++++ .../__tests__/__snapshots__/exports.ts.snap | 3 ++ .../react/src/primitives/Avatar/Avatar.tsx | 16 +++++++++- packages/react/src/primitives/Avatar/types.ts | 6 ++++ packages/ui/src/theme/components/avatar.ts | 9 +++++- .../ui/src/theme/css/component/avatar.scss | 24 ++++++++++++++ .../types/primitives/componentClassName.ts | 1 + 13 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 .changeset/olive-bats-obey.md create mode 100644 docs/src/pages/[platform]/components/avatar/examples/AvatarLoadingExample.tsx diff --git a/.changeset/olive-bats-obey.md b/.changeset/olive-bats-obey.md new file mode 100644 index 00000000000..2e56662393a --- /dev/null +++ b/.changeset/olive-bats-obey.md @@ -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 + +``` diff --git a/docs/__tests__/__snapshots__/props-table.test.ts.snap b/docs/__tests__/__snapshots__/props-table.test.ts.snap index 7c35648de59..49277ca027d 100644 --- a/docs/__tests__/__snapshots__/props-table.test.ts.snap +++ b/docs/__tests__/__snapshots__/props-table.test.ts.snap @@ -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", diff --git a/docs/src/components/ComponentsMetadata.ts b/docs/src/components/ComponentsMetadata.ts index 7e13d53bc87..7948e9c9b07 100644 --- a/docs/src/components/ComponentsMetadata.ts +++ b/docs/src/components/ComponentsMetadata.ts @@ -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'], diff --git a/docs/src/pages/[platform]/components/avatar/examples/AvatarLoadingExample.tsx b/docs/src/pages/[platform]/components/avatar/examples/AvatarLoadingExample.tsx new file mode 100644 index 00000000000..06873e9738d --- /dev/null +++ b/docs/src/pages/[platform]/components/avatar/examples/AvatarLoadingExample.tsx @@ -0,0 +1,11 @@ +import { Avatar, Flex } from '@aws-amplify/ui-react'; + +export default function AvatarLoadingExample() { + return ( + + + + + + ); +} diff --git a/docs/src/pages/[platform]/components/avatar/examples/index.ts b/docs/src/pages/[platform]/components/avatar/examples/index.ts index 6e6475b8cfa..6c7048e57d6 100644 --- a/docs/src/pages/[platform]/components/avatar/examples/index.ts +++ b/docs/src/pages/[platform]/components/avatar/examples/index.ts @@ -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'; diff --git a/docs/src/pages/[platform]/components/avatar/react.mdx b/docs/src/pages/[platform]/components/avatar/react.mdx index e3c74fb8c06..8cbf76272c3 100644 --- a/docs/src/pages/[platform]/components/avatar/react.mdx +++ b/docs/src/pages/[platform]/components/avatar/react.mdx @@ -15,6 +15,7 @@ import { AvatarStyleExample, AvatarThemeExample, AvatarAccessibilityExample, + AvatarLoadingExample, } from './examples'; ## Demo @@ -89,6 +90,23 @@ Import the Avatar primitive and styles. + +### Loading + + + + + + + + +```jsx file=./examples/AvatarLoadingExample.tsx + +``` + + + + ### Changing the default icon You can use the `` to change the default icon for all Avatars in your application. diff --git a/packages/react-ai/src/components/AIConversation/views/default/MessageList.tsx b/packages/react-ai/src/components/AIConversation/views/default/MessageList.tsx index 015b5dcbc5e..5c4e060b45d 100644 --- a/packages/react-ai/src/components/AIConversation/views/default/MessageList.tsx +++ b/packages/react-ai/src/components/AIConversation/views/default/MessageList.tsx @@ -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 @@ -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 ( + + + {avatar?.avatar} + + + + + {avatar?.username} + + + + + ); +}; + const Message = ({ message }: { message: ConversationMessage }) => { const avatars = React.useContext(AvatarsContext); const variant = React.useContext(MessageVariantContext); @@ -68,11 +98,13 @@ const Message = ({ message }: { message: ConversationMessage }) => { export const MessageList: ControlsContextProps['MessageList'] = ({ messages, }) => { + const isLoading = React.useContext(LoadingContext); return ( {messages.map((message, i) => ( ))} + {isLoading ? : null} ); }; diff --git a/packages/react/__tests__/__snapshots__/exports.ts.snap b/packages/react/__tests__/__snapshots__/exports.ts.snap index 7fd50825b42..de8d2ffb9e8 100644 --- a/packages/react/__tests__/__snapshots__/exports.ts.snap +++ b/packages/react/__tests__/__snapshots__/exports.ts.snap @@ -1129,6 +1129,9 @@ exports[`primitive catalog should match primitives catalog snapshot 1`] = ` "isDisabled": { "type": "boolean", }, + "isLoading": { + "type": "boolean", + }, "left": { "type": "string", }, diff --git a/packages/react/src/primitives/Avatar/Avatar.tsx b/packages/react/src/primitives/Avatar/Avatar.tsx index 825e4d4c988..7b3ee3fdaf5 100644 --- a/packages/react/src/primitives/Avatar/Avatar.tsx +++ b/packages/react/src/primitives/Avatar/Avatar.tsx @@ -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 = ( - { className, children, variation, colorTheme, size, src, alt, ...rest }, + { + className, + children, + variation, + colorTheme, + size, + src, + alt, + isLoading, + ...rest + }, ref ) => { const icons = useIcons('avatar'); @@ -40,6 +51,9 @@ const AvatarPrimitive: Primitive = ( ) )} + {isLoading ? ( + + ) : null} ); }; diff --git a/packages/react/src/primitives/Avatar/types.ts b/packages/react/src/primitives/Avatar/types.ts index fb6286bd475..5d469d4d3bb 100644 --- a/packages/react/src/primitives/Avatar/types.ts +++ b/packages/react/src/primitives/Avatar/types.ts @@ -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 = PrimitiveProps< diff --git a/packages/ui/src/theme/components/avatar.ts b/packages/ui/src/theme/components/avatar.ts index 890217227cd..093892f83d0 100644 --- a/packages/ui/src/theme/components/avatar.ts +++ b/packages/ui/src/theme/components/avatar.ts @@ -8,4 +8,11 @@ import { export type AvatarTheme = ComponentStyles & Modifiers & - Elements<{ icon?: ComponentStyles; image?: ComponentStyles }, Required>; + Elements< + { + icon?: ComponentStyles; + image?: ComponentStyles; + loader?: ComponentStyles; + }, + Required + >; diff --git a/packages/ui/src/theme/css/component/avatar.scss b/packages/ui/src/theme/css/component/avatar.scss index ad021581669..ec3ff01a308 100644 --- a/packages/ui/src/theme/css/component/avatar.scss +++ b/packages/ui/src/theme/css/component/avatar.scss @@ -10,6 +10,7 @@ --amplify-components-icon-height: 100%; + position: relative; display: inline-flex; align-items: center; justify-content: center; @@ -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 { @@ -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 { @@ -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 { @@ -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 @@ -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; + } } diff --git a/packages/ui/src/types/primitives/componentClassName.ts b/packages/ui/src/types/primitives/componentClassName.ts index 7589d7f7d28..0875fb0aa89 100644 --- a/packages/ui/src/types/primitives/componentClassName.ts +++ b/packages/ui/src/types/primitives/componentClassName.ts @@ -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',