Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for Avatar sizes and images. #39

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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,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 = (
<svg
Expand Down Expand Up @@ -38,15 +41,73 @@ 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={smiley}
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 smiley" imgUrl={smiley} width={20} />

{/* Image with default width of 28px */}
<Avatar ariaLabel="An awesome picture of smiley" imgUrl={smiley} />

{/* Image should take priority over initials */}
<Avatar ariaLabel="An awesome picture of smiley" initials="WV" imgUrl={smiley} width={40} />

{/* Image and tooltip should take priority over icon */}
<Avatar
ariaLabel="An awesome picture of smiley"
tooltipText="Snikt!"
imgUrl={smiley}
iconSvg={customIconSvg}
width={60}
/>

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

{/* Icon name with custom width */}
<Avatar color="gen-ai" iconName="gen-ai" ariaLabel="Gen AI assistant" width={80} />

{/* Icon name with custom width */}
<Avatar iconName="calendar" ariaLabel="Gen AI assistant" width={100} />

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

{/* Loading with custom width */}
<Avatar color="gen-ai" initials="GW" loading={true} ariaLabel="Gen AI assistant GW" width={160} />

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

{/* Loading with custom width */}
<Avatar color="gen-ai" initials="GW" loading={true} ariaLabel="Gen AI assistant GW" width={200} />
</SpaceBetween>
</TestBed>
</main>
</ScreenshotArea>
Expand Down
Binary file added pages/avatar/smiley.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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>
);
}
14 changes: 14 additions & 0 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ If you set both \`iconUrl\` and \`iconSvg\`, \`iconSvg\` will take precedence.
"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.
Expand All @@ -78,6 +84,14 @@ When you use this property, make sure to include it in the \`ariaLabel\`.
"optional": true,
"type": "string",
},
{
"description": "Defines the width of the avatar.
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 of 28px.",
"name": "width",
"optional": true,
"type": "number",
},
],
"regions": [
{
Expand Down
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.
* 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 of 28px.
*/
width?: number;
}

export namespace AvatarProps {
Expand Down
35 changes: 30 additions & 5 deletions src/avatar/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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/index.js";

import { getDataAttributes } from "../internal/base-component/get-data-attributes";
import { InternalBaseComponentProps } from "../internal/base-component/use-base-component";
Expand All @@ -17,9 +18,23 @@

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} />;
return <LoadingDots color={color} width={width} />;
}

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

Check warning on line 37 in src/avatar/internal.tsx

View check run for this annotation

Codecov / codecov/patch

src/avatar/internal.tsx#L37

Added line #L37 was not covered by tests
}

if (initials) {
Expand All @@ -29,10 +44,14 @@
warnOnce("Avatar", `"initials" is longer than 2 characters. Only the first two characters are shown.`);
}

return <span>{letters}</span>;
return (
<span style={{ fontSize: `clamp(${awsui.fontSizeBodyS}, calc(0.4px * ${width}), calc(0.4px * ${width}))` }}>
{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 +63,16 @@
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 = width && width > 28 ? width : 28;

const tooltipAttributes = {
onFocus: () => {
Expand Down Expand Up @@ -84,6 +106,7 @@
role="img"
aria-label={ariaLabel}
{...tooltipAttributes}
style={{ height: computedSize, width: computedSize }}
>
{showTooltip && tooltipText && (
<Tooltip
Expand All @@ -96,7 +119,7 @@

{/* aria-hidden is added so that screen readers focus only the parent div */}
{/* when it is not hidden, it becomes unstable in JAWS */}
<div className={styles.content} aria-hidden="true">
<div className={styles.content} aria-hidden="true" style={{ lineHeight: `calc(.8px * ${computedSize})` }}>
<AvatarContent
color={color}
ariaLabel={ariaLabel}
Expand All @@ -105,6 +128,8 @@
iconName={iconName}
iconSvg={iconSvg}
iconUrl={iconUrl}
imgUrl={imgUrl}
width={computedSize}
/>
</div>
</div>
Expand Down
11 changes: 7 additions & 4 deletions src/avatar/loading-dots/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<div className={clsx(styles.root, { [styles["gen-ai"]]: color === "gen-ai" })}>
<div className={styles.typing}>
<div className={styles.dot}></div>
<div className={styles.dot}></div>
<div className={styles.dot}></div>
<div className={styles.dot} style={{ blockSize: dotSize, inlineSize: dotSize }}></div>
<div className={styles.dot} style={{ blockSize: dotSize, inlineSize: dotSize }}></div>
<div className={styles.dot} style={{ blockSize: dotSize, inlineSize: dotSize }}></div>
</div>
</div>
);
Expand Down
10 changes: 5 additions & 5 deletions src/avatar/loading-dots/motion.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
}

50% {
block-size: 44px;
inline-size: 44px;
block-size: 150%;
inline-size: 150%;
}

100% {
Expand All @@ -55,8 +55,8 @@
}

50% {
block-size: 44px;
inline-size: 44px;
block-size: 150%;
inline-size: 150%;
inset-inline-start: -100%;
}

Expand Down Expand Up @@ -103,7 +103,7 @@
}

28% {
transform: translateY(-4px);
transform: translateY(-100%);
}

44% {
Expand Down
2 changes: 1 addition & 1 deletion src/avatar/loading-dots/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ $dot-size: 4px;
align-items: center;
justify-content: space-between;
display: flex;
inline-size: 18px;
inline-size: 64%;
}

.dot {
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