From ad86c75f100b7b87dd70d71fd4a6b598899c05e4 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Thu, 20 Jun 2024 18:10:53 +0200 Subject: [PATCH 01/10] feat: Avatar --- pages/avatar/permutations.page.tsx | 54 ++++++++ pages/avatar/simple.page.tsx | 42 +++++-- .../__snapshots__/documenter.test.ts.snap | 74 ++++++++++- src/avatar/__tests__/avatar.test.tsx | 99 +++++++++++++-- src/avatar/index.tsx | 11 +- src/avatar/interfaces.ts | 64 +++++++++- src/avatar/internal.tsx | 102 ++++++++++++++- src/avatar/loading-dots/index.tsx | 21 ++++ src/avatar/loading-dots/motion.scss | 117 ++++++++++++++++++ src/avatar/loading-dots/styles.scss | 36 ++++++ src/avatar/mixins.scss | 11 ++ src/avatar/styles.scss | 51 +++++++- src/test-utils/dom/avatar/index.ts | 6 + test/functional/avatar/simple.test.ts | 18 --- 14 files changed, 659 insertions(+), 47 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..3213df8 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 b13c2b4..54d0863 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -7,12 +7,84 @@ 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", + }, + { + "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. +", + "name": "iconName", + "optional": true, + "type": "IconProps.Name", + }, + { + "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/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..a8b8fd1 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", ...props }: AvatarProps) { + const baseComponentProps = useBaseComponent("Avatar", { props: { color } }); + return ; } - applyDisplayName(Avatar, "Avatar"); diff --git a/src/avatar/interfaces.ts b/src/avatar/interfaces.ts index 334e785..9c82301 100644 --- a/src/avatar/interfaces.ts +++ b/src/avatar/interfaces.ts @@ -1,8 +1,70 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { IconProps } from "@cloudscape-design/components/icon"; + 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"; +} diff --git a/src/avatar/internal.tsx b/src/avatar/internal.tsx index 3d4dbe7..c73379e 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..8bb6d31 --- /dev/null +++ b/src/avatar/loading-dots/motion.scss @@ -0,0 +1,117 @@ +/* + 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 { + &::after { + 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 { + // waiting for motion tokens to go public + // https://github.com/cloudscape-design/components/pull/2400 + animation: gradientMove 3.6s infinite cubic-bezier(0.7, 0, 0.3, 1); + // animation: gradientMove cs.$motion-duration-avatar-gen-ai-gradient infinite + // cs.$motion-easing-avatar-gen-ai-gradient; + } + } + + @include shared.with-direction("rtl") { + &::after { + @include shared.with-motion { + animation: gradientMoveReverse 3.6s infinite cubic-bezier(0.7, 0, 0.3, 1); + // animation: gradientMoveReverse awsui.$motion-duration-avatar-gen-ai-gradient infinite + // awsui.$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; + animation: dotsDancing 1.2s 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..7d31e5a --- /dev/null +++ b/src/avatar/loading-dots/styles.scss @@ -0,0 +1,36 @@ +/* + 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; + z-index: 0; + 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; + z-index: 1; +} 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..8324f63 100644 --- a/src/avatar/styles.scss +++ b/src/avatar/styles.scss @@ -1,3 +1,52 @@ +/* + 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 */ + // do we need this? + // @include styles.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/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"]); - }), - ); -}); From 07d232ae91c42edc3b16f59273b866304a6ce80e Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Fri, 21 Jun 2024 10:02:51 +0200 Subject: [PATCH 02/10] Update default props --- src/__tests__/default-props.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From 3a258ba17d3c22697b2e0becf632a1be86fc8096 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Fri, 21 Jun 2024 17:08:06 +0200 Subject: [PATCH 03/10] remove padding from simple avatar page --- pages/avatar/simple.page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/avatar/simple.page.tsx b/pages/avatar/simple.page.tsx index 3213df8..39fe54d 100644 --- a/pages/avatar/simple.page.tsx +++ b/pages/avatar/simple.page.tsx @@ -11,7 +11,7 @@ export default function AvatarPage() { const [initials, setInitials] = useState(false); return ( -
+ <>

