diff --git a/packages/main/cypress/specs/Tokenizer.cy.tsx b/packages/main/cypress/specs/Tokenizer.cy.tsx
index 184490575e5eb..d6cc7e3d60fe0 100755
--- a/packages/main/cypress/specs/Tokenizer.cy.tsx
+++ b/packages/main/cypress/specs/Tokenizer.cy.tsx
@@ -2102,3 +2102,74 @@ describe("Tokenizer - getFocusDomRef Method", () => {
.should("be.focused");
});
});
+
+describe("Token deletion announcements", () => {
+ it("announces singular deletion on Backspace with a single token", () => {
+ cy.mount(
+
+
+
+
+ );
+
+ cy.get(".ui5-invisiblemessage-assertive")
+ .as("liveRegion")
+ .should("have.text", "");
+
+ cy.get("[ui5-token]")
+ .eq(0)
+ .realClick();
+
+ cy.realPress("Backspace");
+
+ cy.get("@liveRegion")
+ .should("have.text", "1 token deleted");
+ });
+
+ it("announces plural deletion on Backspace with multiple selected tokens", () => {
+ cy.mount(
+
+
+
+
+
+ );
+
+ cy.get(".ui5-invisiblemessage-assertive")
+ .as("liveRegion")
+ .should("have.text", "");
+
+ cy.get("[ui5-token]")
+ .eq(0)
+ .realClick();
+
+ cy.realPress(["Shift", "ArrowRight"]);
+ cy.realPress("Backspace");
+
+ cy.get("@liveRegion")
+ .should("have.text", "2 tokens deleted");
+ });
+
+ it("announces plural deletion equal to token count on 'Clear All'", () => {
+ cy.mount(
+
+
+
+
+
+ );
+
+ cy.get(".ui5-invisiblemessage-assertive")
+ .as("liveRegion")
+ .should("have.text", "");
+
+ cy.get("[ui5-tokenizer]")
+ .shadow()
+ .find(".ui5-tokenizer--clear-all")
+ .eq(0)
+ .realClick();
+
+ cy.get("@liveRegion")
+ .should("have.text", "3 tokens deleted");
+ });
+});
diff --git a/packages/main/src/Tokenizer.ts b/packages/main/src/Tokenizer.ts
index c567282e8d938..fa73fbe57cb83 100644
--- a/packages/main/src/Tokenizer.ts
+++ b/packages/main/src/Tokenizer.ts
@@ -10,6 +10,8 @@ import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js";
+import announce from "@ui5/webcomponents-base/dist/util/InvisibleMessage.js";
+import InvisibleMessageMode from "@ui5/webcomponents-base/dist/types/InvisibleMessageMode.js";
import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js";
import { getFocusedElement } from "@ui5/webcomponents-base/dist/util/PopupUtils.js";
import ScrollEnablement from "@ui5/webcomponents-base/dist/delegate/ScrollEnablement.js";
@@ -69,6 +71,8 @@ import {
TOKENIZER_CLEAR_ALL,
TOKENIZER_DIALOG_OK_BUTTON,
TOKENIZER_DIALOG_CANCEL_BUTTON,
+ TOKENIZER_TOKEN_DELETED_SINGULAR,
+ TOKENIZER_TOKEN_DELETED_PLURAL,
INPUT_SUGGESTIONS_TITLE,
} from "./generated/i18n/i18n-defaults.js";
@@ -416,9 +420,23 @@ class Tokenizer extends UI5Element implements IFormInputElement {
}
handleClearAll() {
+ this._announceTokenDeletion(this._tokens.length);
this.fireDecoratorEvent("token-delete", { tokens: this._tokens });
}
+ /**
+ * Announces the number of deleted tokens to screen readers.
+ * @private
+ * @param count The number of tokens being deleted
+ */
+ _announceTokenDeletion(count: number) {
+ const text = count === 1
+ ? Tokenizer.i18nBundle.getText(TOKENIZER_TOKEN_DELETED_SINGULAR)
+ : Tokenizer.i18nBundle.getText(TOKENIZER_TOKEN_DELETED_PLURAL, count);
+
+ announce(text, InvisibleMessageMode.Assertive);
+ }
+
onBeforeRendering() {
if (!this.multiLine) {
this._scrollEnablement = new ScrollEnablement(this);
@@ -590,11 +608,13 @@ class Tokenizer extends UI5Element implements IFormInputElement {
const deletedTokenIndex = token ? tokens.indexOf(token) : tokens.indexOf(target); // The index of the token that just got deleted
const nextTokenIndex = deletedTokenIndex === tokens.length - 1 ? deletedTokenIndex - 1 : deletedTokenIndex + 1; // The index of the next token that needs to be focused next due to the deletion
const nextToken = tokens[nextTokenIndex]; // if the last item was deleted this will be undefined
+ const tokensToDelete = [token];
+ this._announceTokenDeletion(tokensToDelete.length);
this._handleCurrentItemAfterDeletion(nextToken);
this._tokenDeleting = true;
- this.fireDecoratorEvent("token-delete", { tokens: [token] });
+ this.fireDecoratorEvent("token-delete", { tokens: tokensToDelete });
}
_handleCurrentItemAfterDeletion(nextToken: Token) {
@@ -643,28 +663,29 @@ class Tokenizer extends UI5Element implements IFormInputElement {
nextToken = notSelectedTokens[0];
}
+ const tokensToDelete = this._selectedTokens.length ? this._selectedTokens : [token];
+ this._announceTokenDeletion(tokensToDelete.length);
+
this._handleCurrentItemAfterDeletion(nextToken);
this._tokenDeleting = true;
- if (this._selectedTokens.length) {
- this.fireDecoratorEvent("token-delete", { tokens: this._selectedTokens });
- } else {
- this.fireDecoratorEvent("token-delete", { tokens: [token] });
- }
+ this.fireDecoratorEvent("token-delete", { tokens: tokensToDelete });
}
async itemDelete(e: CustomEvent) {
const token = this.getTokenByRefId(e.detail.item.getAttribute("data-ui5-token-ref-id")!);
const tokensArray = this._tokens;
+ const tokensToDelete = [token];
// delay the token deletion in order to close the popover before removing token of the DOM
if (tokensArray.length === 1) {
const morePopover = this.getPopover();
morePopover.addEventListener("ui5-close", () => {
- this.fireDecoratorEvent("token-delete", { tokens: [token] });
+ this._announceTokenDeletion(tokensToDelete.length);
+ this.fireDecoratorEvent("token-delete", { tokens: tokensToDelete });
}, {
once: true,
});
@@ -673,7 +694,8 @@ class Tokenizer extends UI5Element implements IFormInputElement {
if (isPhone()) {
this._deletedDialogItems.push(token);
} else {
- this.fireDecoratorEvent("token-delete", { tokens: [token] });
+ this._announceTokenDeletion(tokensToDelete.length);
+ this.fireDecoratorEvent("token-delete", { tokens: tokensToDelete });
}
const currentListItem = e.detail.item as ListItem;
const nextListItem = currentListItem.nextElementSibling as ListItem;
@@ -717,6 +739,7 @@ class Tokenizer extends UI5Element implements IFormInputElement {
const confirm = !!isOkButton;
if (confirm && this._deletedDialogItems.length) {
+ this._announceTokenDeletion(this._deletedDialogItems.length);
this.fireDecoratorEvent("token-delete", { tokens: this._deletedDialogItems });
}
diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties
index 8124b01bcbc93..b169885058ef3 100644
--- a/packages/main/src/i18n/messagebundle.properties
+++ b/packages/main/src/i18n/messagebundle.properties
@@ -670,6 +670,12 @@ TOKENIZER_DIALOG_CANCEL_BUTTON=Cancel
#XBUT: Tokenizer's Dialog OK button on mobile devices
TOKENIZER_DIALOG_OK_BUTTON=OK
+#XACT: ARIA announcement for single token deletion
+TOKENIZER_TOKEN_DELETED_SINGULAR=1 token deleted
+
+#XACT: ARIA announcement for multiple tokens deletion
+TOKENIZER_TOKEN_DELETED_PLURAL={0} tokens deleted
+
#XACT: Label text for TreeListItem
TREE_ITEM_ARIA_LABEL=Tree Item