Skip to content

feat(ui5-ai-button): add customizable accessibility attributes to AI Button and SplitButton #11929

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 41 commits into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ac1bcb8
feat: improve AI button acc
GDamyanov Jul 16, 2025
4d8141d
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 16, 2025
21f4646
feat: improve acc for ai button
GDamyanov Jul 18, 2025
d80e887
refactor: refactor getter
GDamyanov Jul 18, 2025
0bc576f
refactor: remove redundant trim
GDamyanov Jul 18, 2025
ea193e5
refactor: remove redundant attribute
GDamyanov Jul 18, 2025
ccbe215
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 21, 2025
8bad9c9
fix: address review comments
GDamyanov Jul 21, 2025
1905be9
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 21, 2025
ee027a0
test: add test suite
GDamyanov Jul 21, 2025
8ba148e
Merge branch 'ai-button-acc-improvements' of https://github.com/SAP/u…
GDamyanov Jul 21, 2025
af4b0d0
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 21, 2025
3a04bf0
feat: enhance sample
GDamyanov Jul 21, 2025
6bad9ed
fix: update jsdocs
GDamyanov Jul 21, 2025
a52994a
fix: address review comments
GDamyanov Jul 21, 2025
74d166d
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 21, 2025
6587784
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 22, 2025
e2f4b57
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 22, 2025
aed55c9
fix: edit samples
GDamyanov Jul 22, 2025
50ee3a8
refactor: edit variable name
GDamyanov Jul 22, 2025
de4d5ae
fix: fix typo in variable name
GDamyanov Jul 22, 2025
029b100
feat: extract seperate getters for accInfo and accAttr
GDamyanov Jul 22, 2025
a9f3630
fix: rename proeprty name
GDamyanov Jul 22, 2025
4a948a4
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 22, 2025
e7db9ae
fix: lint error
GDamyanov Jul 22, 2025
6c19ee1
Merge branch 'ai-button-acc-improvements' of https://github.com/SAP/u…
GDamyanov Jul 22, 2025
c92bdc3
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 23, 2025
2273e59
fix: rename property
GDamyanov Jul 23, 2025
d559ddf
Merge branch 'ai-button-acc-improvements' of https://github.com/SAP/u…
GDamyanov Jul 23, 2025
bb8f4f8
fix: update property in tests and samples
GDamyanov Jul 23, 2025
bb853fc
fix: update jsdocs versions
GDamyanov Jul 23, 2025
19c2039
fix: update jdsocs
GDamyanov Jul 23, 2025
ee396b2
fix: update property
GDamyanov Jul 23, 2025
901f7e5
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 24, 2025
65bb56c
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 25, 2025
196938e
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 25, 2025
b1abf04
fix: address review comments
GDamyanov Jul 28, 2025
724d4ba
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 28, 2025
97aa6b2
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 28, 2025
179ff42
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 28, 2025
1ff515f
Merge branch 'main' into ai-button-acc-improvements
GDamyanov Jul 28, 2025
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
70 changes: 70 additions & 0 deletions packages/ai/cypress/specs/Button.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,73 @@ describe("Initial rendering", () => {
);
});
});

