Skip to content

Commit d4d7899

Browse files
committed
refactor(ui5-table): advanced custom announcement
1 parent 00afa74 commit d4d7899

17 files changed

Lines changed: 1102 additions & 16 deletions

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

Lines changed: 235 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ const {
3535
TABLE_ROW_NAVIGABLE: { defaultText: NAVIGABLE },
3636
TABLE_ROW_NAVIGATED: { defaultText: NAVIGATED },
3737
TABLE_ROW_ACTIVE: { defaultText: ACTIVE },
38+
TABLE_ROW_ACTION: { defaultText: ACTION_TEMPLATE },
39+
TABLE_ROW_ACTIONS_LIST: { defaultText: ACTIONS_LIST_TEMPLATE },
40+
TABLE_ROW_MORE_ACTIONS: { defaultText: MORE_ACTIONS },
41+
TABLE_ENTERING: { defaultText: ENTERING_TEMPLATE },
42+
TABLE_ENTERING_MULTI_SELECTABLE: { defaultText: MULTI_SELECTABLE },
43+
TABLE_ENTERING_SELECTED: { defaultText: ENTERING_SELECTED_TEMPLATE },
44+
TABLE_ROW_SELECTED_LIVE: { defaultText: SELECTED_LIVE_TEMPLATE },
45+
TABLE_ROW_NOT_SELECTED_LIVE: { defaultText: NOT_SELECTED_LIVE_TEMPLATE },
46+
TABLE_HIGHLIGHT_NEGATIVE: { defaultText: HIGHLIGHT_NEGATIVE },
47+
TABLE_HIGHLIGHT_CRITICAL: { defaultText: HIGHLIGHT_CRITICAL },
48+
TABLE_HIGHLIGHT_POSITIVE: { defaultText: HIGHLIGHT_POSITIVE },
49+
TABLE_HIGHLIGHT_INFORMATION: { defaultText: HIGHLIGHT_INFORMATION },
3850
} = Translations;
3951

4052
describe("Cell Custom Announcement - More details", () => {
@@ -301,28 +313,23 @@ describe("Row Custom Announcement - Less details", () => {
301313

302314
it("should announce table rows", () => {
303315
cy.get("@row1").realClick();
304-
checkAnnouncement(`Row . 2 of 2 . ${SELECTED} . ${NAVIGABLE} . H1`);
305-
checkAnnouncement(`H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H3 . ${EMPTY} . H4 . C4 Button C4Button`);
306-
checkAnnouncement(ONE_ROW_ACTION);
307-
cy.focused().should("have.attr", "aria-rowindex", "2")
308-
.should("have.attr", "role", "row");
316+
// No identifier column → legacy format: Row → position → selected → navigable/active → cells → actions → navigated
317+
checkAnnouncement(`Row . 2 of 2 . ${SELECTED} . ${NAVIGABLE} . H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H3 . ${EMPTY} . H4 . C4 Button C4Button . ${ONE_ROW_ACTION} . ${NAVIGATED}`);
309318

310319
cy.get("#selection").invoke("attr", "selected", "");
311-
checkAnnouncement(`Row . 2 of 2 . ${NAVIGABLE}`, true);
320+
checkAnnouncement(`Row . 2 of 2 . ${NAVIGABLE} . H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H3 . ${EMPTY} . H4 . C4 Button C4Button . ${ONE_ROW_ACTION} . ${NAVIGATED}`, true);
312321

313322
cy.get("#row1-nav-action").invoke("prop", "interactive", true);
314-
checkAnnouncement(`Row . 2 of 2 . ${ACTIVE} . H1`, true);
315-
checkAnnouncement(Table.i18nBundle.getText(MULTIPLE_ACTIONS, 2));
323+
checkAnnouncement(`Row . 2 of 2 . ${ACTIVE} . H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H3 . ${EMPTY} . H4 . C4 Button C4Button . ${MULTIPLE_ACTIONS.replace("{0}", 2)} . ${NAVIGATED}`, true);
316324

317325
cy.get("@row1").invoke("prop", "interactive", false);
318-
checkAnnouncement(`Row . 2 of 2 . H1`, true);
326+
checkAnnouncement(`Row . 2 of 2 . H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H3 . ${EMPTY} . H4 . C4 Button C4Button . ${MULTIPLE_ACTIONS.replace("{0}", 2)} . ${NAVIGATED}`, true);
319327

320328
cy.get("#table0").invoke("css", "width", "301px");
321-
checkAnnouncement(`Row . 2 of 2 . H1`, true);
322-
checkAnnouncement(`H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H3 . ${EMPTY} . H4Popin . C4 Button C4Button`);
329+
checkAnnouncement(`Row . 2 of 2 . H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H3 . ${EMPTY} . H4Popin . C4 Button C4Button . ${MULTIPLE_ACTIONS.replace("{0}", 2)} . ${NAVIGATED}`, true);
323330

324331
cy.get("#Header3").invoke("prop", "popinHidden", true);
325-
checkAnnouncement(`H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H4Popin . C4 Button C4Button`, true);
332+
checkAnnouncement(`Row . 2 of 2 . H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H4Popin . C4 Button C4Button . ${MULTIPLE_ACTIONS.replace("{0}", 2)} . ${NAVIGATED}`, true);
326333

327334
cy.get("#row1-nav-action").invoke("remove");
328335
cy.get("#row1-add-action").invoke("remove");
@@ -423,3 +430,219 @@ describe("Row Custom Announcement - Less details", () => {
423430
});
424431
});
425432
});
433+
434+
describe("Identifier Column Announcement", () => {
435+
function checkAnnouncement(expectedText: string, check = "equal") {
436+
cy.get("body").then($body => {
437+
expect($body.find("#ui5-invisible-text").text())[check](expectedText);
438+
});
439+
}
440+
441+
it("should announce only identifier column when set", () => {
442+
cy.mount(
443+
<Table id="table0">
444+
<TableHeaderRow slot="headerRow">
445+
<TableHeaderCell id="docNumHeader" identifier>Document Number</TableHeaderCell>
446+
<TableHeaderCell>Company</TableHeaderCell>
447+
<TableHeaderCell>City</TableHeaderCell>
448+
</TableHeaderRow>
449+
<TableRow rowKey="Row1">
450+
<TableCell>305382373</TableCell>
451+
<TableCell>SAP SE</TableCell>
452+
<TableCell>Walldorf</TableCell>
453+
</TableRow>
454+
<TableRow rowKey="Row2">
455+
<TableCell>123456789</TableCell>
456+
<TableCell>Acme Corp</TableCell>
457+
<TableCell>Berlin</TableCell>
458+
</TableRow>
459+
</Table>
460+
);
461+
462+
// First focus on row1 — entering text + identifier + Row + position
463+
cy.get("[ui5-table-row]").first().realClick();
464+
checkAnnouncement("with 2 rows", "contains");
465+
checkAnnouncement("Document Number 305382373 . Row . 2 of 3", "contains");
466+
467+
// Focus row2 — no entering announcement
468+
cy.realPress("ArrowDown");
469+
checkAnnouncement("Document Number 123456789 . Row . 3 of 3");
470+
471+
// Remove identifier — fallback to legacy format (Row first)
472+
cy.get("#docNumHeader").invoke("prop", "identifier", false);
473+
cy.realPress("ArrowUp");
474+
cy.realPress("ArrowDown");
475+
checkAnnouncement("Row . 3 of 3 . Document Number . 123456789 . Company . Acme Corp . City . Berlin");
476+
});
477+
478+
it("should set role=rowheader on identifier column cells", () => {
479+
cy.mount(
480+
<Table id="table0">
481+
<TableHeaderRow slot="headerRow">
482+
<TableHeaderCell identifier>ID</TableHeaderCell>
483+
<TableHeaderCell>Name</TableHeaderCell>
484+
</TableHeaderRow>
485+
<TableRow rowKey="Row1">
486+
<TableCell id="idCell">001</TableCell>
487+
<TableCell id="nameCell">Alice</TableCell>
488+
</TableRow>
489+
</Table>
490+
);
491+
492+
cy.get("#idCell").should("have.attr", "role", "rowheader");
493+
cy.get("#nameCell").should("have.attr", "role", "gridcell");
494+
});
495+
});
496+
497+
describe("Row Highlight Announcement", () => {
498+
function checkAnnouncement(expectedText: string, focusAgain = false, check = "equal") {
499+
if (focusAgain) {
500+
cy.realPress("ArrowUp");
501+
cy.realPress("ArrowDown");
502+
}
503+
504+
cy.get("body").then($body => {
505+
expect($body.find("#ui5-invisible-text").text())[check](expectedText);
506+
});
507+
}
508+
509+
it("should announce highlight state and text", () => {
510+
cy.mount(
511+
<Table id="table0">
512+
<TableHeaderRow slot="headerRow">
513+
<TableHeaderCell identifier>Order</TableHeaderCell>
514+
<TableHeaderCell>Status</TableHeaderCell>
515+
</TableHeaderRow>
516+
<TableRow id="row1" rowKey="Row1" highlight="Negative" highlightText="Cancelled">
517+
<TableCell>12345</TableCell>
518+
<TableCell>Cancelled</TableCell>
519+
</TableRow>
520+
<TableRow id="row2" rowKey="Row2" highlight="Critical" highlightText="Return Initiated">
521+
<TableCell>67890</TableCell>
522+
<TableCell>Pending</TableCell>
523+
</TableRow>
524+
<TableRow id="row3" rowKey="Row3" highlight="Positive">
525+
<TableCell>11111</TableCell>
526+
<TableCell>Complete</TableCell>
527+
</TableRow>
528+
<TableRow id="row4" rowKey="Row4" highlight="Information" highlightText="Unread">
529+
<TableCell>22222</TableCell>
530+
<TableCell>New</TableCell>
531+
</TableRow>
532+
</Table>
533+
);
534+
535+
// Row 1: Negative + "Cancelled" (first focus includes entering text)
536+
cy.get("#row1").realClick();
537+
checkAnnouncement(`Order 12345 . Row . ${HIGHLIGHT_NEGATIVE} . Cancelled . 2 of 5`, false, "contains");
538+
539+
// Row 2: Critical + "Return Initiated"
540+
cy.realPress("ArrowDown");
541+
checkAnnouncement(`Order 67890 . Row . ${HIGHLIGHT_CRITICAL} . Return Initiated . 3 of 5`);
542+
543+
// Row 3: Positive, no text
544+
cy.realPress("ArrowDown");
545+
checkAnnouncement(`Order 11111 . Row . ${HIGHLIGHT_POSITIVE} . 4 of 5`);
546+
547+
// Row 4: Information + "Unread"
548+
cy.realPress("ArrowDown");
549+
checkAnnouncement(`Order 22222 . Row . ${HIGHLIGHT_INFORMATION} . Unread . 5 of 5`);
550+
551+
// Change highlight to None — no highlight announcement
552+
cy.get("#row4").invoke("prop", "highlight", "None");
553+
checkAnnouncement("Order 22222 . Row . 5 of 5", true);
554+
});
555+
});
556+
557+
describe("Entering-the-Table Announcement", () => {
558+
function checkAnnouncement(expectedText: string, check = "contains") {
559+
cy.get("body").then($body => {
560+
expect($body.find("#ui5-invisible-text").text())[check](expectedText);
561+
});
562+
}
563+
564+
it("should announce table info on first focus only", () => {
565+
cy.mount(
566+
<Table id="table0">
567+
<TableSelectionMulti slot="features" selected="Row1"></TableSelectionMulti>
568+
<TableHeaderRow slot="headerRow">
569+
<TableHeaderCell identifier>Name</TableHeaderCell>
570+
</TableHeaderRow>
571+
<TableRow rowKey="Row1">
572+
<TableCell>Alice</TableCell>
573+
</TableRow>
574+
<TableRow rowKey="Row2">
575+
<TableCell>Bob</TableCell>
576+
</TableRow>
577+
</Table>
578+
);
579+
580+
// Add an external button to allow focusing outside the table
581+
cy.document().then(doc => {
582+
const btn = doc.createElement("button");
583+
btn.id = "outside-btn";
584+
btn.textContent = "Outside";
585+
doc.body.appendChild(btn);
586+
});
587+
588+
// First focus — entering announcement is part of the custom announcement
589+
cy.get("[ui5-table-row]").first().realClick();
590+
cy.focused().should("have.attr", "ui5-table-row");
591+
checkAnnouncement(`with 2 rows`);
592+
checkAnnouncement(`${MULTI_SELECTABLE}`);
593+
checkAnnouncement(`1 rows selected`);
594+
checkAnnouncement(`Name Alice . Row . 2 of 3`);
595+
596+
// Navigate to next row — no entering announcement, just row content
597+
cy.realPress("ArrowDown");
598+
checkAnnouncement("Name Bob . Row . 3 of 3", "equal");
599+
600+
// Focus outside the table and back — entering announcement appears again
601+
cy.get("#outside-btn").realClick();
602+
cy.get("[ui5-table-row]").first().realClick();
603+
cy.focused().should("have.attr", "ui5-table-row");
604+
checkAnnouncement(`with 2 rows`);
605+
checkAnnouncement(`${MULTI_SELECTABLE}`);
606+
});
607+
});
608+
609+
describe("Row Actions Announcement Format", () => {
610+
function checkAnnouncement(expectedText: string, focusAgain = false, check = "contains") {
611+
if (focusAgain) {
612+
cy.realPress("ArrowUp");
613+
cy.realPress("ArrowDown");
614+
}
615+
616+
cy.get("body").then($body => {
617+
expect($body.find("#ui5-invisible-text").text())[check](expectedText);
618+
});
619+
}
620+
621+
it("should announce action text in row announcement", () => {
622+
cy.mount(
623+
<Table id="table0" rowActionCount={3}>
624+
<TableHeaderRow slot="headerRow">
625+
<TableHeaderCell identifier>Name</TableHeaderCell>
626+
</TableHeaderRow>
627+
<TableRow id="row1" rowKey="Row1">
628+
<TableCell>Alice</TableCell>
629+
<TableRowActionNavigation slot="actions" interactive></TableRowActionNavigation>
630+
<TableRowAction slot="actions" icon={add} text="Add"></TableRowAction>
631+
<TableRowAction slot="actions" icon={edit} text="Edit"></TableRowAction>
632+
</TableRow>
633+
</Table>
634+
);
635+
636+
// Multiple actions
637+
cy.get("#row1").realClick();
638+
checkAnnouncement(`${ACTIONS_LIST_TEMPLATE.replace("{0}", "Add, Edit, Navigation")}`);
639+
640+
// Remove an action — format changes
641+
cy.get("#row1").find("[ui5-table-row-action]").last().invoke("remove");
642+
checkAnnouncement(`${ACTIONS_LIST_TEMPLATE.replace("{0}", "Add, Navigation")}`, true);
643+
644+
// Remove another — single action
645+
cy.get("#row1").find("[ui5-table-row-action]").first().invoke("remove");
646+
checkAnnouncement(`${ACTION_TEMPLATE.replace("{0}", "Navigation")}`, true);
647+
});
648+
});

packages/main/src/Table.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,9 @@ class Table extends UI5Element {
401401
@property({ type: Boolean, noAttribute: true })
402402
_renderNavigated = false;
403403

404+
@property({ type: Boolean, noAttribute: true })
405+
_renderHighlight = false;
406+
404407
@query("[ui5-drop-indicator]")
405408
dropIndicatorDOM!: DropIndicator;
406409

@@ -456,8 +459,10 @@ class Table extends UI5Element {
456459

457460
onBeforeRendering(): void {
458461
this._renderNavigated = this.rows.some(row => row.navigated);
462+
this._renderHighlight = this.rows.some(row => row.highlight !== "None");
459463
[...this.headerRow, ...this.rows].forEach((row, index) => {
460464
row._renderNavigated = this._renderNavigated;
465+
row._hasHighlight = this._renderHighlight;
461466
row._rowActionCount = this.rowActionCount;
462467
row._alternate = this.alternateRowColors && index % 2 === 0;
463468
});
@@ -634,6 +639,11 @@ class Table extends UI5Element {
634639
const widths = [];
635640
const visibleHeaderCells = this.headerRow[0]._visibleCells;
636641

642+
// Highlight Cell Width
643+
if (this._renderHighlight) {
644+
widths.push("var(--_ui5_table_highlight_width)");
645+
}
646+
637647
// Selection Cell Width
638648
if (this._isRowSelectorRequired) {
639649
widths.push("min-content");

packages/main/src/TableCell.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ class TableCell extends TableCellBase {
6060
this.style.textAlign = `var(--halign-${this._headerCell._id})`;
6161
this.style.justifyContent = `var(--halign-${this._headerCell._id})`;
6262
}
63+
64+
if (this._headerCell) {
65+
const newRole = this._headerCell.identifier ? "rowheader" : this.ariaRole;
66+
if (this.getAttribute("role") !== newRole) {
67+
this.setAttribute("role", newRole);
68+
}
69+
}
6370
}
6471

6572
_injectHeaderNodes(ref: HTMLElement | null) {

0 commit comments

Comments
 (0)