Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions pages/avatar/permutations.page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// 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";
Expand Down Expand Up @@ -38,15 +40,74 @@ export default function AvatarPage() {
<Avatar color="gen-ai" initials="GW" ariaLabel="Gen AI assistant GW" tooltipText="Gen AI assistant" />

<Avatar loading={true} ariaLabel="User avatar typing" tooltipText="User avatar typing" />

{/* Loading should take prioirty over image */}
<Avatar
color="gen-ai"
loading={true}
imgUrl="https://cdn.marvel.com/content/1x/x-men_origins_-_gambit_2009_1_art_by_david_yardin.jpg"
ariaLabel="Gen AI assistant generating response"
tooltipText="Gen AI assistant generating response"
/>

<Avatar iconSvg={customIconSvg} ariaLabel="Avatar with custom SVG icon" />
<Avatar color="gen-ai" iconSvg={customIconSvg} ariaLabel="Gen AI avatar with custom SVG icon" />

<br />

<SpaceBetween direction="vertical" size="xxs">
{/* Image with tiny width enforce minimum of 28px */}
<Avatar
ariaLabel="An awesome picture of Wolverine"
imgUrl="https://static1.colliderimages.com/wordpress/wp-content/uploads/2024/08/deadpool-wolverine-hugh-jackman-mask-reveal.jpg"
width={20}
/>

{/* Image with default width of 28px */}
<Avatar
ariaLabel="An awesome picture of Wolverine"
imgUrl="https://static1.colliderimages.com/wordpress/wp-content/uploads/2024/08/deadpool-wolverine-hugh-jackman-mask-reveal.jpg"
/>

{/* Image with custom width should take priority over initials */}
<Avatar
ariaLabel="An awesome picture of Wolverine"
initials="WV"
imgUrl="https://static1.colliderimages.com/wordpress/wp-content/uploads/2024/08/deadpool-wolverine-hugh-jackman-mask-reveal.jpg"
width={50}
/>

{/* Image with custom width and tooltip should take priority over icon */}
<Avatar
ariaLabel="An awesome picture of Wolverine"
tooltipText="Snikt!"
imgUrl="https://static1.colliderimages.com/wordpress/wp-content/uploads/2024/08/deadpool-wolverine-hugh-jackman-mask-reveal.jpg"
iconSvg={customIconSvg}
width={70}
/>

{/* Icon SVG with custom width not allowed */}
<Avatar iconSvg={customIconSvg} ariaLabel="Avatar with custom SVG icon" width={90} />

{/* Initials with custom width not allowed */}
<Avatar
color="gen-ai"
initials="GW"
ariaLabel="Gen AI assistant GW"
tooltipText="Gen AI assistant"
width={110}
/>

{/* Loading with custom width not allowed */}
<Avatar
color="gen-ai"
initials="GW"
loading={true}
ariaLabel="Gen AI assistant GW"
tooltipText="Gen AI assistant"
width={110}
/>
</SpaceBetween>
</TestBed>
</main>
</ScreenshotArea>
Expand Down
31 changes: 31 additions & 0 deletions pages/avatar/width-and-image.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { useState } from "react";

import Header from "@cloudscape-design/components/header";
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");

return (
<SpaceBetween direction="vertical" size="m">
<Header>Input an Image URL and custom size.</Header>

<SpaceBetween direction="horizontal" size="m">
<Input onChange={({ detail }) => setURL(detail.value)} value={url} />

<Input onChange={({ detail }) => setWidth(detail.value)} value={width} inputMode="numeric" type="number" />
</SpaceBetween>

<Avatar ariaLabel="An awesome picture of Wolverine" initials="WV" imgUrl={url} width={Number(width)} />
</SpaceBetween>
);
}
12 changes: 12 additions & 0 deletions src/avatar/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ export interface AvatarProps {
* 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 of the avatar when using a custom image.
* This value corresponds to the `width` CSS-property and will center and crop images using `object-fit: cover`.
* If no width is provided the avatar will use the default width value.
*/
width?: number;
}

export namespace AvatarProps {
Expand Down
24 changes: 22 additions & 2 deletions src/avatar/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,25 @@ 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 <LoadingDots color={color} />;
}

if (imgUrl) {
return <img src={imgUrl} style={{ height: width, width: width }} />;
}

if (initials) {
const letters = initials.length > 2 ? initials.slice(0, 2) : initials;

Expand All @@ -32,7 +46,7 @@ const AvatarContent = ({ color, loading, initials, iconName, iconSvg, iconUrl, a
return <span>{letters}</span>;
}

return <Icon name={iconName} svg={iconSvg} url={iconUrl} alt={ariaLabel} />;
return <Icon name={iconName} svg={iconSvg} url={iconUrl} alt={ariaLabel} size="inherit" />;
};

export default function InternalAvatar({
Expand All @@ -44,13 +58,16 @@ export default function InternalAvatar({
iconName,
iconSvg,
iconUrl,
imgUrl,
width,
__internalRootRef = null,
...rest
}: InternalAvatarProps) {
const handleRef = useRef<HTMLDivElement>(null);
const [showTooltip, setShowTooltip] = useState(false);

const mergedRef = useMergeRefs(handleRef, __internalRootRef);
const computedSize = imgUrl && width && width > 28 ? width : 28;

const tooltipAttributes = {
onFocus: () => {
Expand Down Expand Up @@ -84,6 +101,7 @@ export default function InternalAvatar({
role="img"
aria-label={ariaLabel}
{...tooltipAttributes}
style={{ height: computedSize, width: computedSize }}
>
{showTooltip && tooltipText && (
<Tooltip
Expand All @@ -105,6 +123,8 @@ export default function InternalAvatar({
iconName={iconName}
iconSvg={iconSvg}
iconUrl={iconUrl}
imgUrl={imgUrl}
width={computedSize}
/>
</div>
</div>
Expand Down
8 changes: 7 additions & 1 deletion src/avatar/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,4 +47,11 @@ $avatar-size: 28px;
block-size: inherit;
inline-size: inherit;
overflow: hidden;

img {
@include mixins.border-radius-avatar;
block-size: $avatar-size;
inline-size: $avatar-size;
object-fit: cover;
}
}
Loading