describe("Accessibility", () => {
it("should set correct tooltip to right text button", () => {
cy.mount(
<Button>
<ButtonState name="generate" text="Generate" icon="ai">Click me</ButtonState>
<ButtonState name="revise" text="Revise" icon="stop">Click me</ButtonState>
</Button>
);

cy.get("[ui5-ai-button]")
.ui5AIButtonCheckAttributeInTextButton("tooltip", "Generate with Artificial Intelligence");
});

it("should set correct aria-haspopup to SplitButton root element", () => {
cy.mount(
<Button accessibilityAttributes={{ root: { hasPopup: "menu" } }}>
<ButtonState name="generate" text="Generate" icon="ai">Click me</ButtonState>
</Button>
);

cy.get("[ui5-ai-button]")
.ui5AIButtonCheckAttributeSplitButtonRoot("aria-haspopup", "menu");
});

it("should set correct aria-roledescription to SplitButton root element", () => {
cy.mount(
<Button accessibilityAttributes={{ root: { roleDescription: "Open Menu" } }}>
<ButtonState name="generate" text="Generate" icon="ai">Click me</ButtonState>
</Button>
);

cy.get("[ui5-ai-button]")
.ui5AIButtonCheckAttributeSplitButtonRoot("aria-roledescription", "Open Menu");
});

it("should set correct aria-haspopup to arrow button if shown", () => {
cy.mount(
<Button accessibilityAttributes={{ arrowButton: { hasPopup: "menu", expanded: false } }}>
<ButtonState name="generate" text="Generate" icon="ai" showArrowButton={true}>Click me</ButtonState>
</Button>
);

cy.get("[ui5-ai-button]")
.ui5AIButtonCheckAttributeInArrowButton("aria-haspopup", "menu");
});

it("should set correct aria attributes with default values when not provided", () => {
cy.mount(
<Button>
<ButtonState name="generate" text="Generate" icon="ai" showArrowButton={true}>Click me</ButtonState>
</Button>
);

cy.get("[ui5-ai-button]")
.as("button");

cy.get("@button")
.ui5AIButtonCheckAttributeSplitButtonRoot("aria-haspopup", "false");

cy.get("@button")
.ui5AIButtonCheckAttributeSplitButtonRoot("aria-roledescription", "Split Button");

cy.get("@button")
.ui5AIButtonCheckAttributeInArrowButton("aria-haspopup", "menu");

cy.get("@button")
.ui5AIButtonCheckAttributeInArrowButton("aria-expanded", "false");
});
});
13 changes: 12 additions & 1 deletion packages/ai/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,15 @@
// }

import "@ui5/cypress-internal/commands.js";
import "../../../main/cypress/support/commands.js";
import "../../../main/cypress/support/commands.js";
import "./commands/Button.commands.js";

declare global {
namespace Cypress {
interface Chainable {
ui5AIButtonCheckAttributeInTextButton(attrName: string, attrValue: string): Chainable<void>
ui5AIButtonCheckAttributeInArrowButton(attrName: string, attrValue: string): Chainable<void>
ui5AIButtonCheckAttributeSplitButtonRoot(attrName: string, attrValue: string): Chainable<void>
}
}
}
33 changes: 33 additions & 0 deletions packages/ai/cypress/support/commands/Button.commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Button from "../../../src/Button.js";

Cypress.Commands.add("ui5AIButtonCheckAttributeInTextButton", { prevSubject: true }, (subject: JQuery<Button>, attrName: string, attrValue: string) => {
cy.wrap(subject)
.shadow()
.find("[ui5-split-button]")
.shadow()
.find(".ui5-split-text-button")
.should("be.visible")
.should("have.attr", attrName, attrValue);
});

Cypress.Commands.add("ui5AIButtonCheckAttributeInArrowButton", { prevSubject: true }, (subject: JQuery<Button>, attrName: string, attrValue: string) => {
cy.wrap(subject)
.shadow()
.find("[ui5-split-button]")
.shadow()
.find(".ui5-split-arrow-button")
.shadow()
.find(".ui5-button-root")
.should("be.visible")
.should("have.attr", attrName, attrValue);
});

Cypress.Commands.add("ui5AIButtonCheckAttributeSplitButtonRoot", { prevSubject: true }, (subject: JQuery<Button>, attrName: string, attrValue: string) => {
cy.wrap(subject)
.shadow()
.find("[ui5-split-button]")
.shadow()
.find(".ui5-split-button-root")
.should("be.visible")
.should("have.attr", attrName, attrValue);
});
65 changes: 64 additions & 1 deletion packages/ai/src/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,24 @@ import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import type SplitButton from "@ui5/webcomponents/dist/SplitButton.js";
import type ButtonDesign from "@ui5/webcomponents/dist/types/ButtonDesign.js";
import type ButtonState from "./ButtonState.js";
import { BUTTON_TOOLTIP_TEXT } from "./generated/i18n/i18n-defaults.js";
import "./ButtonState.js";

import ButtonTemplate from "./ButtonTemplate.js";
import {
getEffectiveAriaLabelText,
getAssociatedLabelForTexts,
getAllAccessibleNameRefTexts,
} from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js";

// Styles
import ButtonCss from "./generated/themes/Button.css.js";
import { i18n } from "@ui5/webcomponents-base/dist/decorators.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import type { AccessibilityAttributes } from "@ui5/webcomponents-base/dist/types.js";