Avatar

-
+ ); } From 1f18cd3a18f33fbcc71861d9ff8a086f6ddcd7ab Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Mon, 24 Jun 2024 16:36:20 +0300 Subject: [PATCH 04/10] uncomment tooltip behavior and replace motions with tokens --- src/avatar/internal.tsx | 2 +- src/avatar/loading-dots/motion.scss | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/avatar/internal.tsx b/src/avatar/internal.tsx index c73379e..20ef0a5 100644 --- a/src/avatar/internal.tsx +++ b/src/avatar/internal.tsx @@ -89,7 +89,7 @@ export default function InternalAvatar({ value={tooltipText} trackRef={handleRef} // This is added to ensure tooltip is closed when clicked for consistency with other tooltip usages - // contentAttributes={{ onPointerDown: () => setShowTooltip(false) }} + contentAttributes={{ onPointerDown: () => setShowTooltip(false) }} /> )} diff --git a/src/avatar/loading-dots/motion.scss b/src/avatar/loading-dots/motion.scss index 8bb6d31..f30903d 100644 --- a/src/avatar/loading-dots/motion.scss +++ b/src/avatar/loading-dots/motion.scss @@ -17,20 +17,16 @@ background: cs.$color-background-avatar-gen-ai; @include shared.with-motion { - // waiting for motion tokens to go public - // https://github.com/cloudscape-design/components/pull/2400 - animation: gradientMove 3.6s infinite cubic-bezier(0.7, 0, 0.3, 1); - // animation: gradientMove cs.$motion-duration-avatar-gen-ai-gradient infinite - // cs.$motion-easing-avatar-gen-ai-gradient; + animation: gradientMove cs.$motion-duration-avatar-gen-ai-gradient infinite + cs.$motion-easing-avatar-gen-ai-gradient; } } @include shared.with-direction("rtl") { &::after { @include shared.with-motion { - animation: gradientMoveReverse 3.6s infinite cubic-bezier(0.7, 0, 0.3, 1); - // animation: gradientMoveReverse awsui.$motion-duration-avatar-gen-ai-gradient infinite - // awsui.$motion-easing-avatar-gen-ai-gradient; + animation: gradientMoveReverse awsui.$motion-duration-avatar-gen-ai-gradient infinite + awsui.$motion-easing-avatar-gen-ai-gradient; } } } From c9bcad86486daa648dbb8b8582233e4dfe3f74c4 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Mon, 24 Jun 2024 16:40:14 +0300 Subject: [PATCH 05/10] fix token import --- src/avatar/loading-dots/motion.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/avatar/loading-dots/motion.scss b/src/avatar/loading-dots/motion.scss index f30903d..8174229 100644 --- a/src/avatar/loading-dots/motion.scss +++ b/src/avatar/loading-dots/motion.scss @@ -25,8 +25,8 @@ @include shared.with-direction("rtl") { &::after { @include shared.with-motion { - animation: gradientMoveReverse awsui.$motion-duration-avatar-gen-ai-gradient infinite - awsui.$motion-easing-avatar-gen-ai-gradient; + animation: gradientMoveReverse cs.$motion-duration-avatar-gen-ai-gradient infinite + cs.$motion-easing-avatar-gen-ai-gradient; } } } From 02e739243a3de415a7ebe7a6524e5794f943d4ab Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Mon, 24 Jun 2024 18:36:21 +0300 Subject: [PATCH 06/10] fix api description of ariaLabel --- src/__tests__/__snapshots__/documenter.test.ts.snap | 2 +- src/avatar/interfaces.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 54d0863..90b4395 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -9,7 +9,7 @@ exports[`definition for avatar matches the snapshot > avatar 1`] = ` { "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". +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", diff --git a/src/avatar/interfaces.ts b/src/avatar/interfaces.ts index 9c82301..65ecce6 100644 --- a/src/avatar/interfaces.ts +++ b/src/avatar/interfaces.ts @@ -34,7 +34,7 @@ export interface AvatarProps { /** * 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". + * 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". */ From 693d366c7c3a1fa50052fc3edebf117efac67394 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Wed, 26 Jun 2024 10:08:36 +0300 Subject: [PATCH 07/10] add icon names type to avatar interface temporarily --- .../__snapshots__/documenter.test.ts.snap | 107 ++++++++++++++++- src/avatar/interfaces.ts | 109 +++++++++++++++++- 2 files changed, 213 insertions(+), 3 deletions(-) diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 90b4395..4cc63e4 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -37,9 +37,114 @@ Use \`gen-ai\` for AI assistants and \`default\` otherwise.", 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": "IconProps.Name", + "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. diff --git a/src/avatar/interfaces.ts b/src/avatar/interfaces.ts index 65ecce6..4a0a213 100644 --- a/src/avatar/interfaces.ts +++ b/src/avatar/interfaces.ts @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { IconProps } from "@cloudscape-design/components/icon"; - export interface AvatarProps { /** * Determines the color of the avatar. @@ -68,3 +66,110 @@ export interface 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"; +} From 3fb5816952d5ee1152ef729ed2a506bb0bd76dea Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Wed, 26 Jun 2024 13:02:06 +0300 Subject: [PATCH 08/10] fixes for comments --- src/__tests__/__snapshots__/documenter.test.ts.snap | 9 +++++---- src/avatar/index.tsx | 6 +++--- src/avatar/interfaces.ts | 8 ++++---- src/avatar/internal.tsx | 2 +- src/avatar/loading-dots/motion.scss | 3 +-- src/avatar/loading-dots/styles.scss | 1 - 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 4cc63e4..a6ca443 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -10,7 +10,7 @@ exports[`definition for avatar matches the snapshot > avatar 1`] = ` "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". +If \`tooltipText\` or \`initials\` are used make sure to include them in the \`ariaLabel\`. ", "name": "ariaLabel", "optional": false, @@ -33,8 +33,9 @@ Use \`gen-ai\` for AI assistants and \`default\` otherwise.", "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. +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": { @@ -159,7 +160,7 @@ If you set both \`iconUrl\` and \`iconSvg\`, \`iconSvg\` will take precedence. "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". +When you use this property, make sure to include it in the \`ariaLabel\`. ", "name": "initials", "optional": true, @@ -173,7 +174,7 @@ When you use this property, make sure to include it in the "ariaLabel". }, { "description": "The text content shown in the avatar's tooltip. -When you use this property, make sure to include it in the "ariaLabel". +When you use this property, make sure to include it in the \`ariaLabel\`. ", "name": "tooltipText", "optional": true, diff --git a/src/avatar/index.tsx b/src/avatar/index.tsx index a8b8fd1..07a5118 100644 --- a/src/avatar/index.tsx +++ b/src/avatar/index.tsx @@ -7,8 +7,8 @@ import InternalAvatar from "./internal"; export type { AvatarProps }; -export default function Avatar({ color = "default", ...props }: AvatarProps) { - const baseComponentProps = useBaseComponent("Avatar", { props: { color } }); - 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 4a0a213..e6ff62e 100644 --- a/src/avatar/interfaces.ts +++ b/src/avatar/interfaces.ts @@ -11,7 +11,7 @@ export interface AvatarProps { /** * The text content shown in the avatar's tooltip. * - * When you use this property, make sure to include it in the "ariaLabel". + * When you use this property, make sure to include it in the `ariaLabel`. */ tooltipText?: string; @@ -20,7 +20,7 @@ export interface AvatarProps { * 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". + * When you use this property, make sure to include it in the `ariaLabel`. */ initials?: string; @@ -34,13 +34,13 @@ export interface AvatarProps { * 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". + * 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. + * 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. */ diff --git a/src/avatar/internal.tsx b/src/avatar/internal.tsx index 20ef0a5..c697d07 100644 --- a/src/avatar/internal.tsx +++ b/src/avatar/internal.tsx @@ -31,7 +31,7 @@ const AvatarContent = ({ color, loading, initials, iconName, iconSvg, iconUrl, a return {letters}; } - return ; + return ; }; export default function InternalAvatar({ diff --git a/src/avatar/loading-dots/motion.scss b/src/avatar/loading-dots/motion.scss index 8174229..3dc50e5 100644 --- a/src/avatar/loading-dots/motion.scss +++ b/src/avatar/loading-dots/motion.scss @@ -68,8 +68,7 @@ .dot { @include shared.with-motion { - // animation: dotsDancing cs.$motion-duration-avatar-loading-dots infinite ease-in-out; - animation: dotsDancing 1.2s infinite ease-in-out; + animation: dotsDancing cs.$motion-duration-avatar-loading-dots infinite ease-in-out; } } diff --git a/src/avatar/loading-dots/styles.scss b/src/avatar/loading-dots/styles.scss index 7d31e5a..9e054ee 100644 --- a/src/avatar/loading-dots/styles.scss +++ b/src/avatar/loading-dots/styles.scss @@ -16,7 +16,6 @@ $dot-size: 4px; justify-content: center; align-items: center; position: relative; - z-index: 0; overflow: hidden; } From 1c261ae2f6aac2e313a352c0bad6ded89cce7fcd Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Wed, 26 Jun 2024 13:55:05 +0300 Subject: [PATCH 09/10] add and use styles-reset mixin --- src/avatar/styles.scss | 3 +-- src/internal/shared.scss | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/avatar/styles.scss b/src/avatar/styles.scss index 8324f63..7ec4f0d 100644 --- a/src/avatar/styles.scss +++ b/src/avatar/styles.scss @@ -9,8 +9,7 @@ $avatar-size: 28px; .root { - // do we need this? - // @include styles.styles-reset; + @include shared.styles-reset; color: cs.$color-text-avatar; block-size: $avatar-size; 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; +} From 19a380df3084016f0076a414d312277eaecdd080 Mon Sep 17 00:00:00 2001 From: Cansu Aksu Date: Wed, 26 Jun 2024 15:04:33 +0300 Subject: [PATCH 10/10] switch gradient motion to 'before' and remove z-index --- src/avatar/loading-dots/motion.scss | 4 ++-- src/avatar/loading-dots/styles.scss | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/avatar/loading-dots/motion.scss b/src/avatar/loading-dots/motion.scss index 3dc50e5..72c2e32 100644 --- a/src/avatar/loading-dots/motion.scss +++ b/src/avatar/loading-dots/motion.scss @@ -6,7 +6,7 @@ @use "../../../node_modules/@cloudscape-design/design-tokens/index.scss" as cs; .gen-ai { - &::after { + &::before { content: ""; position: absolute; inline-size: inherit; @@ -23,7 +23,7 @@ } @include shared.with-direction("rtl") { - &::after { + &::before { @include shared.with-motion { animation: gradientMoveReverse cs.$motion-duration-avatar-gen-ai-gradient infinite cs.$motion-easing-avatar-gen-ai-gradient; diff --git a/src/avatar/loading-dots/styles.scss b/src/avatar/loading-dots/styles.scss index 9e054ee..373fc79 100644 --- a/src/avatar/loading-dots/styles.scss +++ b/src/avatar/loading-dots/styles.scss @@ -31,5 +31,4 @@ $dot-size: 4px; @include mixins.border-radius-avatar; block-size: $dot-size; inline-size: $dot-size; - z-index: 1; }