Skip to content

Commit 72847a8

Browse files
authored
refactor(ui5-table): externalize custom announcement (#12976)
1 parent 050b2a3 commit 72847a8

9 files changed

Lines changed: 197 additions & 174 deletions

packages/main/cypress/specs/TableCustomAnnouncement.cy.tsx

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ import "../../src/TableSelectionSingle.js";
1515
import * as Translations from "../../src/generated/i18n/i18n-defaults.js";
1616

1717
const {
18-
TABLE_CELL_MULTIPLE_CONTROLS: { defaultText: CONTAINS_CONTROLS },
19-
TABLE_CELL_SINGLE_CONTROL: { defaultText: CONTAINS_CONTROL },
20-
TABLE_ACC_STATE_READONLY: { defaultText: READONLY },
21-
TABLE_ACC_STATE_DISABLED: { defaultText: DISABLED },
22-
TABLE_ACC_STATE_REQUIRED: { defaultText: REQUIRED },
18+
ACC_STATE_MULTIPLE_CONTROLS: { defaultText: CONTAINS_CONTROLS },
19+
ACC_STATE_SINGLE_CONTROL: { defaultText: CONTAINS_CONTROL },
20+
ACC_STATE_READONLY: { defaultText: READONLY },
21+
ACC_STATE_DISABLED: { defaultText: DISABLED },
22+
ACC_STATE_REQUIRED: { defaultText: REQUIRED },
23+
ACC_STATE_EMPTY: { defaultText: EMPTY },
24+
CHECKBOX_CHECKED: { defaultText: CHECKED },
25+
CHECKBOX_NOT_CHECKED: { defaultText: NOT_CHECKED },
2326
TABLE_ROW_SINGLE_ACTION: { defaultText: ONE_ROW_ACTION },
2427
TABLE_ROW_MULTIPLE_ACTIONS: { defaultText: MULTIPLE_ACTIONS },
25-
TABLE_ACC_STATE_EMPTY: { defaultText: EMPTY },
2628
TABLE_GENERATED_BY_AI: { defaultText: GENERATED_BY_AI },
2729
TABLE_ROW_ACTIONS: { defaultText: ROW_ACTIONS },
2830
TABLE_COLUMNHEADER_SELECTALL_DESCRIPTION: { defaultText: SELECT_ALL_CHECKBOX },
29-
TABLE_COLUMNHEADER_SELECTALL_CHECKED: { defaultText: CHECKED },
30-
TABLE_COLUMNHEADER_SELECTALL_NOT_CHECKED: { defaultText: NOT_CHECKED },
3131
TABLE_COLUMNHEADER_CLEARALL_DESCRIPTION: { defaultText: CLEAR_ALL_BUTTON },
3232
TABLE_SELECTION: { defaultText: SELECTION },
3333
TABLE_COLUMN_HEADER_ROW: { defaultText: COLUMN_HEADER_ROW },
@@ -47,7 +47,7 @@ describe("Cell Custom Announcement - More details", () => {
4747
<Label required>Header1</Label>
4848
<TableHeaderCellActionAI slot="action"></TableHeaderCellActionAI>
4949
</TableHeaderCell>
50-
<TableHeaderCell data-ui5-table-acc-text="Header2"><input/></TableHeaderCell>
50+
<TableHeaderCell data-ui5-acc-text="Header2"><input /></TableHeaderCell>
5151
<TableHeaderCell><div>Header3</div></TableHeaderCell>
5252
<TableHeaderCell sort-indicator="Descending"></TableHeaderCell>
5353
</TableHeaderRow>
@@ -84,8 +84,7 @@ describe("Cell Custom Announcement - More details", () => {
8484
}
8585

8686
cy.get("body").then($body => {
87-
debugger;
88-
expect($body.find("#ui5-table-invisible-text").text()).to.equal(expectedText);
87+
expect($body.find("#ui5-invisible-text").text()).to.equal(expectedText);
8988
});
9089
}
9190

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

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

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

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

149148
cy.realPress("ArrowRight"); // Row actions cell
@@ -152,7 +151,7 @@ describe("Cell Custom Announcement - More details", () => {
152151
.should("have.attr", "role", "gridcell")
153152
.then($rowActionsCell => {
154153
const rowActionsCell = $rowActionsCell[0];
155-
const invisibleText = document.getElementById("ui5-table-invisible-text");
154+
const invisibleText = document.getElementById("ui5-invisible-text");
156155
expect(rowActionsCell.ariaLabelledByElements[0]).to.equal(invisibleText);
157156
rowActionsCell.blur();
158157
expect(rowActionsCell.ariaLabelledByElements).to.equal(null);
@@ -225,7 +224,7 @@ describe("Row Custom Announcement - Less details", () => {
225224
<div style={{ display: "none" }}>H1DisplayNone</div>
226225
</TableHeaderCell>
227226
<TableHeaderCell minWidth="200px">
228-
<div data-ui5-table-acc-text="H2">H2 Custom Text</div>
227+
<div data-ui5-acc-text="H2">H2 Custom Text</div>
229228
</TableHeaderCell>
230229
<TableHeaderCell id="Header3" minWidth="200px">
231230
<div>H3<div aria-hidden="true">H3AriaHidden</div></div>
@@ -296,7 +295,7 @@ describe("Row Custom Announcement - Less details", () => {
296295
});
297296

298297
cy.get("body").then($body => {
299-
expect($body.find("#ui5-table-invisible-text").text())[check](expectedText);
298+
expect($body.find("#ui5-invisible-text").text())[check](expectedText);
300299
});
301300
}
302301

packages/main/cypress/specs/TableSelections.cy.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import type TableSelectionBase from "../../src/TableSelectionBase.js";
1010
import * as Translations from "../../src/generated/i18n/i18n-defaults.js";
1111

1212
const {
13-
TABLE_COLUMNHEADER_SELECTALL_DESCRIPTION: { defaultText: SELECT_ALL_CHECKBOX },
14-
TABLE_COLUMNHEADER_SELECTALL_CHECKED: { defaultText: CHECKED },
15-
TABLE_COLUMNHEADER_SELECTALL_NOT_CHECKED: { defaultText: NOT_CHECKED },
13+
ACC_STATE_DISABLED: { defaultText: DISABLED },
14+
CHECKBOX_CHECKED: { defaultText: CHECKED },
15+
CHECKBOX_NOT_CHECKED: { defaultText: NOT_CHECKED },
1616
TABLE_SELECT_ALL_ROWS: { defaultText: SELECT_ALL_ROWS },
1717
TABLE_DESELECT_ALL_ROWS: { defaultText: DESELECT_ALL_ROWS },
1818
TABLE_COLUMNHEADER_CLEARALL_DESCRIPTION: { defaultText: CLEAR_ALL_BUTTON },
19-
TABLE_ACC_STATE_DISABLED: { defaultText: DISABLED }
19+
TABLE_COLUMNHEADER_SELECTALL_DESCRIPTION: { defaultText: SELECT_ALL_CHECKBOX },
2020
} = Translations;
2121

2222
function mountTestpage(selectionMode: string) {
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
2+
import { getTabbableElements } from "@ui5/webcomponents-base/dist/util/TabbableElements.js";
3+
import type { AccessibilityInfo } from "@ui5/webcomponents-base";
4+
import {
5+
ACC_STATE_EMPTY,
6+
ACC_STATE_REQUIRED,
7+
ACC_STATE_DISABLED,
8+
ACC_STATE_READONLY,
9+
ACC_STATE_SINGLE_CONTROL,
10+
ACC_STATE_MULTIPLE_CONTROLS,
11+
} from "./generated/i18n/i18n-defaults.js";
12+
13+
let i18nBundle: I18nBundle;
14+
let invisibleText: HTMLElement;
15+
16+
const getBundle = (): I18nBundle => {
17+
i18nBundle ??= new I18nBundle("@ui5/webcomponents-base");
18+
return i18nBundle;
19+
};
20+
21+
const checkVisibility = (element: HTMLElement): boolean => {
22+
return element.checkVisibility() || getComputedStyle(element).display === "contents";
23+
};
24+
25+
const applyCustomAnnouncement = (element: HTMLElement, text: string | string[] = []) => {
26+
if (!invisibleText || !invisibleText.isConnected) {
27+
invisibleText = document.createElement("span");
28+
invisibleText.id = "ui5-invisible-text";
29+
invisibleText.hidden = true;
30+
document.body.appendChild(invisibleText);
31+
}
32+
33+
const ariaLabelledByElements = [...((element as any).ariaLabelledByElements || [])];
34+
const invisibleTextIndex = ariaLabelledByElements.indexOf(invisibleText);
35+
text = Array.isArray(text) ? text.filter(Boolean).join(" . ").trim() : text.trim();
36+
invisibleText.textContent = text;
37+
38+
if (text && invisibleTextIndex === -1) {
39+
ariaLabelledByElements.unshift(invisibleText);
40+
(element as any).ariaLabelledByElements = ariaLabelledByElements;
41+
} else if (!text && invisibleTextIndex > -1) {
42+
ariaLabelledByElements.splice(invisibleTextIndex, 1);
43+
(element as any).ariaLabelledByElements = ariaLabelledByElements.length ? ariaLabelledByElements : null;
44+
}
45+
};
46+
47+
type CustomAnnouncementOptions = {
48+
lessDetails?: boolean;
49+
};
50+
51+
const getCustomAnnouncement = (element: Node, options: CustomAnnouncementOptions = {}, _isRootElement: boolean = true): string => {
52+
if (!element) {
53+
return "";
54+
}
55+
56+
if (element.nodeType === Node.TEXT_NODE) {
57+
return (element as Text).data.trim();
58+
}
59+
60+
if (!(element instanceof HTMLElement)) {
61+
return "";
62+
}
63+
64+
if (element.hasAttribute("data-ui5-acc-text")) {
65+
return element.getAttribute("data-ui5-acc-text") || "";
66+
}
67+
68+
if (element.ariaHidden === "true" || !checkVisibility(element)) {
69+
return _isRootElement ? getBundle().getText(ACC_STATE_EMPTY) : "";
70+
}
71+
72+
let childNodes = [] as Array<Node>;
73+
const descriptions = [] as Array<string>;
74+
const accessibilityInfo = (element as any).accessibilityInfo as AccessibilityInfo | undefined;
75+
const { lessDetails } = options;
76+
77+
if (accessibilityInfo) {
78+
const {
79+
type, description, required, disabled, readonly, children,
80+
} = accessibilityInfo;
81+
82+
childNodes = children || [];
83+
type && descriptions.push(type);
84+
description && descriptions.push(description);
85+
86+
if (!lessDetails) {
87+
required && descriptions.push(getBundle().getText(ACC_STATE_REQUIRED));
88+
disabled && descriptions.push(getBundle().getText(ACC_STATE_DISABLED));
89+
readonly && descriptions.push(getBundle().getText(ACC_STATE_READONLY));
90+
}
91+
} else if (element.localName === "slot") {
92+
childNodes = (element as HTMLSlotElement).assignedNodes({ flatten: true });
93+
} else {
94+
childNodes = element.shadowRoot ? [...element.shadowRoot.childNodes] : [...element.childNodes];
95+
}
96+
97+
childNodes.forEach(child => {
98+
const childDescription = getCustomAnnouncement(child, options, false);
99+
childDescription && descriptions.push(childDescription);
100+
});
101+
102+
if (_isRootElement) {
103+
const hasDescription = descriptions.length > 0;
104+
if (!hasDescription || !lessDetails) {
105+
const tabbables = getTabbableElements(element);
106+
const bundleKey = [
107+
hasDescription ? "" : ACC_STATE_EMPTY,
108+
ACC_STATE_SINGLE_CONTROL,
109+
ACC_STATE_MULTIPLE_CONTROLS,
110+
][Math.min(tabbables.length, 2)];
111+
if (bundleKey) {
112+
hasDescription && descriptions.push(".");
113+
descriptions.push(getBundle().getText(bundleKey));
114+
}
115+
}
116+
}
117+
118+
return descriptions.join(" ").trim();
119+
};
120+
121+
export {
122+
getCustomAnnouncement,
123+
applyCustomAnnouncement,
124+
};

packages/main/src/Table.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ type TableRowActionClickEventDetail = {
170170
* This can only be achieved through a custom accessibility announcement.
171171
* To support this, UI5 Web Components expose its own accessibility metadata via the `accessibilityInfo` property.
172172
* The `ui5-table` uses this information to create the required custom announcements dynamically.
173-
* 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.
173+
* 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.
174174
*
175175
* ### ES6 Module Import
176176
*
@@ -359,11 +359,11 @@ class Table extends UI5Element {
359359
loading = false;
360360

361361
/**
362-
* Defines the delay in milliseconds, after which the loading indicator will show up for this component.
362+
* Defines the delay in milliseconds, after which the loading indicator will show up for this component.
363363
*
364-
* @default 1000
365-
* @public
366-
*/
364+
* @default 1000
365+
* @public
366+
*/
367367
@property({ type: Number })
368368
loadingDelay = 1000;
369369

@@ -431,7 +431,7 @@ class Table extends UI5Element {
431431
_tableNavigation?: TableNavigation;
432432
_tableDragAndDrop?: TableDragAndDrop;
433433
_tableCustomAnnouncement?: TableCustomAnnouncement;
434-
_poppedIn: Array<{col: TableHeaderCell, width: number}> = [];
434+
_poppedIn: Array<{ col: TableHeaderCell, width: number }> = [];
435435
_containerWidth = 0;
436436

437437
constructor() {

0 commit comments

Comments
 (0)