Skip to content

Commit 9d88ec0

Browse files
committed
refactor(ui5-table): externalize custom announcement
1 parent b082fcf commit 9d88ec0

File tree

9 files changed

+499
-466
lines changed

9 files changed

+499
-466
lines changed

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

Lines changed: 325 additions & 325 deletions
Large diffs are not rendered by default.

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import * as Translations from "../../src/generated/i18n/i18n-defaults.js";
1111

1212
const {
1313
TABLE_COLUMNHEADER_SELECTALL_DESCRIPTION: { defaultText: SELECT_ALL_CHECKBOX },
14-
TABLE_COLUMNHEADER_SELECTALL_CHECKED: { defaultText: CHECKED },
15-
TABLE_COLUMNHEADER_SELECTALL_NOT_CHECKED: { defaultText: NOT_CHECKED },
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+
ACC_STATE_DISABLED: { defaultText: DISABLED }
2020
} = Translations;
2121

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

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() {

packages/main/src/TableCustomAnnouncement.ts

Lines changed: 14 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import TableExtension from "./TableExtension.js";
2-
import I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
3-
import { getTabbableElements } from "@ui5/webcomponents-base/dist/util/TabbableElements.js";
4-
import type { AccessibilityInfo } from "@ui5/webcomponents-base";
2+
import { updateInvisibleText, getAccessibilityDescription } from "./CustomAnnouncement.js";
53
import type Table from "./Table.js";
64
import type TableRow from "./TableRow.js";
75
import type TableCell from "./TableCell.js";
@@ -14,112 +12,8 @@ import {
1412
TABLE_ROW_NAVIGABLE,
1513
TABLE_ROW_NAVIGATED,
1614
TABLE_COLUMN_HEADER_ROW,
17-
TABLE_CELL_SINGLE_CONTROL,
18-
TABLE_CELL_MULTIPLE_CONTROLS,
19-
TABLE_ACC_STATE_EMPTY,
20-
TABLE_ACC_STATE_REQUIRED,
21-
TABLE_ACC_STATE_DISABLED,
22-
TABLE_ACC_STATE_READONLY,
2315
} from "./generated/i18n/i18n-defaults.js";
2416

25-
let invisibleText: HTMLElement;
26-
const i18nBundle = new I18nBundle("@ui5/webcomponents");
27-
28-
const checkVisibility = (element: HTMLElement): boolean => {
29-
return element.checkVisibility() || getComputedStyle(element).display === "contents";
30-
};
31-
32-
const updateInvisibleText = (element: any, text: string | string[] = []) => {
33-
if (!invisibleText || !invisibleText.isConnected) {
34-
invisibleText = document.createElement("span");
35-
invisibleText.id = "ui5-table-invisible-text";
36-
invisibleText.ariaHidden = "true";
37-
invisibleText.style.display = "none";
38-
document.body.appendChild(invisibleText);
39-
}
40-
41-
const ariaLabelledByElements = [...(element.ariaLabelledByElements || [])];
42-
const invisibleTextIndex = ariaLabelledByElements.indexOf(invisibleText);
43-
text = Array.isArray(text) ? text.filter(Boolean).join(" . ").trim() : text.trim();
44-
invisibleText.textContent = text;
45-
46-
if (text && invisibleTextIndex === -1) {
47-
ariaLabelledByElements.unshift(invisibleText);
48-
element.ariaLabelledByElements = ariaLabelledByElements;
49-
} else if (!text && invisibleTextIndex > -1) {
50-
ariaLabelledByElements.splice(invisibleTextIndex, 1);
51-
element.ariaLabelledByElements = ariaLabelledByElements.length ? ariaLabelledByElements : null;
52-
}
53-
};
54-
55-
const getAccessibilityDescription = (element: Node, lessDetails: boolean = false, _isRootElement: boolean = true): string => {
56-
if (!element) {
57-
return "";
58-
}
59-
60-
if (element.nodeType === Node.TEXT_NODE) {
61-
return (element as Text).data.trim();
62-
}
63-
64-
if (!(element instanceof HTMLElement)) {
65-
return "";
66-
}
67-
68-
if (element.hasAttribute("data-ui5-table-acc-text")) {
69-
return element.getAttribute("data-ui5-table-acc-text") || "";
70-
}
71-
72-
if (element.ariaHidden === "true" || !checkVisibility(element)) {
73-
return _isRootElement ? i18nBundle.getText(TABLE_ACC_STATE_EMPTY) : "";
74-
}
75-
76-
let childNodes = [] as Array<Node>;
77-
const descriptions = [] as Array<string>;
78-
const accessibilityInfo = (element as any).accessibilityInfo as AccessibilityInfo | undefined;
79-
if (accessibilityInfo) {
80-
const {
81-
type, description, required, disabled, readonly, children,
82-
} = accessibilityInfo;
83-
84-
childNodes = children || [];
85-
type && descriptions.push(type);
86-
description && descriptions.push(description);
87-
88-
if (!lessDetails) {
89-
required && descriptions.push(i18nBundle.getText(TABLE_ACC_STATE_REQUIRED));
90-
disabled && descriptions.push(i18nBundle.getText(TABLE_ACC_STATE_DISABLED));
91-
readonly && descriptions.push(i18nBundle.getText(TABLE_ACC_STATE_READONLY));
92-
}
93-
} else if (element.localName === "slot") {
94-
childNodes = (element as HTMLSlotElement).assignedNodes({ flatten: true });
95-
} else {
96-
childNodes = element.shadowRoot ? [...element.shadowRoot.childNodes] : [...element.childNodes];
97-
}
98-
99-
childNodes.forEach(child => {
100-
const childDescription = getAccessibilityDescription(child, lessDetails, false);
101-
childDescription && descriptions.push(childDescription);
102-
});
103-
104-
if (_isRootElement) {
105-
const hasDescription = descriptions.length > 0;
106-
if (!hasDescription || !lessDetails) {
107-
const tabbables = getTabbableElements(element);
108-
const bundleKey = [
109-
hasDescription ? "" : TABLE_ACC_STATE_EMPTY,
110-
TABLE_CELL_SINGLE_CONTROL,
111-
TABLE_CELL_MULTIPLE_CONTROLS,
112-
][Math.min(tabbables.length, 2)];
113-
if (bundleKey) {
114-
hasDescription && descriptions.push(".");
115-
descriptions.push(i18nBundle.getText(bundleKey));
116-
}
117-
}
118-
}
119-
120-
return descriptions.join(" ").trim();
121-
};
122-
12317
/**
12418
* Handles the custom announcement for the ui5-table.
12519
*
@@ -135,6 +29,10 @@ class TableCustomAnnouncement extends TableExtension {
13529
this._table = table;
13630
}
13731

32+
private get i18nBundle() {
33+
return (this._table.constructor as typeof Table).i18nBundle;
34+
}
35+
13836
_onfocusin(e: FocusEvent, eventOrigin: HTMLElement) {
13937
const tableAttribute = this._tableAttributes.find(attr => eventOrigin.hasAttribute(attr));
14038
if (!tableAttribute) {
@@ -163,15 +61,15 @@ class TableCustomAnnouncement extends TableExtension {
16361

16462
_handleTableHeaderRowFocusin(headerRow: TableHeaderRow) {
16563
const descriptions = [
166-
i18nBundle.getText(TABLE_COLUMN_HEADER_ROW),
64+
this.i18nBundle.getText(TABLE_COLUMN_HEADER_ROW),
16765
];
16866

16967
if (headerRow._hasSelector) {
17068
descriptions.push(headerRow._isMultiSelect ? headerRow._selectionCellAriaDescription! : headerRow._i18nSelection);
17169
}
17270

17371
headerRow._visibleCells.forEach(headerCell => {
174-
const cellDescription = getAccessibilityDescription(headerCell, true);
72+
const cellDescription = getAccessibilityDescription(headerCell, { lessDetails: true });
17573
descriptions.push(cellDescription);
17674
});
17775

@@ -188,25 +86,25 @@ class TableCustomAnnouncement extends TableExtension {
18886
}
18987

19088
const descriptions = [
191-
i18nBundle.getText(TABLE_ROW),
192-
i18nBundle.getText(TABLE_ROW_INDEX, row.ariaRowIndex!, this._table._ariaRowCount),
89+
this.i18nBundle.getText(TABLE_ROW),
90+
this.i18nBundle.getText(TABLE_ROW_INDEX, row.ariaRowIndex!, this._table._ariaRowCount),
19391
];
19492

19593
if (row._isSelected) {
196-
descriptions.push(i18nBundle.getText(TABLE_ROW_SELECTED));
94+
descriptions.push(this.i18nBundle.getText(TABLE_ROW_SELECTED));
19795
}
19896

19997
if (row._isNavigable) {
200-
descriptions.push(i18nBundle.getText(TABLE_ROW_NAVIGABLE));
98+
descriptions.push(this.i18nBundle.getText(TABLE_ROW_NAVIGABLE));
20199
} else if (row.interactive) {
202-
descriptions.push(i18nBundle.getText(TABLE_ROW_ACTIVE));
100+
descriptions.push(this.i18nBundle.getText(TABLE_ROW_ACTIVE));
203101
}
204102

205103
const cells = [...row._visibleCells, ...row._popinCells];
206104
cells.flatMap(cell => {
207105
return cell._popin ? [cell._popinHeader!, cell._popinContent!] : [cell._headerCell!, cell];
208106
}).forEach(node => {
209-
const nodeDescription = getAccessibilityDescription(node, true);
107+
const nodeDescription = getAccessibilityDescription(node, { lessDetails: true });
210108
descriptions.push(nodeDescription);
211109
});
212110

@@ -215,7 +113,7 @@ class TableCustomAnnouncement extends TableExtension {
215113
}
216114

217115
if (row._renderNavigated && row.navigated) {
218-
descriptions.push(i18nBundle.getText(TABLE_ROW_NAVIGATED));
116+
descriptions.push(this.i18nBundle.getText(TABLE_ROW_NAVIGATED));
219117
}
220118

221119
updateInvisibleText(row, descriptions);

packages/main/src/TableHeaderRowTemplate.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function TableHeaderRowTemplate(this: TableHeaderRow, ariaColInde
1717
aria-colindex={ariaColIndex++}
1818
data-ui5-table-selection-cell
1919
data-ui5-table-cell-fixed
20-
data-ui5-table-acc-text=""
20+
data-ui5-acc-text=""
2121
>
2222
{ !this._isMultiSelect ?
2323
<></>

packages/main/src/TableRowTemplate.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default function TableRowTemplate(this: TableRow, ariaColIndex: number =
1515
aria-colindex={ariaColIndex++}
1616
data-ui5-table-selection-cell
1717
data-ui5-table-cell-fixed
18-
data-ui5-table-acc-text=""
18+
data-ui5-acc-text=""
1919
>
2020
{ this._isMultiSelect ?
2121
<CheckBox id="selection-component"
@@ -51,7 +51,7 @@ export default function TableRowTemplate(this: TableRow, ariaColIndex: number =
5151
{ this._rowActionCount > 0 &&
5252
<TableCell id="actions-cell"
5353
aria-colindex={ariaColIndex++}
54-
data-ui5-table-acc-text={this._actionCellAccText}
54+
data-ui5-acc-text={this._actionCellAccText}
5555
>
5656
{ this._flexibleActions.map(action => (
5757
<slot name={action._individualSlot}></slot>

0 commit comments

Comments
 (0)