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
31 changes: 15 additions & 16 deletions packages/main/cypress/specs/TableCustomAnnouncement.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ import "../../src/TableSelectionSingle.js";
import * as Translations from "../../src/generated/i18n/i18n-defaults.js";

const {
TABLE_CELL_MULTIPLE_CONTROLS: { defaultText: CONTAINS_CONTROLS },
TABLE_CELL_SINGLE_CONTROL: { defaultText: CONTAINS_CONTROL },
TABLE_ACC_STATE_READONLY: { defaultText: READONLY },
TABLE_ACC_STATE_DISABLED: { defaultText: DISABLED },
TABLE_ACC_STATE_REQUIRED: { defaultText: REQUIRED },
ACC_STATE_MULTIPLE_CONTROLS: { defaultText: CONTAINS_CONTROLS },
ACC_STATE_SINGLE_CONTROL: { defaultText: CONTAINS_CONTROL },
ACC_STATE_READONLY: { defaultText: READONLY },
ACC_STATE_DISABLED: { defaultText: DISABLED },
ACC_STATE_REQUIRED: { defaultText: REQUIRED },
ACC_STATE_EMPTY: { defaultText: EMPTY },
CHECKBOX_CHECKED: { defaultText: CHECKED },
CHECKBOX_NOT_CHECKED: { defaultText: NOT_CHECKED },
TABLE_ROW_SINGLE_ACTION: { defaultText: ONE_ROW_ACTION },
TABLE_ROW_MULTIPLE_ACTIONS: { defaultText: MULTIPLE_ACTIONS },
TABLE_ACC_STATE_EMPTY: { defaultText: EMPTY },
TABLE_GENERATED_BY_AI: { defaultText: GENERATED_BY_AI },
TABLE_ROW_ACTIONS: { defaultText: ROW_ACTIONS },
TABLE_COLUMNHEADER_SELECTALL_DESCRIPTION: { defaultText: SELECT_ALL_CHECKBOX },
TABLE_COLUMNHEADER_SELECTALL_CHECKED: { defaultText: CHECKED },
TABLE_COLUMNHEADER_SELECTALL_NOT_CHECKED: { defaultText: NOT_CHECKED },
TABLE_COLUMNHEADER_CLEARALL_DESCRIPTION: { defaultText: CLEAR_ALL_BUTTON },
TABLE_SELECTION: { defaultText: SELECTION },
TABLE_COLUMN_HEADER_ROW: { defaultText: COLUMN_HEADER_ROW },
Expand All @@ -47,7 +47,7 @@ describe("Cell Custom Announcement - More details", () => {
<Label required>Header1</Label>
<TableHeaderCellActionAI slot="action"></TableHeaderCellActionAI>
</TableHeaderCell>
<TableHeaderCell data-ui5-table-acc-text="Header2"><input/></TableHeaderCell>
<TableHeaderCell data-ui5-acc-text="Header2"><input /></TableHeaderCell>
<TableHeaderCell><div>Header3</div></TableHeaderCell>
<TableHeaderCell sort-indicator="Descending"></TableHeaderCell>
</TableHeaderRow>
Expand Down Expand Up @@ -84,8 +84,7 @@ describe("Cell Custom Announcement - More details", () => {
}

cy.get("body").then($body => {
debugger;
expect($body.find("#ui5-table-invisible-text").text()).to.equal(expectedText);
expect($body.find("#ui5-invisible-text").text()).to.equal(expectedText);
});
}

Expand All @@ -112,7 +111,7 @@ describe("Cell Custom Announcement - More details", () => {
cy.get("@row1Input2").invoke("removeAttr", "hidden");
checkAnnouncement(CONTAINS_CONTROLS, true);

cy.get("@row1Input1").invoke("attr", "data-ui5-table-acc-text", "Input with custom accessibility text");
cy.get("@row1Input1").invoke("attr", "data-ui5-acc-text", "Input with custom accessibility text");
checkAnnouncement(`Input with custom accessibility text . ${CONTAINS_CONTROLS}`, true);

cy.realPress("ArrowRight"); // third cell focused
Expand Down Expand Up @@ -143,7 +142,7 @@ describe("Cell Custom Announcement - More details", () => {
});
checkAnnouncement(`Button Row1Cell3Button ${REQUIRED} ${DISABLED} ${READONLY} . ${CONTAINS_CONTROL}`, true);

cy.get("@row1Button").invoke("attr", "data-ui5-table-acc-text", "Button with custom accessibility text");
cy.get("@row1Button").invoke("attr", "data-ui5-acc-text", "Button with custom accessibility text");
checkAnnouncement(`Button with custom accessibility text . ${CONTAINS_CONTROL}`, true);

cy.realPress("ArrowRight"); // Row actions cell
Expand All @@ -152,7 +151,7 @@ describe("Cell Custom Announcement - More details", () => {
.should("have.attr", "role", "gridcell")
.then($rowActionsCell => {
const rowActionsCell = $rowActionsCell[0];
const invisibleText = document.getElementById("ui5-table-invisible-text");
const invisibleText = document.getElementById("ui5-invisible-text");
expect(rowActionsCell.ariaLabelledByElements[0]).to.equal(invisibleText);
rowActionsCell.blur();
expect(rowActionsCell.ariaLabelledByElements).to.equal(null);
Expand Down Expand Up @@ -225,7 +224,7 @@ describe("Row Custom Announcement - Less details", () => {
<div style={{ display: "none" }}>H1DisplayNone</div>
</TableHeaderCell>
<TableHeaderCell minWidth="200px">
<div data-ui5-table-acc-text="H2">H2 Custom Text</div>
<div data-ui5-acc-text="H2">H2 Custom Text</div>
</TableHeaderCell>
<TableHeaderCell id="Header3" minWidth="200px">
<div>H3<div aria-hidden="true">H3AriaHidden</div></div>
Expand Down Expand Up @@ -296,7 +295,7 @@ describe("Row Custom Announcement - Less details", () => {
});

cy.get("body").then($body => {
expect($body.find("#ui5-table-invisible-text").text())[check](expectedText);
expect($body.find("#ui5-invisible-text").text())[check](expectedText);
});
}

Expand Down
8 changes: 4 additions & 4 deletions packages/main/cypress/specs/TableSelections.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import type TableSelectionBase from "../../src/TableSelectionBase.js";
import * as Translations from "../../src/generated/i18n/i18n-defaults.js";

const {
TABLE_COLUMNHEADER_SELECTALL_DESCRIPTION: { defaultText: SELECT_ALL_CHECKBOX },
TABLE_COLUMNHEADER_SELECTALL_CHECKED: { defaultText: CHECKED },
TABLE_COLUMNHEADER_SELECTALL_NOT_CHECKED: { defaultText: NOT_CHECKED },
ACC_STATE_DISABLED: { defaultText: DISABLED },
CHECKBOX_CHECKED: { defaultText: CHECKED },
CHECKBOX_NOT_CHECKED: { defaultText: NOT_CHECKED },
TABLE_SELECT_ALL_ROWS: { defaultText: SELECT_ALL_ROWS },
TABLE_DESELECT_ALL_ROWS: { defaultText: DESELECT_ALL_ROWS },
TABLE_COLUMNHEADER_CLEARALL_DESCRIPTION: { defaultText: CLEAR_ALL_BUTTON },
TABLE_ACC_STATE_DISABLED: { defaultText: DISABLED }
TABLE_COLUMNHEADER_SELECTALL_DESCRIPTION: { defaultText: SELECT_ALL_CHECKBOX },
} = Translations;

function mountTestpage(selectionMode: string) {
Expand Down
124 changes: 124 additions & 0 deletions packages/main/src/CustomAnnouncement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import { getTabbableElements } from "@ui5/webcomponents-base/dist/util/TabbableElements.js";
import type { AccessibilityInfo } from "@ui5/webcomponents-base";
import {
ACC_STATE_EMPTY,
ACC_STATE_REQUIRED,
ACC_STATE_DISABLED,
ACC_STATE_READONLY,
ACC_STATE_SINGLE_CONTROL,
ACC_STATE_MULTIPLE_CONTROLS,
} from "./generated/i18n/i18n-defaults.js";

let i18nBundle: I18nBundle;
let invisibleText: HTMLElement;

const getBundle = (): I18nBundle => {
i18nBundle ??= new I18nBundle("@ui5/webcomponents-base");
return i18nBundle;
};

const checkVisibility = (element: HTMLElement): boolean => {
return element.checkVisibility() || getComputedStyle(element).display === "contents";
};

const applyCustomAnnouncement = (element: HTMLElement, text: string | string[] = []) => {
if (!invisibleText || !invisibleText.isConnected) {
invisibleText = document.createElement("span");
invisibleText.id = "ui5-invisible-text";
invisibleText.hidden = true;
document.body.appendChild(invisibleText);
}

const ariaLabelledByElements = [...((element as any).ariaLabelledByElements || [])];
const invisibleTextIndex = ariaLabelledByElements.indexOf(invisibleText);
text = Array.isArray(text) ? text.filter(Boolean).join(" . ").trim() : text.trim();
invisibleText.textContent = text;

if (text && invisibleTextIndex === -1) {
ariaLabelledByElements.unshift(invisibleText);
(element as any).ariaLabelledByElements = ariaLabelledByElements;
} else if (!text && invisibleTextIndex > -1) {
ariaLabelledByElements.splice(invisibleTextIndex, 1);
(element as any).ariaLabelledByElements = ariaLabelledByElements.length ? ariaLabelledByElements : null;
}
};

type CustomAnnouncementOptions = {
lessDetails?: boolean;
};

const getCustomAnnouncement = (element: Node, options: CustomAnnouncementOptions = {}, _isRootElement: boolean = true): string => {
if (!element) {
return "";
}

if (element.nodeType === Node.TEXT_NODE) {
return (element as Text).data.trim();
}

if (!(element instanceof HTMLElement)) {
return "";
}

if (element.hasAttribute("data-ui5-acc-text")) {
return element.getAttribute("data-ui5-acc-text") || "";
}

if (element.ariaHidden === "true" || !checkVisibility(element)) {
return _isRootElement ? getBundle().getText(ACC_STATE_EMPTY) : "";
}

let childNodes = [] as Array<Node>;
const descriptions = [] as Array<string>;
const accessibilityInfo = (element as any).accessibilityInfo as AccessibilityInfo | undefined;
const { lessDetails } = options;

if (accessibilityInfo) {
const {
type, description, required, disabled, readonly, children,
} = accessibilityInfo;

childNodes = children || [];
type && descriptions.push(type);
description && descriptions.push(description);

if (!lessDetails) {
required && descriptions.push(getBundle().getText(ACC_STATE_REQUIRED));
disabled && descriptions.push(getBundle().getText(ACC_STATE_DISABLED));
readonly && descriptions.push(getBundle().getText(ACC_STATE_READONLY));
}
} else if (element.localName === "slot") {
childNodes = (element as HTMLSlotElement).assignedNodes({ flatten: true });
} else {
childNodes = element.shadowRoot ? [...element.shadowRoot.childNodes] : [...element.childNodes];
}

childNodes.forEach(child => {
const childDescription = getCustomAnnouncement(child, options, false);
childDescription && descriptions.push(childDescription);
});

if (_isRootElement) {
const hasDescription = descriptions.length > 0;
if (!hasDescription || !lessDetails) {
const tabbables = getTabbableElements(element);
const bundleKey = [
hasDescription ? "" : ACC_STATE_EMPTY,
ACC_STATE_SINGLE_CONTROL,
ACC_STATE_MULTIPLE_CONTROLS,
][Math.min(tabbables.length, 2)];
if (bundleKey) {
hasDescription && descriptions.push(".");
descriptions.push(getBundle().getText(bundleKey));
}
}
}

return descriptions.join(" ").trim();
};

export {
getCustomAnnouncement,
applyCustomAnnouncement,
};
12 changes: 6 additions & 6 deletions packages/main/src/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ type TableRowActionClickEventDetail = {
* This can only be achieved through a custom accessibility announcement.
* To support this, UI5 Web Components expose its own accessibility metadata via the `accessibilityInfo` property.
* The `ui5-table` uses this information to create the required custom announcements dynamically.
* If you include custom web components inside table cells that are not part of the standard UI5 Web Components set, their accessibility information can be provided using the `data-ui5-table-acc-text` attribute.
* If you include custom web components inside table cells that are not part of the standard UI5 Web Components set, their accessibility information can be provided using the `data-ui5-acc-text` attribute.
*
* ### ES6 Module Import
*
Expand Down Expand Up @@ -359,11 +359,11 @@ class Table extends UI5Element {
loading = false;

/**
* Defines the delay in milliseconds, after which the loading indicator will show up for this component.
* Defines the delay in milliseconds, after which the loading indicator will show up for this component.
*
* @default 1000
* @public
*/
* @default 1000
* @public
*/
@property({ type: Number })
loadingDelay = 1000;

Expand Down Expand Up @@ -431,7 +431,7 @@ class Table extends UI5Element {
_tableNavigation?: TableNavigation;
_tableDragAndDrop?: TableDragAndDrop;
_tableCustomAnnouncement?: TableCustomAnnouncement;
_poppedIn: Array<{col: TableHeaderCell, width: number}> = [];
_poppedIn: Array<{ col: TableHeaderCell, width: number }> = [];
_containerWidth = 0;

constructor() {
Expand Down
Loading
Loading