Skip to content
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
57 changes: 57 additions & 0 deletions packages/main/cypress/specs/Avatar.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -828,6 +829,62 @@ describe("Avatar with Badge", () => {
.should("have.css", "width", "16px");
});

it("shows default tooltip from icon accessible name", () => {
cy.mount(
<Avatar id="avatar-with-default-badge-tooltip" initials="AB" size="M">
<AvatarBadge slot="badge" icon="edit"></AvatarBadge>
</Avatar>
);

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(
<Avatar id="avatar-with-custom-badge-tooltip" initials="AB" size="M">
<AvatarBadge slot="badge" icon="edit" accessibleName={customTooltip}></AvatarBadge>
</Avatar>
);

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 => {
Expand Down
41 changes: 40 additions & 1 deletion packages/main/src/AvatarBadge.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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";

/**
Expand Down Expand Up @@ -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.
*
Expand All @@ -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);
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion packages/main/src/AvatarBadgeTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export default function AvatarBadgeTemplate(this: AvatarBadge) {
<Icon
name={this.icon}
class="ui5-avatar-badge-icon"
mode="Decorative"
accessibleName={this.effectiveAccessibleName}
showTooltip={true}
mode="Image"
></Icon>
)}
</>
Expand Down
19 changes: 19 additions & 0 deletions packages/main/test/pages/Avatar.html
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,25 @@ <h3>Avatar Badge - With Icons</h3>
</ui5-avatar>
</section>

<section>
<h3>Avatar Badge - Tooltip Behavior</h3>
<p>The badge tooltip is shown by default from the icon semantic text. You can override it with <code>accessible-name</code>.</p>
<div style="display: flex; flex-direction: row; align-items: end; column-gap: 0.5rem;">
<div style="text-align: center;">
<ui5-avatar size="M" initials="DF" color-scheme="Accent6">
<ui5-avatar-badge icon="edit" slot="badge"></ui5-avatar-badge>
</ui5-avatar>
<p style="font-size: 0.75rem;">Default tooltip (icon text)</p>
</div>
<div style="text-align: center;">
<ui5-avatar size="M" initials="CT" color-scheme="Accent8">
<ui5-avatar-badge icon="edit" accessible-name="Open profile editor" slot="badge"></ui5-avatar-badge>
</ui5-avatar>
<p style="font-size: 0.75rem;">Custom tooltip (accessible-name)</p>
</div>
</div>
</section>

<section>
<h3>Avatar Badge - Handling Invalid Icon Names</h3>
<p>The badge should not be displayed in the following cases:</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<WithBadge />

### All Variants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
<ui5-avatar-badge icon="edit" state="None" slot="badge"></ui5-avatar-badge>
</ui5-avatar>

<ui5-avatar mode="Interactive" size="M" initials="TT" color-scheme="Accent7">
<ui5-avatar-badge icon="edit" accessible-name="Open profile editor" slot="badge"></ui5-avatar-badge>
</ui5-avatar>

<ui5-avatar mode="Interactive" size="M" icon="employee" color-scheme="Accent10">
<ui5-avatar-badge icon="alert" state="Critical" slot="badge"></ui5-avatar-badge>
</ui5-avatar>
Expand Down
12 changes: 10 additions & 2 deletions packages/website/docs/_samples/main/Avatar/WithBadge/sample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,19 @@ const AvatarBadge = createReactComponent(AvatarBadgeClass);

function App() {
return (
<>
<div style={{ display: 'flex', alignItems: 'center', columnGap: '0.5rem' }}>
<Avatar mode="Interactive" size="M" initials="JD" colorScheme="Accent5">
<AvatarBadge icon="edit" state="None" slot="badge"></AvatarBadge>
</Avatar>

<Avatar mode="Interactive" size="M" initials="TT" colorScheme="Accent7">
<AvatarBadge
icon="edit"
accessibleName="Open profile editor"
slot="badge"
></AvatarBadge>
</Avatar>

<Avatar
mode="Interactive"
size="M"
Expand All @@ -39,7 +47,7 @@ function App() {
<img src="/images/avatars/woman_avatar_5.png" alt="Woman Avatar 5" />
<AvatarBadge icon="error" state="Negative" slot="badge"></AvatarBadge>
</Avatar>
</>
</div>
);
}

Expand Down
Loading