Skip to content
Merged
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
106 changes: 105 additions & 1 deletion packages/main/cypress/specs/Avatar.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,110 @@ describe("Accessibility", () => {
cy.get("#disabled-avatar").realClick();
cy.get("#click-event").should("have.value", "0");
});

// New tests for mode property
it("mode='Decorative' renders with role='presentation' and aria-hidden", () => {
cy.mount(
<Avatar
id="decorative-avatar"
initials="AB"
mode="Decorative"
></Avatar>
);

cy.get("#decorative-avatar")
.shadow()
.find(".ui5-avatar-root")
.should("have.attr", "role", "presentation")
.should("have.attr", "aria-hidden", "true");
});

it("mode='Interactive' renders with role='button' and is focusable", () => {
cy.mount(
<Avatar
id="interactive-mode-avatar"
initials="IJ"
mode="Interactive"
></Avatar>
);

cy.get("#interactive-mode-avatar")
.shadow()
.find(".ui5-avatar-root")
.should("have.attr", "role", "button")
.should("have.attr", "tabindex", "0");
});

it("interactive property takes precedence over mode property", () => {
cy.mount(
<Avatar
interactive
mode="Decorative"
initials="PR"
id="precedence-avatar"
></Avatar>
);

// Even though mode="Decorative", interactive=true takes precedence
cy.get("#precedence-avatar")
.shadow()
.find(".ui5-avatar-root")
.should("have.attr", "role", "button")
.should("have.attr", "tabindex", "0")
.should("not.have.attr", "aria-hidden");
});

it("interactive=true with disabled=true renders with role='img' and is not focusable", () => {
cy.mount(
<Avatar
interactive
disabled
initials="DI"
id="disabled-interactive-avatar"
></Avatar>
);

cy.get("#disabled-interactive-avatar")
.shadow()
.find(".ui5-avatar-root")
.should("have.attr", "role", "img")
.should("not.have.attr", "tabindex");
});

it("mode='Interactive' with disabled=true renders with role='img' and is not focusable", () => {
cy.mount(
<Avatar
mode="Interactive"
disabled
initials="DM"
id="disabled-mode-interactive-avatar"
></Avatar>
);

cy.get("#disabled-mode-interactive-avatar")
.shadow()
.find(".ui5-avatar-root")
.should("have.attr", "role", "img")
.should("not.have.attr", "tabindex");
});

it("disabled interactive avatar doesn't fire click event with mode='Interactive'", () => {
cy.mount(
<div>
<Avatar mode="Interactive" disabled initials="DM" id="disabled-mode-click" onClick={increment}>
</Avatar>
<input value="0" id="mode-click-event" />
</div>
);

function increment() {
const input = document.getElementById("mode-click-event") as HTMLInputElement;
input.value = "1";
}

cy.get("#disabled-mode-click").realClick();
cy.get("#mode-click-event").should("have.value", "0");
});
});

