Skip to content

Commit ff322d0

Browse files
authored
Merge branch 'main' into numberinputdocs
2 parents 785fa89 + 663fc51 commit ff322d0

11 files changed

Lines changed: 251 additions & 4 deletions

File tree

packages/fiori/cypress/specs/Timeline.cy.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,76 @@ describe("Timeline - getFocusDomRef", () => {
512512
});
513513
});
514514

515+
describe("TimelineItem iconTooltip", () => {
516+
it("should render tooltip on the icon when iconTooltip is set", () => {
517+
cy.mount(
518+
<Timeline>
519+
<TimelineItem
520+
id="itemWithTooltip"
521+
titleText="Deployment"
522+
icon={accept}
523+
iconTooltip="Success"
524+
>
525+
Deployed successfully.
526+
</TimelineItem>
527+
</Timeline>
528+
);
529+
530+
cy.get("#itemWithTooltip")
531+
.shadow()
532+
.find("[ui5-icon]")
533+
.should("have.attr", "show-tooltip")
534+
.and("exist");
535+
536+
cy.get("#itemWithTooltip")
537+
.shadow()
538+
.find("[ui5-icon]")
539+
.should("have.attr", "accessible-name", "Success");
540+
});
541+
542+
it("should not render tooltip on icon when iconTooltip is not set", () => {
543+
cy.mount(
544+
<Timeline>
545+
<TimelineItem
546+
id="itemWithoutTooltip"
547+
titleText="Deployment"
548+
icon={accept}
549+
>
550+
Deployed successfully.
551+
</TimelineItem>
552+
</Timeline>
553+
);
554+
555+
cy.get("#itemWithoutTooltip")
556+
.shadow()
557+
.find("[ui5-icon]")
558+
.should("not.have.attr", "show-tooltip");
559+
});
560+
561+
it("should include iconTooltip in the accessible label of the bubble", () => {
562+
cy.mount(
563+
<Timeline>
564+
<TimelineItem
565+
id="itemAccLabel"
566+
titleText="Build"
567+
subtitleText="Step 1"
568+
icon={accept}
569+
iconTooltip="Passed"
570+
name="CI Pipeline"
571+
>
572+
Build completed.
573+
</TimelineItem>
574+
</Timeline>
575+
);
576+
577+
cy.get("#itemAccLabel")
578+
.shadow()
579+
.find(".ui5-tli-bubble")
580+
.should("have.attr", "aria-label")
581+
.and("include", "Passed");
582+
});
583+
});
584+
515585
describe("Timeline Header Bar", () => {
516586
describe("Search functionality", () => {
517587
it("should show header bar when slotted", () => {

packages/fiori/src/TimelineItem.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ class TimelineItem extends UI5Element implements ITimelineItem {
6969
@property()
7070
icon?: string;
7171

72+
/**
73+
* Defines the tooltip of the graphical icon.
74+
* @default undefined
75+
* @public
76+
* @since 2.22.0
77+
*/
78+
@property()
79+
iconTooltip?: string;
80+
7281
/**
7382
* Defines the name of the item, displayed before the `title-text`.
7483
* @default undefined
@@ -235,6 +244,10 @@ class TimelineItem extends UI5Element implements ITimelineItem {
235244
parts.push(this.timelineItemStateText);
236245
}
237246

247+
if (this.iconTooltip) {
248+
parts.push(this.iconTooltip);
249+
}
250+
238251
return parts.join(", ");
239252
}
240253
}

packages/fiori/src/TimelineItemTemplate.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ export default function TimelineItemTemplate(this: TimelineItem) {
1919
<div class="ui5-tli-icon-outer">
2020
{
2121
this.icon ?
22-
<Icon class="ui5-tli-icon" name={this.icon} mode="Decorative"/>
22+
<Icon
23+
class="ui5-tli-icon"
24+
name={this.icon}
25+
mode="Decorative"
26+
showTooltip={!!this.iconTooltip}
27+
accessibleName={this.iconTooltip}
28+
/>
2329
:
2430
<div class="ui5-tli-dummy-icon-container"></div>
2531
}

packages/fiori/test/pages/Timeline.html

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,33 @@ <h2>Advanced Timeline - Horizontal With Groups and Diverse Components</h2>
364364
</div>
365365
</section>
366366

367+
<section style="width: 100%;">
368+
<h2>Timeline with Various Timeline Item States</h2>
369+
<div style="width: 50%; margin: 1rem;">
370+
<ui5-timeline id="test-timeline">
371+
<ui5-timeline-group-item group-name="Build">
372+
<ui5-timeline-item title="Compile" subtitle="Testing suite A" icon="sap-icon://accept" icon-tooltip="Compilation successful" name="Testing suite A" state="Positive">
373+
Compilation succeeded.
374+
</ui5-timeline-item>
375+
<ui5-timeline-item title="Lint" subtitle="Testing suite B" icon="sap-icon://message-information" name="Testing suite B" state="Information">
376+
Lint completed with minor issues.
377+
</ui5-timeline-item>
378+
</ui5-timeline-group-item>
379+
<ui5-timeline-group-item group-name="Test">
380+
<ui5-timeline-item title="Unit Test" subtitle="Testing suite C" icon="sap-icon://decline" name="Testing suite C" state="Negative">
381+
Unit tests failed.
382+
</ui5-timeline-item>
383+
<ui5-timeline-item title="Integration Test" subtitle="Testing suite D" icon="sap-icon://message-warning" name="Testing suite D" state="Critical">
384+
Integration tests have warnings.
385+
</ui5-timeline-item>
386+
<ui5-timeline-item title="E2E Test" subtitle="Testing suite E" icon="sap-icon://accept" name="Testing suite E" state="Positive">
387+
End-to-end tests passed.
388+
</ui5-timeline-item>
389+
</ui5-timeline-group-item>
390+
</ui5-timeline>
391+
</div>
392+
</section>
393+
367394
<section style="width: 100%;">
368395
<h2>Timeline with Various Timeline Item States</h2>
369396
<div style="width: 50%; margin: 1rem;">

packages/main/cypress/specs/Avatar.cy.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import "@ui5/webcomponents-icons/dist/alert.js";
55
import "@ui5/webcomponents-icons/dist/person-placeholder.js";
66
import "@ui5/webcomponents-icons/dist/accelerated.js";
77
import "@ui5/webcomponents-icons/dist/accept.js";
8+
import "@ui5/webcomponents-icons/dist/edit.js";
89
import "@ui5/webcomponents-icons/dist/message-error.js";
910
import "@ui5/webcomponents-icons/dist/information.js";
1011
import "@ui5/webcomponents-icons/dist/ai.js";
@@ -829,6 +830,62 @@ describe("Avatar with Badge", () => {
829830
.should("have.css", "width", "16px");
830831
});
831832

833+
it("shows default tooltip from icon accessible name", () => {
834+
cy.mount(
835+
<Avatar id="avatar-with-default-badge-tooltip" initials="AB" size="M">
836+
<AvatarBadge slot="badge" icon="edit"></AvatarBadge>
837+
</Avatar>
838+
);
839+
840+
cy.get("#avatar-with-default-badge-tooltip [ui5-avatar-badge]")
841+
.shadow()
842+
.find(".ui5-avatar-badge-icon")
843+
.then(($icon) => {
844+
cy.wrap($icon[0])
845+
.invoke("prop", "_id")
846+
.then((iconId) => {
847+
cy.get("#avatar-with-default-badge-tooltip [ui5-avatar-badge]")
848+
.shadow()
849+
.find(".ui5-avatar-badge-icon")
850+
.shadow()
851+
.find(`#${iconId}-tooltip`)
852+
.should("contain.text", "Edit");
853+
});
854+
});
855+
});
856+
857+
it("uses accessibleName as tooltip text when provided", () => {
858+
const customTooltip = "Open profile editor";
859+
860+
cy.mount(
861+
<Avatar id="avatar-with-custom-badge-tooltip" initials="AB" size="M">
862+
<AvatarBadge slot="badge" icon="edit" accessibleName={customTooltip}></AvatarBadge>
863+
</Avatar>
864+
);
865+
866+
cy.get("#avatar-with-custom-badge-tooltip [ui5-avatar-badge]")
867+
.shadow()
868+
.find(".ui5-avatar-badge-icon")
869+
.shadow()
870+
.find("title")
871+
.should("contain.text", customTooltip);
872+
});
873+
874+
it("does not set badge-level accessible text when icon is invalid", () => {
875+
cy.document().then(doc => {
876+
const badge = doc.createElement("ui5-avatar-badge") as AvatarBadge & { effectiveAccessibleName?: string };
877+
badge.id = "badge-fallback-tooltip";
878+
badge.icon = "non-existent-icon-xyz";
879+
doc.body.appendChild(badge);
880+
881+
cy.wait(100).then(() => {
882+
expect(badge.effectiveAccessibleName).to.be.undefined;
883+
expect(badge.hasAttribute("invalid")).to.be.true;
884+
badge.remove();
885+
});
886+
});
887+
});
888+
832889
it("hides badge when icon is invalid and shows when valid", () => {
833890
// Test all invalid cases and valid case in one test using direct DOM manipulation
834891
cy.document().then(doc => {

packages/main/src/AvatarBadge.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement
33
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
44
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
55
import { getIconData, getIconDataSync } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js";
6+
import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
67

78
// Template
89
import AvatarBadgeTemplate from "./AvatarBadgeTemplate.js";
@@ -45,6 +46,7 @@ const ICON_NOT_FOUND = "ICON_NOT_FOUND";
4546
*/
4647
@customElement({
4748
tag: "ui5-avatar-badge",
49+
languageAware: true,
4850
renderer: jsxRenderer,
4951
styles: AvatarBadgeCss,
5052
template: AvatarBadgeTemplate,
@@ -63,6 +65,18 @@ class AvatarBadge extends UI5Element {
6365
@property()
6466
icon?: string;
6567

68+
/**
69+
* Defines the custom text alternative of the badge icon.
70+
*
71+
* **Note:** If not provided, the badge uses the icon accessible name.
72+
* If no icon accessible name is available, a generic fallback text is used.
73+
* @default undefined
74+
* @public
75+
* @since 2.22.0
76+
*/
77+
@property()
78+
accessibleName?: string;
79+
6680
/**
6781
* Defines the state of the badge, which determines its styling.
6882
*
@@ -85,15 +99,40 @@ class AvatarBadge extends UI5Element {
8599
@property({ type: Boolean })
86100
invalid = false;
87101

102+
/**
103+
* @private
104+
*/
105+
@property({ noAttribute: true })
106+
effectiveAccessibleName?: string;
107+
88108
async onBeforeRendering() {
89109
const icon = this.icon;
90110
if (!icon) {
91111
this.invalid = true;
112+
this.effectiveAccessibleName = undefined;
92113
return;
93114
}
94115

95116
const iconData = getIconDataSync(icon) || await getIconData(icon);
96117
this.invalid = !iconData || iconData === ICON_NOT_FOUND;
118+
119+
if (this.invalid) {
120+
this.effectiveAccessibleName = undefined;
121+
} else if (this.accessibleName) {
122+
// User-provided accessible name takes precedence
123+
this.effectiveAccessibleName = this.accessibleName;
124+
} else if (iconData && iconData !== ICON_NOT_FOUND && iconData.accData) {
125+
// Use the icon's registered i18n label (e.g., message-error -> "Error")
126+
if (iconData.packageName) {
127+
const i18nBundle = await getI18nBundle(iconData.packageName);
128+
this.effectiveAccessibleName = i18nBundle.getText(iconData.accData) || undefined;
129+
} else {
130+
this.effectiveAccessibleName = iconData.accData.defaultText || undefined;
131+
}
132+
} else {
133+
// Derive from icon name (e.g., "edit" -> "Edit")
134+
this.effectiveAccessibleName = icon.charAt(0).toUpperCase() + icon.slice(1);
135+
}
97136
}
98137
}
99138

packages/main/src/AvatarBadgeTemplate.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ export default function AvatarBadgeTemplate(this: AvatarBadge) {
88
<Icon
99
name={this.icon}
1010
class="ui5-avatar-badge-icon"
11-
mode="Decorative"
11+
accessibleName={this.effectiveAccessibleName}
12+
showTooltip={true}
13+
mode="Image"
1214
></Icon>
1315
)}
1416
</>

packages/main/test/pages/Avatar.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,25 @@ <h3>Avatar Badge - With Icons</h3>
305305
</ui5-avatar>
306306
</section>
307307

308+
<section>
309+
<h3>Avatar Badge - Tooltip Behavior</h3>
310+
<p>The badge tooltip is shown by default from the icon semantic text. You can override it with <code>accessible-name</code>.</p>
311+
<div style="display: flex; flex-direction: row; align-items: end; column-gap: 0.5rem;">
312+
<div style="text-align: center;">
313+
<ui5-avatar size="M" initials="DF" color-scheme="Accent6">
314+
<ui5-avatar-badge icon="edit" slot="badge"></ui5-avatar-badge>
315+
</ui5-avatar>
316+
<p style="font-size: 0.75rem;">Default tooltip (icon text)</p>
317+
</div>
318+
<div style="text-align: center;">
319+
<ui5-avatar size="M" initials="CT" color-scheme="Accent8">
320+
<ui5-avatar-badge icon="edit" accessible-name="Open profile editor" slot="badge"></ui5-avatar-badge>
321+
</ui5-avatar>
322+
<p style="font-size: 0.75rem;">Custom tooltip (accessible-name)</p>
323+
</div>
324+
</div>
325+
</section>
326+
308327
<section>
309328
<h3>Avatar Badge - Handling Invalid Icon Names</h3>
310329
<p>The badge should not be displayed in the following cases:</p>

packages/website/docs/_components_pages/main/Avatar/Avatar.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ The Avatar can show images.
6565
### With Badge
6666
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.
6767

68+
`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.
69+
6870
<WithBadge />
6971

7072
### All Variants

packages/website/docs/_samples/main/Avatar/WithBadge/sample.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
<ui5-avatar-badge icon="edit" state="None" slot="badge"></ui5-avatar-badge>
1616
</ui5-avatar>
1717

18+
<ui5-avatar mode="Interactive" size="M" initials="TT" color-scheme="Accent7">
19+
<ui5-avatar-badge icon="edit" accessible-name="Open profile editor" slot="badge"></ui5-avatar-badge>
20+
</ui5-avatar>
21+
1822
<ui5-avatar mode="Interactive" size="M" icon="employee" color-scheme="Accent10">
1923
<ui5-avatar-badge icon="alert" state="Critical" slot="badge"></ui5-avatar-badge>
2024
</ui5-avatar>

0 commit comments

Comments
 (0)