type AIButtonRootAccessibilityAttributes = Pick<AccessibilityAttributes, "hasPopup" | "roleDescription" | "title">;
type AIButtonArrowButtonAccessibilityAttributes = Pick<AccessibilityAttributes, "hasPopup" | "expanded" | "title">;
type AIButtonAccessibilityAttributes = { root?: AIButtonRootAccessibilityAttributes, arrowButton?: AIButtonArrowButtonAccessibilityAttributes}

/**
* @class
Expand Down Expand Up @@ -119,6 +131,31 @@ class Button extends UI5Element {
@property({ type: Boolean, noAttribute: true })
arrowButtonPressed = false;

/**
* Defines the additional accessibility attributes that will be applied to the component.
*
* This property allows for fine-tuned control of ARIA attributes for screen reader support.
* It accepts an object with the following optional fields:
*
* - **root**: Accessibility attributes that will be applied to the root element.
* - **hasPopup**: Indicates the availability and type of interactive popup element (such as a menu or dialog).
* Accepts string values: `"dialog"`, `"grid"`, `"listbox"`, `"menu"`, or `"tree"`.
* - **roleDescription**: Defines a human-readable description for the button's role.
* Accepts any string value.
*
* - **arrowButton**: Accessibility attributes that will be applied to the arrow (split) button element.
* - **hasPopup**: Indicates the type of popup triggered by the arrow button.
* Accepts string values: `"dialog"`, `"grid"`, `"listbox"`, `"menu"`, or `"tree"`.
* - **expanded**: Indicates whether the popup controlled by the arrow button is currently expanded.
* Accepts boolean values: `true` or `false`.
*
* @public
* @since 2.6.0
* @default {}
*/
@property({ type: Object })
accessibilityAttributes: AIButtonAccessibilityAttributes = {};

