diff --git a/packages/main/cypress/specs/Avatar.cy.tsx b/packages/main/cypress/specs/Avatar.cy.tsx index 043bd6098d34..91bd3ab237a3 100644 --- a/packages/main/cypress/specs/Avatar.cy.tsx +++ b/packages/main/cypress/specs/Avatar.cy.tsx @@ -13,7 +13,7 @@ describe("Accessibility", () => { it("checks if initials of avatar are correctly announced", () => { const INITIALS = "XS"; - cy.mount(); + cy.mount(); // Store the expected label to compare against const expectedLabel = `Avatar ${INITIALS}`; @@ -34,7 +34,7 @@ describe("Accessibility", () => { @@ -77,7 +77,7 @@ describe("Accessibility", () => { ); @@ -121,6 +121,98 @@ describe("Accessibility", () => { cy.get("#disabled-avatar").realClick(); cy.get("#click-event").should("have.value", "0"); }); + + it("should support mode='Decorative' with aria-hidden and role='presentation'", () => { + cy.mount( + + ); + + cy.get("#decorative-avatar") + .shadow() + .find(".ui5-avatar-root") + .should("have.attr", "role", "presentation") + .should("have.attr", "aria-hidden", "true") + .should("not.have.attr", "tabindex"); + }); + + it("should support mode='Interactive' with role='button' and focusable", () => { + cy.mount( + + ); + + cy.get("#interactive-mode-avatar") + .shadow() + .find(".ui5-avatar-root") + .should("have.attr", "role", "button") + .should("have.attr", "tabindex", "0") + .should("not.have.attr", "aria-hidden"); + }); + + it("deprecated interactive property still allows click events but doesn't affect accessibility", () => { + cy.mount( +
+ + +
+ ); + + function increment() { + const input = document.getElementById("click-event-deprecated") as HTMLInputElement; + input.value = "1"; + } + + // Should have role="img" (not button) since mode is not Interactive + cy.get("#deprecated-interactive") + .shadow() + .find(".ui5-avatar-root") + .should("have.attr", "role", "img") + .should("not.have.attr", "tabindex"); + + // But should still fire click event + cy.get("#deprecated-interactive").realClick(); + cy.get("#click-event-deprecated").should("have.value", "1"); + }); + + it("should use role='img' in Image mode when not interactive", () => { + cy.mount( + + ); + + cy.get("#image-mode-avatar") + .shadow() + .find(".ui5-avatar-root") + .should("have.attr", "role", "img") + .should("not.have.attr", "aria-hidden"); + }); + + it("should use role='button' in Interactive mode", () => { + cy.mount( + + ); + + cy.get("#interactive-mode") + .shadow() + .find(".ui5-avatar-root") + .should("have.attr", "role", "button") + .should("have.attr", "tabindex", "0") + .should("not.have.attr", "aria-hidden"); + }); }); describe("Fallback Logic", () => { diff --git a/packages/main/src/Avatar.ts b/packages/main/src/Avatar.ts index b73cc6b86ed1..28e9ab7158dc 100644 --- a/packages/main/src/Avatar.ts +++ b/packages/main/src/Avatar.ts @@ -30,6 +30,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"; @@ -48,7 +49,7 @@ type AvatarAccessibilityAttributes = Pick; * * ### 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 @@ -92,16 +93,38 @@ class Avatar extends UI5Element implements ITabbable, IAvatarGroupItem { disabled = false; /** - * Defines if the avatar is interactive (focusable and pressable). + * Defines if the avatar fires click events. + * + * **Note:** This property only controls event firing behavior. + * It does not affect the component's accessibility attributes. + * To make the avatar interactive with proper accessibility (role="button", focusable), + * use `mode="Interactive"` instead. * * **Note:** This property won't have effect if the `disabled` * property is set to `true`. * @default false * @public + * @deprecated Since version 2.19. Use the `mode` property with value `Interactive` instead. + * For accessibility and visual affordance (role="button", focusable), set `mode="Interactive"`. + * This property now only controls whether click events are fired. */ @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 + * @default "Image" + * @public + * @since 2.19 + */ + @property() + mode: `${AvatarMode}` = "Image"; + /** * Defines the name of the UI5 Icon, that will be displayed. * @@ -273,7 +296,11 @@ class Avatar extends UI5Element implements ITabbable, IAvatarGroupItem { if (this.forcedTabIndex) { return parseInt(this.forcedTabIndex); } - return this._interactive ? 0 : undefined; + // Interactive mode makes the avatar focusable + if (this.mode === AvatarMode.Interactive) { + return 0; + } + return undefined; } /** @@ -297,7 +324,18 @@ class Avatar extends UI5Element implements ITabbable, IAvatarGroupItem { } get _role() { - return this._interactive ? "button" : "img"; + switch (this.mode) { + case AvatarMode.Interactive: + return "button"; + case AvatarMode.Decorative: + return "presentation"; + default: + return "img"; + } + } + + get effectiveAriaHidden() { + return this.mode === AvatarMode.Decorative ? "true" : undefined; } get _ariaHasPopup() { @@ -429,7 +467,7 @@ class Avatar extends UI5Element implements ITabbable, IAvatarGroupItem { _getAriaHasPopup() { const ariaHaspopup = this.accessibilityAttributes.hasPopup; - if (!this._interactive || !ariaHaspopup) { + if (this.mode !== AvatarMode.Interactive || !ariaHaspopup) { return; } @@ -500,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.mode === AvatarMode.Interactive ? Avatar.i18nBundle.getText(AVATAR_TYPE_BUTTON) : Avatar.i18nBundle.getText(AVATAR_TYPE_IMAGE), description: this.accessibleNameText, disabled: this.disabled, }; diff --git a/packages/main/src/AvatarTemplate.tsx b/packages/main/src/AvatarTemplate.tsx index 455584484cb5..a84754be1825 100644 --- a/packages/main/src/AvatarTemplate.tsx +++ b/packages/main/src/AvatarTemplate.tsx @@ -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} diff --git a/packages/main/src/types/AvatarMode.ts b/packages/main/src/types/AvatarMode.ts new file mode 100644 index 000000000000..3facebc35bd7 --- /dev/null +++ b/packages/main/src/types/AvatarMode.ts @@ -0,0 +1,33 @@ +/** + * Different Avatar modes. + * @public + */ +enum AvatarMode { + /** + * Image mode (by default). + * Configures the component to internally render role="img". + * @public + * @since 2.19 + */ + Image = "Image", + + /** + * Decorative mode. + * Configures the component to internally render role="presentation" and aria-hidden="true", + * making it purely decorative without semantic content or interactivity. + * @public + * @since 2.19 + */ + Decorative = "Decorative", + + /** + * Interactive mode. + * Configures the component to internally render role="button". + * This mode also supports focus and enables keyboard interaction. + * @public + * @since 2.19 + */ + Interactive = "Interactive", +} + +export default AvatarMode; diff --git a/packages/main/test/pages/Avatar.html b/packages/main/test/pages/Avatar.html index a47543ca1f18..dd796ac8c3de 100644 --- a/packages/main/test/pages/Avatar.html +++ b/packages/main/test/pages/Avatar.html @@ -167,10 +167,34 @@

Avatar - interactive


- + +
+

Avatar - mode="Decorative"

+

Decorative avatars are purely visual with role="presentation" and aria-hidden="true"

+ + + + John Miller + + +

Avatar - mode="Image" (default)

+

Image mode avatars have role="img"

+ + +

Avatar - mode="Interactive"

+

Interactive mode avatars have role="button", are focusable, and support keyboard interaction

+ + +

Avatar - Deprecated interactive property

+

The interactive property (deprecated) fires events but doesn't affect accessibility

+ +

For proper accessibility, use mode="Interactive" instead

+ +
+

Avatar Badge - All Sizes

diff --git a/packages/website/docs/_components_pages/main/Avatar/Avatar.mdx b/packages/website/docs/_components_pages/main/Avatar/Avatar.mdx index c98f8f303d74..ea28bf8de93c 100644 --- a/packages/website/docs/_components_pages/main/Avatar/Avatar.mdx +++ b/packages/website/docs/_components_pages/main/Avatar/Avatar.mdx @@ -4,6 +4,7 @@ slug: ../Avatar import Basic from "../../../_samples/main/Avatar/Basic/Basic.md"; import Interactive from "../../../_samples/main/Avatar/Interactive/Interactive.md"; +import Decorative from "../../../_samples/main/Avatar/Decorative/Decorative.md"; import WithBadge from "../../../_samples/main/Avatar/WithBadge/WithBadge.md"; import ColorSchemes from "../../../_samples/main/Avatar/ColorSchemes/ColorSchemes.md"; import Sizes from "../../../_samples/main/Avatar/Sizes/Sizes.md"; @@ -31,11 +32,16 @@ The Avatar comes in several sizes (S to XL). -### Interactive -The Avatar can be interactive, e.g. responds to hover, focus and press. +### Interactive Mode +The Avatar can be set to interactive mode with `mode="Interactive"`, making it focusable with role="button" and enabling keyboard interaction. This is the recommended way to make an avatar interactive. +### Decorative Mode +The Avatar can be set to decorative mode with `mode="Decorative"`, making it purely visual without semantic content (role="presentation", aria-hidden="true"). This is useful when the avatar is used for visual purposes only and shouldn't be announced by screen readers. + + + ### Color Schemes The Avatar can be displayed in multiple color schemes. diff --git a/packages/website/docs/_samples/main/Avatar/Decorative/Decorative.md b/packages/website/docs/_samples/main/Avatar/Decorative/Decorative.md new file mode 100644 index 000000000000..17798ecc59ab --- /dev/null +++ b/packages/website/docs/_samples/main/Avatar/Decorative/Decorative.md @@ -0,0 +1,4 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; + + diff --git a/packages/website/docs/_samples/main/Avatar/Decorative/main.js b/packages/website/docs/_samples/main/Avatar/Decorative/main.js new file mode 100644 index 000000000000..4dd3f498b33f --- /dev/null +++ b/packages/website/docs/_samples/main/Avatar/Decorative/main.js @@ -0,0 +1,2 @@ +import "@ui5/webcomponents/dist/Label.js"; +import "@ui5/webcomponents/dist/Avatar.js"; diff --git a/packages/website/docs/_samples/main/Avatar/Decorative/sample.html b/packages/website/docs/_samples/main/Avatar/Decorative/sample.html new file mode 100644 index 000000000000..8cf9bfdcacc1 --- /dev/null +++ b/packages/website/docs/_samples/main/Avatar/Decorative/sample.html @@ -0,0 +1,43 @@ + + + + + + + + Sample + + + + + + + +
+ + Decorative avatar with initials - not in accessibility tree +
+ +
+ + Image mode avatar (default) - announced as "Avatar CD" +
+ +
+ + Interactive mode - focusable with role="button" +
+ + + + + + + diff --git a/packages/website/docs/_samples/main/Avatar/Interactive/sample.html b/packages/website/docs/_samples/main/Avatar/Interactive/sample.html index 2f347b188a0f..08505a234994 100644 --- a/packages/website/docs/_samples/main/Avatar/Interactive/sample.html +++ b/packages/website/docs/_samples/main/Avatar/Interactive/sample.html @@ -11,7 +11,7 @@ - +