Skip to content
Open
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
71 changes: 71 additions & 0 deletions packages/main/cypress/specs/Tokenizer.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Tokenizer onTokenDelete={onTokenDelete}>
<Token text="Andora"></Token>
<Token text="Bulgaria"></Token>
</Tokenizer>
);

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(
<Tokenizer onTokenDelete={onTokenDelete}>
<Token text="Andora"></Token>
<Token text="Bulgaria"></Token>
<Token text="Canada"></Token>
</Tokenizer>
);

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(
<Tokenizer multiLine={true} showClearAll={true} onTokenDelete={onTokenDelete}>
<Token text="Andora"></Token>
<Token text="Bulgaria"></Token>
<Token text="Canada"></Token>
</Tokenizer>
);

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");
});
});
39 changes: 31 additions & 8 deletions packages/main/src/Tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<ListItemDeleteEventDetail>) {
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,
});
Expand All @@ -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;
Expand Down Expand Up @@ -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 });
}

Expand Down
6 changes: 6 additions & 0 deletions packages/main/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading