From 097e8404bb579548127221f3f81b2eee09705173 Mon Sep 17 00:00:00 2001 From: Cansu Aksu <45541797+cansuaa@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:05:33 +0300 Subject: [PATCH] feat: Avatar (#8) --- pages/avatar/permutations.page.tsx | 54 ++++++ pages/avatar/simple.page.tsx | 42 +++- .../__snapshots__/documenter.test.ts.snap | 180 +++++++++++++++++- src/__tests__/default-props.tsx | 2 +- src/avatar/__tests__/avatar.test.tsx | 99 +++++++++- src/avatar/index.tsx | 11 +- src/avatar/interfaces.ts | 169 +++++++++++++++- src/avatar/internal.tsx | 102 +++++++++- src/avatar/loading-dots/index.tsx | 21 ++ src/avatar/loading-dots/motion.scss | 112 +++++++++++ src/avatar/loading-dots/styles.scss | 34 ++++ src/avatar/mixins.scss | 11 ++ src/avatar/styles.scss | 50 ++++- src/internal/shared.scss | 28 +++ src/test-utils/dom/avatar/index.ts | 6 + test/functional/avatar/simple.test.ts | 18 -- 16 files changed, 891 insertions(+), 48 deletions(-) create mode 100644 pages/avatar/permutations.page.tsx create mode 100644 src/avatar/loading-dots/index.tsx create mode 100644 src/avatar/loading-dots/motion.scss create mode 100644 src/avatar/loading-dots/styles.scss create mode 100644 src/avatar/mixins.scss delete mode 100644 test/functional/avatar/simple.test.ts diff --git a/pages/avatar/permutations.page.tsx b/pages/avatar/permutations.page.tsx new file mode 100644 index 0000000..26fb98e --- /dev/null +++ b/pages/avatar/permutations.page.tsx @@ -0,0 +1,54 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Avatar } from "../../lib/components"; +import { TestBed } from "../app/test-bed"; +import { ScreenshotArea } from "../screenshot-area"; + +const customIconSvg = ( + +); + +export default function AvatarPage() { + return ( + +

Avatar

+
+ + + + + + + + + + + + + +
+
+ ); +} diff --git a/pages/avatar/simple.page.tsx b/pages/avatar/simple.page.tsx index 6c31099..39fe54d 100644 --- a/pages/avatar/simple.page.tsx +++ b/pages/avatar/simple.page.tsx @@ -1,19 +1,43 @@ // 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 Toggle from "@cloudscape-design/components/toggle"; +import { useState } from "react"; import { Avatar } from "../../lib/components"; -import { TestBed } from "../app/test-bed"; -import { ScreenshotArea } from "../screenshot-area"; export default function AvatarPage() { + const [loading, setLoading] = useState(false); + const [initials, setInitials] = useState(false); + return ( - + <>

Avatar