/**
* Keeps the current state object of the component.
* @private
Expand Down Expand Up @@ -150,6 +187,9 @@ class Button extends UI5Element {
@query(".ui5-ai-button-hidden[ui5-split-button]")
_hiddenSplitButton?: SplitButton;

@i18n("@ui5/webcomponents")
static i18nBundle: I18nBundle;

get _hideArrowButton() {
return !this._effectiveStateObject?.showArrowButton;
}
Expand Down Expand Up @@ -305,7 +345,30 @@ class Button extends UI5Element {
e.stopImmediatePropagation();
this.fireDecoratorEvent("arrow-button-click");
}

get _computedAccessibilityAttributes(): AIButtonAccessibilityAttributes {
const labelRefTexts = getAllAccessibleNameRefTexts(this) || getEffectiveAriaLabelText(this) || getAssociatedLabelForTexts(this) || "";

const mainTitle = this._hasText ? Button.i18nBundle.getText(BUTTON_TOOLTIP_TEXT, this._stateText as string) : "";
const title = `${mainTitle} ${labelRefTexts}`.trim();

return {
root: {
hasPopup: this.accessibilityAttributes?.root?.hasPopup || "false",
roleDescription: this.accessibilityAttributes?.root?.roleDescription,
title: this.accessibilityAttributes?.root?.title || title,
},
arrowButton: {
hasPopup: this.accessibilityAttributes?.arrowButton?.hasPopup,
expanded: this.accessibilityAttributes?.arrowButton?.expanded,
title: this.accessibilityAttributes?.arrowButton?.title,
},
};
}
}

Button.define();
export default Button;
export type {
AIButtonAccessibilityAttributes,
};
1 change: 1 addition & 0 deletions packages/ai/src/ButtonTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function ButtonTemplate(this: Button) {
_hideArrowButton={this._hideArrowButton}
onClick={this._onClick}
onArrowClick={this._onArrowClick}
accessibilityAttributes={this._computedAccessibilityAttributes}
>
{this._hasText && (
<div class="ui5-ai-button-text">{this._stateText}</div>
Expand Down
6 changes: 5 additions & 1 deletion packages/ai/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@
PROMPT_INPUT_CHARACTERS_LEFT={0} characters remaining

#XTXT: Text for characters over
PROMPT_INPUT_CHARACTERS_EXCEEDED={0} characters over limit
PROMPT_INPUT_CHARACTERS_EXCEEDED={0} characters over limit

#XTXT: Text for
BUTTON_TOOLTIP_TEXT={0} with Artificial Intelligence

2 changes: 1 addition & 1 deletion packages/ai/src/i18n/messagebundle_en.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

PROMPT_INPUT_CHARACTERS_LEFT={0} characters remaining

PROMPT_INPUT_CHARACTERS_EXCEEDED={0} characters over limit
PROMPT_INPUT_CHARACTERS_EXCEEDED={0} characters over limit
44 changes: 36 additions & 8 deletions packages/ai/test/pages/Button.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,28 @@
const predefinedTextsSimplified = data.predefinedTextsSimplified;
const predefinedTextsSummarized = data.predefinedTextsSummarized;

function startGeneration(button) {
function startGeneration(button, isSplitButton = false) {
console.warn("startGeneration");
generationId = setTimeout(function() {
console.warn("Generation completed");
button.state = "revise";
button.accessibilityAttributes = {
root: {
hasPopup: "menu",
roleDescription: isSplitButton ? undefined : "Menu Button"
}
};
}, 3000);
}

function stopGeneration() {
function stopGeneration(button) {
console.warn("stopGeneration");
clearTimeout(generationId);
button.accessibilityAttributes = {
root: {
hasPopup: "false"
}
};
}

function aiButtonClickHandler(evt) {
Expand All @@ -168,7 +179,7 @@
break;
case "generating":
button.state = "generate";
stopGeneration();
stopGeneration(button);
break;
case "revise":
menu.opener = button;
Expand All @@ -185,17 +196,17 @@
case "generate":
prevTriggerState = "generate";
button.state = "generating";
startGeneration(button);
startGeneration(button, true);
break;
case "generating":
button.state = prevTriggerState;
stopGeneration();
stopGeneration(button);
break;
case "revise":
menuReg.open = false;
prevTriggerState = "revise";
button.state = "generating";
startGeneration(button);
startGeneration(button, true);
break;
}
}
Expand Down Expand Up @@ -235,7 +246,6 @@

button.arrowButtonPressed = false;
});

myAiButton.addEventListener("click", aiButtonClickHandler);
myAiButtonSplit.addEventListener("click", aiButtonSplitClickHandler);
myAiButtonSplit.addEventListener("arrow-button-click", aiButtonSplitArrowClickHandler);
Expand Down Expand Up @@ -276,15 +286,26 @@
generationId = setTimeout(function() {
console.warn("Generation completed");
button.state = "revise";
button.accessibilityAttributes = {
root: {
hasPopup: "menu",
roleDescription: "Menu Button"
}
};
}, 2000);
}

function stopQuickPromptGeneration() {
function stopQuickPromptGeneration(button) {
console.warn("stopGeneration");
clearInterval(generationId);
generationStopped = true;
sendButton.disabled = false;
output.disabled = false;
button.accessibilityAttributes = {
root: {
hasPopup: "false"
}
};
}

sendButton.addEventListener("click", function() {
Expand Down Expand Up @@ -420,6 +441,13 @@
if (!generationStopped) {
button.state = "revise";
output.valueState = "None";
button.accessibilityAttributes = {
root: {
hasPopup: "menu",
roleDescription: "Menu Button"
}
};

}
clearInterval(generationId);
sendButton.disabled = false;
Expand Down
4 changes: 3 additions & 1 deletion packages/base/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type AriaRole = JSX.AriaRole;
export type AriaDisabled = JSX.AriaAttributes["aria-disabled"];
export type AriaChecked = JSX.AriaAttributes["aria-checked"];
export type AriaReadonly = JSX.AriaAttributes["aria-readonly"];
export type AriaHasPopup = "dialog" | "grid" | "listbox" | "menu" | "tree";
export type AriaHasPopup = "dialog" | "grid" | "listbox" | "menu" | "tree" | "false";
export type AriaCurrent = "page" | "step" | "location" | "date" | "time" | "true" | "false" | boolean | undefined;
export type AriaAutoComplete = "list" | "none" | "inline" | "both" | undefined;
export type AriaLandmarkRole = "none" | "banner" | "main" | "region" | "navigation" | "search" | "complementary" | "form" | "contentinfo"
Expand Down Expand Up @@ -64,4 +64,6 @@ export type AccessibilityAttributes = {
ariaKeyShortcuts?: string,
ariaCurrent?: AriaCurrent,
current?: AriaCurrent,
roleDescription?: string,
title?: string,
}
Loading
Loading