describe("Fallback Logic", () => {
Expand Down Expand Up @@ -480,7 +584,7 @@ describe("Avatar Rendering and Interaction", () => {

it("Tests noConflict 'ui5-click' event for interactive avatars", () => {
cy.mount(
<Avatar interactive initials="XS" size="XS"></Avatar>
<Avatar mode="Interactive" initials="XS" size="XS"></Avatar>
);

cy.get("[ui5-avatar]")
Expand Down
45 changes: 41 additions & 4 deletions packages/main/src/Avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type Icon from "./Icon.js";
import AvatarSize from "./types/AvatarSize.js";
import type AvatarShape from "./types/AvatarShape.js";
import type AvatarColorScheme from "./types/AvatarColorScheme.js";
import AvatarMode from "./types/AvatarMode.js";

// Icon
import "@ui5/webcomponents-icons/dist/employee.js";
Expand All @@ -49,7 +50,7 @@ type AvatarAccessibilityAttributes = Pick<AccessibilityAttributes, "hasPopup">;
*
* ### Keyboard Handling
*
* - [Space] / [Enter] or [Return] - Fires the `click` event if the `interactive` property is set to true.
* - [Space] / [Enter] or [Return] - Fires the `click` event if the `mode` is set to `Interactive` or the deprecated `interactive` property is set to true.
* - [Shift] - If [Space] is pressed, pressing [Shift] releases the component without triggering the click event.
*
* ### ES6 Module Import
Expand Down Expand Up @@ -95,14 +96,35 @@ class Avatar extends UI5Element implements ITabbable, IAvatarGroupItem {
/**
* Defines if the avatar is interactive (focusable and pressable).
*
* **Note:** When set to `true`, this property takes precedence over the `mode` property,
* and the avatar will be rendered as interactive (role="button", focusable) regardless of the `mode` value.
*
* **Note:** This property won't have effect if the `disabled`
* property is set to `true`.
* @default false
* @public
* @deprecated Set `mode="Interactive"` instead for the same functionality with proper accessibility.
*/
@property({ type: Boolean })
interactive = false;

/**
* Defines the mode of the component.
*
* **Note:**
* - `Image` (default) - renders with role="img"
* - `Decorative` - renders with role="presentation" and aria-hidden="true", making it purely decorative
* - `Interactive` - renders with role="button", focusable (tabindex="0"), and supports keyboard interaction
*
* **Note:** This property is ignored when the `interactive` property is set to `true`.
* In that case, the avatar will always be rendered as interactive.
* @default "Image"
* @public
* @since 2.20
*/
@property()
mode: `${AvatarMode}` = "Image";

/**
* Defines the name of the UI5 Icon, that will be displayed.
*
Expand Down Expand Up @@ -298,15 +320,29 @@ class Avatar extends UI5Element implements ITabbable, IAvatarGroupItem {
}

get _role() {
return this._interactive ? "button" : "img";
if (this._interactive) {
return "button";
}
if (this.mode === AvatarMode.Decorative) {
return "presentation";
}
return "img";
}

get effectiveAriaHidden() {
// interactive property takes precedence - never hidden when interactive
if (this.interactive) {
return undefined;
}
return this.mode === AvatarMode.Decorative ? "true" : undefined;
}

get _ariaHasPopup() {
return this._getAriaHasPopup();
}

get _interactive() {
return this.interactive && !this.disabled;
return (this.interactive || this.mode === AvatarMode.Interactive) && !this.disabled;
}

get validInitials() {
Expand Down Expand Up @@ -430,6 +466,7 @@ class Avatar extends UI5Element implements ITabbable, IAvatarGroupItem {
_getAriaHasPopup() {
const ariaHaspopup = this.accessibilityAttributes.hasPopup;

// aria-haspopup only applies when avatar is interactive
if (!this._interactive || !ariaHaspopup) {
return;
}
Expand Down Expand Up @@ -501,7 +538,7 @@ class Avatar extends UI5Element implements ITabbable, IAvatarGroupItem {
get accessibilityInfo() {
return {
role: this._role as AriaRole,
type: this.interactive ? Avatar.i18nBundle.getText(AVATAR_TYPE_BUTTON) : Avatar.i18nBundle.getText(AVATAR_TYPE_IMAGE),
type: this._interactive ? Avatar.i18nBundle.getText(AVATAR_TYPE_BUTTON) : Avatar.i18nBundle.getText(AVATAR_TYPE_IMAGE),
description: this.accessibleNameText,
disabled: this.disabled,
};
Expand Down
1 change: 1 addition & 0 deletions packages/main/src/AvatarTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default function AvatarTemplate(this: Avatar) {
tabindex={this.tabindex}
data-sap-focus-ref
role={this._role}
aria-hidden={this.effectiveAriaHidden}
aria-haspopup={this._ariaHasPopup}
aria-label={this.accessibleNameText}
onKeyUp={this._onkeyup}
Expand Down
53 changes: 39 additions & 14 deletions packages/main/src/themes/Avatar.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,27 @@
opacity: .7;
}

:host([interactive]:not([disabled])) {
:host([interactive]:not([disabled])),
:host([mode="Interactive"]:not([disabled])) {
cursor: pointer;
}

:host([interactive]:not([hidden]):active) {
:host([interactive]:not([hidden]):active),
:host([mode="Interactive"]:not([hidden]):active) {
background-color: var(--sapButton_Active_Background);
border-color: var(--sapButton_Active_BorderColor);
color: var(--sapButton_Active_TextColor);
}

:host([interactive]:not([hidden]):not([disabled]):not(:active):not([focused]):hover) {
:host([interactive]:not([hidden]):not([disabled]):not(:active):not([focused]):hover),
:host([mode="Interactive"]:not([hidden]):not([disabled]):not(:active):not([focused]):hover) {
box-shadow: var(--ui5-avatar-hover-box-shadow-offset);
}

:host([interactive][desktop]:not([hidden])) .ui5-avatar-root:focus,
:host([interactive]:not([hidden])) .ui5-avatar-root:focus-visible {
:host([interactive]:not([hidden])) .ui5-avatar-root:focus-visible,
:host([mode="Interactive"][desktop]:not([hidden])) .ui5-avatar-root:focus,
:host([mode="Interactive"]:not([hidden])) .ui5-avatar-root:focus-visible {
outline: var(--_ui5_avatar_outline);
outline-offset: var(--_ui5_avatar_focus_offset);
}
Expand Down Expand Up @@ -146,7 +151,9 @@
border-color: var(--ui5-avatar-accent6-border-color);
}
:host([_color-scheme="Accent6"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent6"][interactive]:not([hidden]):not([disabled]):not(:active):hover) {
:host([ui5-avatar][color-scheme="Accent6"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([_color-scheme="Accent6"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent6"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover) {
background-color: var(--sapAvatar_6_Hover_Background);
}

Expand All @@ -158,7 +165,9 @@
}

:host([_color-scheme="Accent1"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent1"][interactive]:not([hidden]):not([disabled]):not(:active):hover) {
:host([ui5-avatar][color-scheme="Accent1"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([_color-scheme="Accent1"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent1"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover) {
background-color: var(--sapAvatar_1_Hover_Background);
}

Expand All @@ -170,7 +179,9 @@
}

:host([_color-scheme="Accent2"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent2"][interactive]:not([hidden]):not([disabled]):not(:active):hover) {
:host([ui5-avatar][color-scheme="Accent2"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([_color-scheme="Accent2"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent2"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover) {
background-color: var(--sapAvatar_2_Hover_Background);
}

Expand All @@ -182,7 +193,9 @@
}

:host([_color-scheme="Accent3"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent3"][interactive]:not([hidden]):not([disabled]):not(:active):hover) {
:host([ui5-avatar][color-scheme="Accent3"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([_color-scheme="Accent3"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent3"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover) {
background-color: var(--sapAvatar_3_Hover_Background);
}

Expand All @@ -194,7 +207,9 @@
}

:host([_color-scheme="Accent4"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent4"][interactive]:not([hidden]):not([disabled]):not(:active):hover) {
:host([ui5-avatar][color-scheme="Accent4"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([_color-scheme="Accent4"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent4"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover) {
background-color: var(--sapAvatar_4_Hover_Background);
}

Expand All @@ -206,7 +221,9 @@
}

:host([_color-scheme="Accent5"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent5"][interactive]:not([hidden]):not([disabled]):not(:active):hover) {
:host([ui5-avatar][color-scheme="Accent5"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([_color-scheme="Accent5"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent5"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover) {
background-color: var(--sapAvatar_5_Hover_Background);
}

Expand All @@ -218,7 +235,9 @@
}

:host([_color-scheme="Accent7"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent7"][interactive]:not([hidden]):not([disabled]):not(:active):hover) {
:host([ui5-avatar][color-scheme="Accent7"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([_color-scheme="Accent7"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent7"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover) {
background-color: var(--sapAvatar_7_Hover_Background);
}

Expand All @@ -230,7 +249,9 @@
}

:host([_color-scheme="Accent8"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent8"][interactive]:not([hidden]):not([disabled]):not(:active):hover) {
:host([ui5-avatar][color-scheme="Accent8"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([_color-scheme="Accent8"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent8"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover) {
background-color: var(--sapAvatar_8_Hover_Background);
}

Expand All @@ -242,7 +263,9 @@
}

:host([_color-scheme="Accent9"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent9"][interactive]:not([hidden]):not([disabled]):not(:active):hover) {
:host([ui5-avatar][color-scheme="Accent9"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([_color-scheme="Accent9"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent9"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover) {
background-color: var(--sapAvatar_9_Hover_Background);
}

Expand All @@ -254,7 +277,9 @@
}

:host([_color-scheme="Accent10"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent10"][interactive]:not([hidden]):not([disabled]):not(:active):hover) {
:host([ui5-avatar][color-scheme="Accent10"][interactive]:not([hidden]):not([disabled]):not(:active):hover),
:host([_color-scheme="Accent10"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover),
:host([ui5-avatar][color-scheme="Accent10"][mode="Interactive"]:not([hidden]):not([disabled]):not(:active):hover) {
background-color: var(--sapAvatar_10_Hover_Background);
}

Expand Down
Loading
Loading