diff --git a/packages/main/cypress/specs/Avatar.cy.tsx b/packages/main/cypress/specs/Avatar.cy.tsx index 49a06abcf724..8d78831fc15b 100644 --- a/packages/main/cypress/specs/Avatar.cy.tsx +++ b/packages/main/cypress/specs/Avatar.cy.tsx @@ -5,6 +5,7 @@ import "@ui5/webcomponents-icons/dist/alert.js"; import "@ui5/webcomponents-icons/dist/person-placeholder.js"; import "@ui5/webcomponents-icons/dist/accelerated.js"; import "@ui5/webcomponents-icons/dist/accept.js"; +import "@ui5/webcomponents-icons/dist/edit.js"; import "@ui5/webcomponents-icons/dist/message-error.js"; import "@ui5/webcomponents-icons/dist/information.js"; import "@ui5/webcomponents-icons/dist/ai.js"; @@ -828,6 +829,62 @@ describe("Avatar with Badge", () => { .should("have.css", "width", "16px"); }); + it("shows default tooltip from icon accessible name", () => { + cy.mount( + + + + ); + + cy.get("#avatar-with-default-badge-tooltip [ui5-avatar-badge]") + .shadow() + .find(".ui5-avatar-badge-icon") + .then(($icon) => { + cy.wrap($icon[0]) + .invoke("prop", "_id") + .then((iconId) => { + cy.get("#avatar-with-default-badge-tooltip [ui5-avatar-badge]") + .shadow() + .find(".ui5-avatar-badge-icon") + .shadow() + .find(`#${iconId}-tooltip`) + .should("contain.text", "Edit"); + }); + }); + }); + + it("uses accessibleName as tooltip text when provided", () => { + const customTooltip = "Open profile editor"; + + cy.mount( + + + + ); + + cy.get("#avatar-with-custom-badge-tooltip [ui5-avatar-badge]") + .shadow() + .find(".ui5-avatar-badge-icon") + .shadow() + .find("title") + .should("contain.text", customTooltip); + }); + + it("does not set badge-level accessible text when icon is invalid", () => { + cy.document().then(doc => { + const badge = doc.createElement("ui5-avatar-badge") as AvatarBadge & { effectiveAccessibleName?: string }; + badge.id = "badge-fallback-tooltip"; + badge.icon = "non-existent-icon-xyz"; + doc.body.appendChild(badge); + + cy.wait(100).then(() => { + expect(badge.effectiveAccessibleName).to.be.undefined; + expect(badge.hasAttribute("invalid")).to.be.true; + badge.remove(); + }); + }); + }); + it("hides badge when icon is invalid and shows when valid", () => { // Test all invalid cases and valid case in one test using direct DOM manipulation cy.document().then(doc => { diff --git a/packages/main/src/AvatarBadge.ts b/packages/main/src/AvatarBadge.ts index 5e0e028cec0e..82f4efd351b2 100644 --- a/packages/main/src/AvatarBadge.ts +++ b/packages/main/src/AvatarBadge.ts @@ -1,7 +1,9 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; +import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; import { getIconDataSync } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js"; // Template @@ -10,6 +12,8 @@ import AvatarBadgeTemplate from "./AvatarBadgeTemplate.js"; // Styles import AvatarBadgeCss from "./generated/themes/AvatarBadge.css.js"; +import { AVATAR_TOOLTIP } from "./generated/i18n/i18n-defaults.js"; + import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; /** @@ -61,6 +65,18 @@ class AvatarBadge extends UI5Element { @property() icon?: string; + /** + * Defines the custom text alternative of the badge icon. + * + * **Note:** If not provided, the badge uses the icon accessible name. + * If no icon accessible name is available, a generic fallback text is used. + * @default undefined + * @public + * @since 2.22.0 + */ + @property() + accessibleName?: string; + /** * Defines the state of the badge, which determines its styling. * @@ -83,8 +99,31 @@ class AvatarBadge extends UI5Element { @property({ type: Boolean }) invalid = false; + /** + * @private + */ + @property({ noAttribute: true }) + effectiveAccessibleName?: string; + + @i18n("@ui5/webcomponents") + static i18nBundle: I18nBundle; + onBeforeRendering() { - this.invalid = !this.icon || !getIconDataSync(this.icon); + const iconData = this.icon ? getIconDataSync(this.icon) : undefined; + this.invalid = !this.icon || !iconData; + + if (this.invalid) { + this.effectiveAccessibleName = undefined; + } else if (this.accessibleName) { + // User-provided accessible name takes precedence + this.effectiveAccessibleName = this.accessibleName; + } else { + // Derive from icon name (e.g., "edit" -> "Edit") + // If not possible, fall back to i18n "Avatar" text + this.effectiveAccessibleName = this.icon + ? this.icon.charAt(0).toUpperCase() + this.icon.slice(1) + : AvatarBadge.i18nBundle.getText(AVATAR_TOOLTIP); + } } } diff --git a/packages/main/src/AvatarBadgeTemplate.tsx b/packages/main/src/AvatarBadgeTemplate.tsx index d00ffb55cd89..7e52f8669475 100644 --- a/packages/main/src/AvatarBadgeTemplate.tsx +++ b/packages/main/src/AvatarBadgeTemplate.tsx @@ -8,7 +8,9 @@ export default function AvatarBadgeTemplate(this: AvatarBadge) { )} diff --git a/packages/main/test/pages/Avatar.html b/packages/main/test/pages/Avatar.html index d479905ff841..ae281cf3376c 100644 --- a/packages/main/test/pages/Avatar.html +++ b/packages/main/test/pages/Avatar.html @@ -305,6 +305,25 @@

Avatar Badge - With Icons

+
+

Avatar Badge - Tooltip Behavior

+

The badge tooltip is shown by default from the icon semantic text. You can override it with accessible-name.

+
+
+ + + +

Default tooltip (icon text)

+
+
+ + + +

Custom tooltip (accessible-name)

+
+
+
+

Avatar Badge - Handling Invalid Icon Names

The badge should not be displayed in the following cases:

diff --git a/packages/website/docs/_components_pages/main/Avatar/Avatar.mdx b/packages/website/docs/_components_pages/main/Avatar/Avatar.mdx index e62351f3b993..4625bff07a9f 100644 --- a/packages/website/docs/_components_pages/main/Avatar/Avatar.mdx +++ b/packages/website/docs/_components_pages/main/Avatar/Avatar.mdx @@ -65,6 +65,8 @@ The Avatar can show images. ### With Badge The Avatar supports visual affordance through badges using the `ui5-avatar-badge` component. Badges can display icons with different value states to indicate status or notifications. **It is recommended to use badges with interactive avatars** for better user experience and accessibility. +`ui5-avatar-badge` displays an icon tooltip by default, based on the icon semantic text. To provide a custom tooltip text, set the badge `accessible-name` property. + ### All Variants diff --git a/packages/website/docs/_samples/main/Avatar/WithBadge/sample.html b/packages/website/docs/_samples/main/Avatar/WithBadge/sample.html index dcddb57c4692..d18109cfee27 100644 --- a/packages/website/docs/_samples/main/Avatar/WithBadge/sample.html +++ b/packages/website/docs/_samples/main/Avatar/WithBadge/sample.html @@ -15,6 +15,10 @@ + + + + diff --git a/packages/website/docs/_samples/main/Avatar/WithBadge/sample.tsx b/packages/website/docs/_samples/main/Avatar/WithBadge/sample.tsx index b90484e80594..fb870031a40d 100644 --- a/packages/website/docs/_samples/main/Avatar/WithBadge/sample.tsx +++ b/packages/website/docs/_samples/main/Avatar/WithBadge/sample.tsx @@ -12,11 +12,19 @@ const AvatarBadge = createReactComponent(AvatarBadgeClass); function App() { return ( - <> +
+ + + + - +
); }