-
- - - -
-
+ + + + + setLoading(detail.checked)}> + Loading + + + setInitials(detail.checked)}> + Initials + + + ); } diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 6468e45..1084506 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -7,12 +7,190 @@ exports[`definition for avatar matches the snapshot > avatar 1`] = ` "name": "Avatar", "properties": [ { + "description": "Text to describe the avatar for assistive technology. +When more than one avatar is used, provide a unique label for each. +For example, "User avatar" and "AI assistant avatar" or "Your avatar" and "User avatar for John Doe". +If \`tooltipText\` or \`initials\` are used make sure to include them in the \`ariaLabel\`. +", + "name": "ariaLabel", + "optional": false, + "type": "string", + }, + { + "defaultValue": ""default"", + "description": "Determines the color of the avatar. +Use \`gen-ai\` for AI assistants and \`default\` otherwise.", + "inlineType": { + "name": "AvatarProps.Color", + "type": "union", + "values": [ + "default", + "gen-ai", + ], + }, + "name": "color", + "optional": true, + "type": "string", + }, + { + "defaultValue": ""user-profile"", + "description": "Specifies the icon to be displayed as Avatar. +Use \`gen-ai\` icon for AI assistants. By default \`user-profile\` icon is used. +If you set both \`iconName\` and \`initials\`, \`initials\` will take precedence. +", + "inlineType": { + "name": "IconProps.Name", + "type": "union", + "values": [ + "add-plus", + "anchor-link", + "angle-left-double", + "angle-left", + "angle-right-double", + "angle-right", + "angle-up", + "angle-down", + "arrow-left", + "arrow-right", + "audio-full", + "audio-half", + "audio-off", + "bug", + "call", + "calendar", + "caret-down-filled", + "caret-down", + "caret-left-filled", + "caret-right-filled", + "caret-up-filled", + "caret-up", + "check", + "contact", + "close", + "copy", + "delete-marker", + "download", + "drag-indicator", + "edit", + "ellipsis", + "envelope", + "expand", + "external", + "file-open", + "file", + "filter", + "flag", + "folder-open", + "folder", + "gen-ai", + "group-active", + "group", + "heart", + "heart-filled", + "insert-row", + "key", + "keyboard", + "lock-private", + "menu", + "microphone", + "microphone-off", + "multiscreen", + "notification", + "redo", + "refresh", + "remove", + "resize-area", + "script", + "search", + "security", + "settings", + "send", + "share", + "shrink", + "star-filled", + "star-half", + "star", + "status-in-progress", + "status-info", + "status-negative", + "status-pending", + "status-positive", + "status-stopped", + "status-warning", + "subtract-minus", + "suggestions", + "thumbs-down-filled", + "thumbs-down", + "thumbs-up-filled", + "thumbs-up", + "ticket", + "treeview-collapse", + "treeview-expand", + "undo", + "unlocked", + "upload-download", + "upload", + "user-profile-active", + "user-profile", + "video-off", + "video-on", + "video-unavailable", + "view-full", + "view-horizontal", + "view-vertical", + "zoom-in", + "zoom-out", + "zoom-to-fit", + ], + }, + "name": "iconName", + "optional": true, + "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. +", + "name": "iconUrl", + "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. +Use it to define initials that uniquely identify the avatar's owner. +When you use this property, make sure to include it in the \`ariaLabel\`. +", "name": "initials", "optional": true, "type": "string", }, + { + "description": "When set to true, a loading indicator is shown in avatar.", + "name": "loading", + "optional": true, + "type": "boolean", + }, + { + "description": "The text content shown in the avatar's tooltip. +When you use this property, make sure to include it in the \`ariaLabel\`. +", + "name": "tooltipText", + "optional": true, + "type": "string", + }, + ], + "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. +", + "isDefault": false, + "name": "iconSvg", + }, ], - "regions": [], "releaseStatus": "stable", } `; diff --git a/src/__tests__/default-props.tsx b/src/__tests__/default-props.tsx index 6162872..f6b664d 100644 --- a/src/__tests__/default-props.tsx +++ b/src/__tests__/default-props.tsx @@ -3,7 +3,7 @@ import type { AvatarProps } from "../../lib/components"; const avatarProps: AvatarProps = { - initials: "", + ariaLabel: "Avatar", }; export const defaultProps = { avatar: avatarProps, diff --git a/src/avatar/__tests__/avatar.test.tsx b/src/avatar/__tests__/avatar.test.tsx index e71a58b..7cfe2ca 100644 --- a/src/avatar/__tests__/avatar.test.tsx +++ b/src/avatar/__tests__/avatar.test.tsx @@ -1,19 +1,104 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { cleanup, render } from "@testing-library/react"; -import { afterEach, describe, expect, test } from "vitest"; -import Avatar from "../../../lib/components/avatar"; +import * as ComponentToolkitInternal from "@cloudscape-design/component-toolkit/internal"; +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import Avatar, { AvatarProps } from "../../../lib/components/avatar"; +import loadingDotsStyles from "../../../lib/components/avatar/loading-dots/styles.selectors.js"; import createWrapper from "../../../lib/components/test-utils/dom"; +const defaultAvatarProps: AvatarProps = { ariaLabel: "Avatar" }; + +function renderAvatar(props: AvatarProps) { + const { container } = render(); + + return createWrapper(container).findAvatar()!; +} + describe("Avatar", () => { afterEach(() => { cleanup(); }); - test("renders avatar", () => { - render(); - const wrapper = createWrapper().findAvatar()!; + test("Renders avatar with initials", () => { + const initials = "JD"; + const wrapper = renderAvatar({ ...defaultAvatarProps, initials }); + + expect(wrapper.getElement().textContent).toBe(initials); + }); + + test("Shows tooltip on focus", () => { + const tooltipText = "Jane Doe"; + const wrapper = renderAvatar({ ...defaultAvatarProps, color: "default", tooltipText }); + wrapper.focus(); + expect(wrapper.findTooltip()?.getElement().textContent).toBe(tooltipText); + + wrapper?.blur(); + expect(wrapper.findTooltip()?.getElement()).toBeUndefined(); + }); + + test("Shows tooltip on mouse enter", () => { + const tooltipText = "Jane Doe"; + const wrapper = renderAvatar({ ...defaultAvatarProps, color: "default", tooltipText }); + act(() => { + fireEvent.mouseEnter(wrapper.getElement()); + }); + expect(wrapper.findTooltip()?.getElement().textContent).toBe(tooltipText); + + act(() => { + fireEvent.mouseLeave(wrapper.getElement()); + }); + expect(wrapper.findTooltip()?.getElement()).toBeUndefined(); + }); + + test("Does not render tooltip when tooltipText is not passed", () => { + const wrapper = renderAvatar({ ...defaultAvatarProps, color: "default" }); + wrapper.focus(); + expect(wrapper.findTooltip()?.getElement()).toBeUndefined(); + }); + + test("Renders avatar in loading state", () => { + const wrapper = renderAvatar({ ...defaultAvatarProps, loading: true }); + + const loading = wrapper.findByClassName(loadingDotsStyles.root)?.getElement(); + expect(loading).toBeInTheDocument(); + }); + + test("Loading takes precedence over initials", () => { + const initials = "JD"; + const wrapper = renderAvatar({ ...defaultAvatarProps, initials, 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"); + + const initials = "JDD"; + renderAvatar({ ...defaultAvatarProps, initials }); + + expect(warnOnce).toHaveBeenCalledTimes(1); + expect(warnOnce).toHaveBeenCalledWith( + "Avatar", + `"initials" is longer than 2 characters. Only the first two characters are shown.`, + ); + }); + + // test("a11y - Validates", async () => { + // const props: AvatarProps = { + // color: "default", + // initials: "JD", + // tooltipText: "Jane Doe", + // ariaLabel: "User avatar", + // }; + // const { container } = render(); + // await expect(container).toValidateA11y(); + // }); - expect(wrapper.getElement().textContent).toBe("GW"); + test("a11y - ariaLabel is directly used", () => { + const ariaLabel = "User avatar JD Jane Doe"; + const wrapper = renderAvatar({ ariaLabel, initials: "JD", tooltipText: "Jane Doe" }); + expect(wrapper.getElement()).toHaveAttribute("aria-label", ariaLabel); }); }); diff --git a/src/avatar/index.tsx b/src/avatar/index.tsx index ca22bf4..07a5118 100644 --- a/src/avatar/index.tsx +++ b/src/avatar/index.tsx @@ -2,14 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import useBaseComponent from "../internal/base-component/use-base-component"; import { applyDisplayName } from "../internal/utils/apply-display-name"; -import type { AvatarProps } from "./interfaces"; -import { InternalAvatar } from "./internal"; +import { AvatarProps } from "./interfaces"; +import InternalAvatar from "./internal"; export type { AvatarProps }; -export default function Avatar(props: AvatarProps) { - const baseComponentProps = useBaseComponent("Avatar"); - return ; +export default function Avatar({ color = "default", iconName = "user-profile", ...props }: AvatarProps) { + const baseComponentProps = useBaseComponent("Avatar", { props: { color, iconName } }); + return ; } - applyDisplayName(Avatar, "Avatar"); diff --git a/src/avatar/interfaces.ts b/src/avatar/interfaces.ts index 334e785..e6ff62e 100644 --- a/src/avatar/interfaces.ts +++ b/src/avatar/interfaces.ts @@ -2,7 +2,174 @@ // SPDX-License-Identifier: Apache-2.0 export interface AvatarProps { + /** + * Determines the color of the avatar. + * Use `gen-ai` for AI assistants and `default` otherwise. + */ + color?: AvatarProps.Color; + + /** + * The text content shown in the avatar's tooltip. + * + * When you use this property, make sure to include it in the `ariaLabel`. + */ + tooltipText?: string; + + /** + * The text content shown directly in the avatar's body. + * Can be 1 or 2 symbols long, every subsequent symbol is ignored. + * Use it to define initials that uniquely identify the avatar's owner. + * + * When you use this property, make sure to include it in the `ariaLabel`. + */ initials?: string; + + /** + * When set to true, a loading indicator is shown in avatar. + */ + loading?: boolean; + + /** + * Text to describe the avatar for assistive technology. + * When more than one avatar is used, provide a unique label for each. + * For example, "User avatar" and "AI assistant avatar" or "Your avatar" and "User avatar for John Doe". + * + * If `tooltipText` or `initials` are used make sure to include them in the `ariaLabel`. + */ + ariaLabel: string; + + /** + * Specifies the icon to be displayed as Avatar. + * Use `gen-ai` icon for AI assistants. By default `user-profile` icon is used. + * + * If you set both `iconName` and `initials`, `initials` will take precedence. + */ + iconName?: IconProps.Name; + + /** + * 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. + */ + 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; } -export namespace AvatarProps {} +export namespace AvatarProps { + export type Color = "default" | "gen-ai"; +} + +// This is added here because cross package reference doesn't work in documenter and icon names are not populated +// It'll be replaced by documenter implementation once documenter is updated to handle cross package reference +namespace IconProps { + // Why not enums? Explained there + // https://stackoverflow.com/questions/52393730/typescript-string-literal-union-type-from-enum + export type Name = + | "add-plus" + | "anchor-link" + | "angle-left-double" + | "angle-left" + | "angle-right-double" + | "angle-right" + | "angle-up" + | "angle-down" + | "arrow-left" + | "arrow-right" + | "audio-full" + | "audio-half" + | "audio-off" + | "bug" + | "call" + | "calendar" + | "caret-down-filled" + | "caret-down" + | "caret-left-filled" + | "caret-right-filled" + | "caret-up-filled" + | "caret-up" + | "check" + | "contact" + | "close" + | "copy" + | "delete-marker" + | "download" + | "drag-indicator" + | "edit" + | "ellipsis" + | "envelope" + | "expand" + | "external" + | "file-open" + | "file" + | "filter" + | "flag" + | "folder-open" + | "folder" + | "gen-ai" + | "group-active" + | "group" + | "heart" + | "heart-filled" + | "insert-row" + | "key" + | "keyboard" + | "lock-private" + | "menu" + | "microphone" + | "microphone-off" + | "multiscreen" + | "notification" + | "redo" + | "refresh" + | "remove" + | "resize-area" + | "script" + | "search" + | "security" + | "settings" + | "send" + | "share" + | "shrink" + | "star-filled" + | "star-half" + | "star" + | "status-in-progress" + | "status-info" + | "status-negative" + | "status-pending" + | "status-positive" + | "status-stopped" + | "status-warning" + | "subtract-minus" + | "suggestions" + | "thumbs-down-filled" + | "thumbs-down" + | "thumbs-up-filled" + | "thumbs-up" + | "ticket" + | "treeview-collapse" + | "treeview-expand" + | "undo" + | "unlocked" + | "upload-download" + | "upload" + | "user-profile-active" + | "user-profile" + | "video-off" + | "video-on" + | "video-unavailable" + | "view-full" + | "view-horizontal" + | "view-vertical" + | "zoom-in" + | "zoom-out" + | "zoom-to-fit"; +} diff --git a/src/avatar/internal.tsx b/src/avatar/internal.tsx index 3d4dbe7..c697d07 100644 --- a/src/avatar/internal.tsx +++ b/src/avatar/internal.tsx @@ -1,15 +1,109 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +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 clsx from "clsx"; +import { useRef, useState } from "react"; + import { getDataAttributes } from "../internal/base-component/get-data-attributes"; import { InternalBaseComponentProps } from "../internal/base-component/use-base-component"; +import { useMergeRefs } from "../internal/utils/use-merge-refs"; -import { AvatarProps } from "./interfaces"; +import { AvatarProps } from "./interfaces.js"; +import LoadingDots from "./loading-dots"; import styles from "./styles.css.js"; -export function InternalAvatar({ initials, __internalRootRef, ...rest }: AvatarProps & InternalBaseComponentProps) { +export interface InternalAvatarProps extends AvatarProps, InternalBaseComponentProps {} + +const AvatarContent = ({ color, loading, initials, iconName, iconSvg, iconUrl, ariaLabel }: AvatarProps) => { + if (loading) { + return ; + } + + if (initials) { + const letters = initials.length > 2 ? initials.slice(0, 2) : initials; + + if (initials.length > 2) { + warnOnce("Avatar", `"initials" is longer than 2 characters. Only the first two characters are shown.`); + } + + return {letters}; + } + + return ; +}; + +export default function InternalAvatar({ + color, + tooltipText, + initials, + loading = false, + ariaLabel, + iconName, + iconSvg, + iconUrl, + __internalRootRef = null, + ...rest +}: InternalAvatarProps) { + const handleRef = useRef(null); + const [showTooltip, setShowTooltip] = useState(false); + + const mergedRef = useMergeRefs(handleRef, __internalRootRef); + + const tooltipAttributes = { + onFocus: () => { + setShowTooltip(true); + }, + onBlur: () => { + setShowTooltip(false); + }, + onMouseEnter: () => { + setShowTooltip(true); + }, + onMouseLeave: () => { + setShowTooltip(false); + }, + onTouchStart: () => { + setShowTooltip(true); + }, + onTouchEnd: () => { + setShowTooltip(false); + }, + }; + return ( -
- {initials} +
+ {showTooltip && tooltipText && ( + setShowTooltip(false) }} + /> + )} + +
+ +
); } diff --git a/src/avatar/loading-dots/index.tsx b/src/avatar/loading-dots/index.tsx new file mode 100644 index 0000000..6777707 --- /dev/null +++ b/src/avatar/loading-dots/index.tsx @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import clsx from "clsx"; +import styles from "./styles.css.js"; + +interface LoadingDotsProps { + color?: string; +} + +export default function LoadingDots({ color }: LoadingDotsProps) { + 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 new file mode 100644 index 0000000..72c2e32 --- /dev/null +++ b/src/avatar/loading-dots/motion.scss @@ -0,0 +1,112 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ +@use "../../internal/shared" as shared; +@use "../../../node_modules/@cloudscape-design/design-tokens/index.scss" as cs; + +.gen-ai { + &::before { + content: ""; + position: absolute; + inline-size: inherit; + block-size: inherit; + inset-block-start: 50%; + inset-inline-start: 50%; + transform: translate(-50%, -50%); + background: cs.$color-background-avatar-gen-ai; + + @include shared.with-motion { + animation: gradientMove cs.$motion-duration-avatar-gen-ai-gradient infinite + cs.$motion-easing-avatar-gen-ai-gradient; + } + } + + @include shared.with-direction("rtl") { + &::before { + @include shared.with-motion { + animation: gradientMoveReverse cs.$motion-duration-avatar-gen-ai-gradient infinite + cs.$motion-easing-avatar-gen-ai-gradient; + } + } + } +} + +// Gradient rotation animation +@keyframes gradientMove { + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + + 50% { + block-size: 44px; + inline-size: 44px; + } + + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } +} + +@keyframes gradientMoveReverse { + 0% { + transform: translate(-50%, -50%) rotate(360deg); + inset-inline-start: -50%; + } + + 50% { + block-size: 44px; + inline-size: 44px; + inset-inline-start: -100%; + } + + 100% { + transform: translate(-50%, -50%) rotate(0deg); + inset-inline-start: -50%; + } +} + +.dot { + @include shared.with-motion { + animation: dotsDancing cs.$motion-duration-avatar-loading-dots infinite ease-in-out; + } +} + +// Dots dancing animation +.dot:nth-child(1) { + @include shared.with-motion { + animation-delay: 100ms; + } +} + +.dot:nth-child(2) { + @include shared.with-motion { + animation-delay: 200ms; + } +} + +.dot:nth-child(3) { + @include shared.with-motion { + animation-delay: 300ms; + } +} + +.dot:last-child { + @include shared.with-motion { + margin-inline-end: 0; + } +} + +@keyframes dotsDancing { + 0% { + transform: translateY(0px); + } + + 28% { + transform: translateY(-4px); + } + + 44% { + transform: translateY(0px); + } +} diff --git a/src/avatar/loading-dots/styles.scss b/src/avatar/loading-dots/styles.scss new file mode 100644 index 0000000..373fc79 --- /dev/null +++ b/src/avatar/loading-dots/styles.scss @@ -0,0 +1,34 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ +@use "../../../node_modules/@cloudscape-design/design-tokens/index.scss" as cs; +@use '../mixins.scss' as mixins; +@use './motion.scss'; + +$dot-size: 4px; + +.root { + inline-size: inherit; + block-size: inherit; + @include mixins.border-radius-avatar; + display: flex; + justify-content: center; + align-items: center; + position: relative; + overflow: hidden; +} + +.typing { + align-items: center; + justify-content: space-between; + display: flex; + inline-size: 18px; +} + +.dot { + background-color: cs.$color-text-avatar; + @include mixins.border-radius-avatar; + block-size: $dot-size; + inline-size: $dot-size; +} diff --git a/src/avatar/mixins.scss b/src/avatar/mixins.scss new file mode 100644 index 0000000..1326dc0 --- /dev/null +++ b/src/avatar/mixins.scss @@ -0,0 +1,11 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@mixin border-radius-avatar { + border-start-start-radius: 50%; + border-start-end-radius: 50%; + border-end-start-radius: 50%; + border-end-end-radius: 50%; +} diff --git a/src/avatar/styles.scss b/src/avatar/styles.scss index 93f2b4f..7ec4f0d 100644 --- a/src/avatar/styles.scss +++ b/src/avatar/styles.scss @@ -1,3 +1,51 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ +@use "../../node_modules/@cloudscape-design/design-tokens/index.scss" as cs; +@use "./mixins.scss" as mixins; +@use "../internal/shared" as shared; + +$avatar-size: 28px; + .root { - /* used in test-utils */ + @include shared.styles-reset; + + color: cs.$color-text-avatar; + block-size: $avatar-size; + inline-size: $avatar-size; + position: relative; + @include mixins.border-radius-avatar; + + &.avatar-color-default { + background-color: cs.$color-background-avatar-default; + } + + &.avatar-color-gen-ai { + background: cs.$color-background-avatar-gen-ai; + } + + &.initials { + font-family: cs.$font-family-base; + font-size: cs.$font-size-body-s; + line-height: cs.$line-height-body-s; + font-weight: cs.$font-weight-heading-l; + } + + &:focus { + outline: none; + + @include shared.when-visible { + @include shared.focus-highlight(1px, 50%); + } + } +} + +.content { + display: flex; + align-items: center; + justify-content: center; + block-size: inherit; + inline-size: inherit; + overflow: hidden; } diff --git a/src/internal/shared.scss b/src/internal/shared.scss index 0c54dcb..257c1e9 100644 --- a/src/internal/shared.scss +++ b/src/internal/shared.scss @@ -51,3 +51,31 @@ @content; } } + +@mixin styles-reset { + border-collapse: separate; + border-spacing: 0; + box-sizing: border-box; + caption-side: top; + cursor: auto; + direction: inherit; + empty-cells: show; + font-family: serif; + font-size: medium; + font-style: normal; + font-variant: normal; + font-weight: 400; + font-stretch: normal; + line-height: normal; + hyphens: none; + letter-spacing: normal; + list-style: disc outside none; + tab-size: 8; + text-align: start; + text-indent: 0; + text-shadow: none; + text-transform: none; + visibility: visible; + white-space: normal; + word-spacing: normal; +} diff --git a/src/test-utils/dom/avatar/index.ts b/src/test-utils/dom/avatar/index.ts index 7f96dbe..1ee9188 100644 --- a/src/test-utils/dom/avatar/index.ts +++ b/src/test-utils/dom/avatar/index.ts @@ -1,8 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import TooltipWrapper from "@cloudscape-design/components/test-utils/dom/internal/tooltip"; import { ComponentWrapper } from "@cloudscape-design/test-utils-core/dom"; +import createWrapper from ".."; import avatarStyles from "../../../avatar/styles.selectors.js"; export default class AvatarWrapper extends ComponentWrapper { static rootSelector: string = avatarStyles.root; + + findTooltip(): TooltipWrapper | null { + return createWrapper().findComponent(`.${TooltipWrapper.rootSelector}`, TooltipWrapper); + } } diff --git a/test/functional/avatar/simple.test.ts b/test/functional/avatar/simple.test.ts deleted file mode 100644 index 29c93b5..0000000 --- a/test/functional/avatar/simple.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import { BasePageObject } from "@cloudscape-design/browser-test-tools/page-objects"; -import { describe, expect, test } from "vitest"; -import createWrapper from "../../../lib/components/test-utils/selectors"; -import { setupTest } from "../../utils"; - -const wrapper = createWrapper(); -const avatarWrapper = wrapper.findAvatar(); - -describe("avatar functional tests", () => { - test( - "renders", - setupTest("/index.html#/avatar/simple", BasePageObject, async (page) => { - await expect(page.getElementsText(avatarWrapper.toSelector())).resolves.toEqual(["GW"]); - }), - ); -});