diff --git a/pages/avatar/custom-width.page.tsx b/pages/avatar/custom-width.page.tsx
new file mode 100644
index 0000000..b507651
--- /dev/null
+++ b/pages/avatar/custom-width.page.tsx
@@ -0,0 +1,66 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { useState } from "react";
+
+import Checkbox from "@cloudscape-design/components/checkbox";
+import FormField from "@cloudscape-design/components/form-field";
+import Header from "@cloudscape-design/components/header";
+import { IconProps } from "@cloudscape-design/components/icon";
+import Input from "@cloudscape-design/components/input";
+import SpaceBetween from "@cloudscape-design/components/space-between";
+
+import { Avatar } from "../../lib/components";
+
+export default function AvatarImageAndWidth() {
+ const [url, setURL] = useState(
+ "https://static1.colliderimages.com/wordpress/wp-content/uploads/2024/08/deadpool-wolverine-hugh-jackman-mask-reveal.jpg",
+ );
+ const [width, setWidth] = useState("100");
+ const [initials, setInitials] = useState("SO");
+ const [iconName, setIconName] = useState("search");
+ const [loading, setLoading] = useState(false);
+ const [genAI, setGenAI] = useState(false);
+
+ return (
+
+ Input an Image URL and custom size.
+
+
+
+ setWidth(detail.value)} value={width} inputMode="numeric" type="number" />
+
+
+
+ setURL(detail.value)} value={url} />
+
+
+
+ setInitials(detail.value)} value={initials} />
+
+
+
+ setIconName(detail.value)} value={iconName} />
+
+
+ setLoading(detail.checked)} checked={loading}>
+ Loading
+
+
+ setGenAI(detail.checked)} checked={genAI}>
+ Gen AI
+
+
+
+
+
+ );
+}
diff --git a/pages/avatar/permutations.page.tsx b/pages/avatar/permutations.page.tsx
index bb4f0e4..6b8c5fb 100644
--- a/pages/avatar/permutations.page.tsx
+++ b/pages/avatar/permutations.page.tsx
@@ -1,9 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
+import SpaceBetween from "@cloudscape-design/components/space-between";
+
import { Avatar } from "../../lib/components";
import { TestBed } from "../app/test-bed";
import { ScreenshotArea } from "../screenshot-area";
+import smiley from "./smiley.png";
const customIconSvg = (
+
+ {/* Loading should take prioirty over image */}
+
+
+
+
+ {/* Image with tiny width enforce minimum of 28px */}
+
+
+ {/* Image with default width of 28px */}
+
+
+ {/* Image should take priority over initials */}
+
+
+ {/* Image and tooltip should take priority over icon */}
+
+
+ {/* Icon SVG with custom width */}
+
+
+ {/* Icon name with custom width */}
+
+
+ {/* Icon name with custom width */}
+
+
+ {/* Initials with custom width */}
+
+
+ {/* Loading with custom width */}
+
+
+ {/* Initials with custom width */}
+
+
+ {/* Loading with custom width */}
+
+
diff --git a/pages/avatar/smiley.png b/pages/avatar/smiley.png
new file mode 100644
index 0000000..206dfd2
Binary files /dev/null and b/pages/avatar/smiley.png differ
diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap
index c855894..01efa59 100644
--- a/src/__tests__/__snapshots__/documenter.test.ts.snap
+++ b/src/__tests__/__snapshots__/documenter.test.ts.snap
@@ -48,14 +48,18 @@ If you set both \`iconName\` and \`initials\`, \`initials\` will take precedence
"type": "string",
},
{
- "description": "Specifies the URL of a custom icon. Use this property if the icon you want isn't available, and your custom icon can't be an SVG.
-For SVG icons, use the \`iconSvg\` slot instead.
-If you set both \`iconUrl\` and \`iconSvg\`, \`iconSvg\` will take precedence.
-",
+ "deprecatedTag": "Use \`iconSvg\` or \`imgUrl\` instead.",
+ "description": "Specifies the URL of a custom icon. Use this property if the icon you want isn't available, and your custom icon can't be an SVG.",
"name": "iconUrl",
"optional": true,
"type": "string",
},
+ {
+ "description": "Specifies the URL of a custom image. If you set both \`iconUrl\` and \`imgUrl\`, \`imgUrl\` will take precedence.",
+ "name": "imgUrl",
+ "optional": true,
+ "type": "string",
+ },
{
"description": "The text content shown directly in the avatar's body.
Can be 1 or 2 symbols long, every subsequent symbol is ignored.
@@ -78,13 +82,19 @@ When you use this property, make sure to include it in the \`ariaLabel\`.
"optional": true,
"type": "string",
},
+ {
+ "description": "Defines the width and height of the avatar.
+This value corresponds to the \`width\` CSS-property and will center and crop images using \`object-fit: cover\`.
+The default and minimum width value is 28px.",
+ "name": "width",
+ "optional": true,
+ "type": "number",
+ },
],
"regions": [
{
"description": "Specifies the SVG of a custom icon.
-Use this property if the icon you want isn't available.
-If you set both \`iconUrl\` and \`iconSvg\`, \`iconSvg\` will take precedence.
-",
+Use this property if the icon you want isn't available.",
"isDefault": false,
"name": "iconSvg",
},
diff --git a/src/avatar/__tests__/avatar.test.tsx b/src/avatar/__tests__/avatar.test.tsx
index e865522..5f9ffc1 100644
--- a/src/avatar/__tests__/avatar.test.tsx
+++ b/src/avatar/__tests__/avatar.test.tsx
@@ -9,6 +9,7 @@ import Avatar, { AvatarProps } from "../../../lib/components/avatar";
import createWrapper from "../../../lib/components/test-utils/dom";
import loadingDotsStyles from "../../../lib/components/avatar/loading-dots/styles.selectors.js";
+import avatarStyles from "../../../lib/components/avatar/styles.selectors.js";
const defaultAvatarProps: AvatarProps = { ariaLabel: "Avatar" };
@@ -30,6 +31,15 @@ describe("Avatar", () => {
expect(wrapper.getElement().textContent).toBe(initials);
});
+ test("Renders avatar with image", () => {
+ const imgUrl =
+ "https://static1.colliderimages.com/wordpress/wp-content/uploads/2024/08/deadpool-wolverine-hugh-jackman-mask-reveal.jpg";
+ const wrapper = renderAvatar({ ...defaultAvatarProps, imgUrl });
+
+ const image = wrapper.findByClassName(avatarStyles.image)?.getElement();
+ expect(image).toBeInTheDocument();
+ });
+
test("Shows tooltip on focus", () => {
const tooltipText = "Jane Doe";
const wrapper = renderAvatar({ ...defaultAvatarProps, color: "default", tooltipText });
@@ -75,6 +85,15 @@ describe("Avatar", () => {
expect(loading).toBeInTheDocument();
});
+ test("Loading takes precedence over image", () => {
+ const imgUrl =
+ "https://static1.colliderimages.com/wordpress/wp-content/uploads/2024/08/deadpool-wolverine-hugh-jackman-mask-reveal.jpg";
+ const wrapper = renderAvatar({ ...defaultAvatarProps, imgUrl, loading: true });
+
+ const loading = wrapper.findByClassName(loadingDotsStyles.root)?.getElement();
+ expect(loading).toBeInTheDocument();
+ });
+
test("Shows warning when initials length is longer than 2", () => {
const warnOnce = vi.spyOn(ComponentToolkitInternal, "warnOnce");
diff --git a/src/avatar/interfaces.ts b/src/avatar/interfaces.ts
index 0898b9d..d035e47 100644
--- a/src/avatar/interfaces.ts
+++ b/src/avatar/interfaces.ts
@@ -47,19 +47,27 @@ export interface AvatarProps {
/**
* Specifies the URL of a custom icon. Use this property if the icon you want isn't available, and your custom icon can't be an SVG.
- * For SVG icons, use the `iconSvg` slot instead.
- *
- * If you set both `iconUrl` and `iconSvg`, `iconSvg` will take precedence.
+ * @deprecated Use `iconSvg` or `imgUrl` instead.
*/
iconUrl?: string;
/**
* Specifies the SVG of a custom icon.
- *
* Use this property if the icon you want isn't available.
- * If you set both `iconUrl` and `iconSvg`, `iconSvg` will take precedence.
*/
iconSvg?: React.ReactNode;
+
+ /**
+ * Specifies the URL of a custom image. If you set both `iconUrl` and `imgUrl`, `imgUrl` will take precedence.
+ */
+ imgUrl?: string;
+
+ /**
+ * Defines the width and height of the avatar.
+ * This value corresponds to the `width` CSS-property and will center and crop images using `object-fit: cover`.
+ * The default and minimum width value is 28px.
+ */
+ width?: number;
}
export namespace AvatarProps {
diff --git a/src/avatar/internal.tsx b/src/avatar/internal.tsx
index 7a5bb03..a15008d 100644
--- a/src/avatar/internal.tsx
+++ b/src/avatar/internal.tsx
@@ -6,6 +6,7 @@ import clsx from "clsx";
import { warnOnce } from "@cloudscape-design/component-toolkit/internal";
import Icon from "@cloudscape-design/components/icon";
import Tooltip from "@cloudscape-design/components/internal/tooltip-do-not-use";
+import * as awsui from "@cloudscape-design/design-tokens";
import { getDataAttributes } from "../internal/base-component/get-data-attributes";
import { InternalBaseComponentProps } from "../internal/base-component/use-base-component";
@@ -17,9 +18,23 @@ import styles from "./styles.css.js";
export interface InternalAvatarProps extends AvatarProps, InternalBaseComponentProps {}
-const AvatarContent = ({ color, loading, initials, iconName, iconSvg, iconUrl, ariaLabel }: AvatarProps) => {
+const AvatarContent = ({
+ color,
+ loading,
+ initials,
+ iconName,
+ iconSvg,
+ iconUrl,
+ ariaLabel,
+ width,
+ imgUrl,
+}: AvatarProps) => {
if (loading) {
- return ;
+ return ;
+ }
+
+ if (imgUrl) {
+ return
;
}
if (initials) {
@@ -29,10 +44,14 @@ const AvatarContent = ({ color, loading, initials, iconName, iconSvg, iconUrl, a
warnOnce("Avatar", `"initials" is longer than 2 characters. Only the first two characters are shown.`);
}
- return {letters};
+ return (
+
+ {letters}
+
+ );
}
- return ;
+ return ;
};
export default function InternalAvatar({
@@ -44,6 +63,8 @@ export default function InternalAvatar({
iconName,
iconSvg,
iconUrl,
+ imgUrl,
+ width = 28,
__internalRootRef = null,
...rest
}: InternalAvatarProps) {
@@ -51,6 +72,7 @@ export default function InternalAvatar({
const [showTooltip, setShowTooltip] = useState(false);
const mergedRef = useMergeRefs(handleRef, __internalRootRef);
+ const computedSize = width < 28 ? 28 : width;
const tooltipAttributes = {
onFocus: () => {
@@ -84,6 +106,7 @@ export default function InternalAvatar({
role="img"
aria-label={ariaLabel}
{...tooltipAttributes}
+ style={{ height: computedSize, width: computedSize }}
>
{showTooltip && tooltipText && (
+
diff --git a/src/avatar/loading-dots/index.tsx b/src/avatar/loading-dots/index.tsx
index e112e51..9095945 100644
--- a/src/avatar/loading-dots/index.tsx
+++ b/src/avatar/loading-dots/index.tsx
@@ -6,16 +6,19 @@ import styles from "./styles.css.js";
interface LoadingDotsProps {
color?: string;
+ width?: number;
}
-export default function LoadingDots({ color }: LoadingDotsProps) {
+export default function LoadingDots({ color, width }: LoadingDotsProps) {
+ const dotSize = `calc(.14px * ${width})`;
+
return (
// "gen-ai" class is added so that the gradient background animates.
);
diff --git a/src/avatar/loading-dots/motion.scss b/src/avatar/loading-dots/motion.scss
index 72c2e32..f7ef051 100644
--- a/src/avatar/loading-dots/motion.scss
+++ b/src/avatar/loading-dots/motion.scss
@@ -39,8 +39,8 @@
}
50% {
- block-size: 44px;
- inline-size: 44px;
+ block-size: 150%;
+ inline-size: 150%;
}
100% {
@@ -55,8 +55,8 @@
}
50% {
- block-size: 44px;
- inline-size: 44px;
+ block-size: 150%;
+ inline-size: 150%;
inset-inline-start: -100%;
}
@@ -103,7 +103,7 @@
}
28% {
- transform: translateY(-4px);
+ transform: translateY(-100%);
}
44% {
diff --git a/src/avatar/loading-dots/styles.scss b/src/avatar/loading-dots/styles.scss
index c87fd23..5cea750 100644
--- a/src/avatar/loading-dots/styles.scss
+++ b/src/avatar/loading-dots/styles.scss
@@ -23,7 +23,7 @@ $dot-size: 4px;
align-items: center;
justify-content: space-between;
display: flex;
- inline-size: 18px;
+ inline-size: 64%;
}
.dot {
diff --git a/src/avatar/styles.scss b/src/avatar/styles.scss
index 7ec4f0d..134055d 100644
--- a/src/avatar/styles.scss
+++ b/src/avatar/styles.scss
@@ -10,7 +10,6 @@ $avatar-size: 28px;
.root {
@include shared.styles-reset;
-
color: cs.$color-text-avatar;
block-size: $avatar-size;
inline-size: $avatar-size;
@@ -39,6 +38,10 @@ $avatar-size: 28px;
@include shared.focus-highlight(1px, 50%);
}
}
+
+ &:has(.image) {
+ background: transparent;
+ }
}
.content {
@@ -48,4 +51,11 @@ $avatar-size: 28px;
block-size: inherit;
inline-size: inherit;
overflow: hidden;
+
+ .image {
+ @include mixins.border-radius-avatar;
+ block-size: $avatar-size;
+ inline-size: $avatar-size;
+ object-fit: cover;
+ }
}