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
6 changes: 3 additions & 3 deletions packages/base/src/UI5Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ type KebabToCamel<T extends string> = T extends `${infer H}-${infer J}${infer K}
: T;
type KebabToPascal<T extends string> = Capitalize<KebabToCamel<T>>;

type GlobalHTMLAttributeNames = "accesskey" | "autocapitalize" | "autofocus" | "autocomplete" | "contenteditable" | "contextmenu" | "class" | "dir" | "draggable" | "enterkeyhint" | "hidden" | "id" | "inputmode" | "lang" | "nonce" | "part" | "exportparts" | "pattern" | "slot" | "spellcheck" | "style" | "tabIndex" | "tabindex" | "title" | "translate" | "ref" | "inert";
type GlobalHTMLAttributeNames = "accesskey" | "autocapitalize" | "autofocus" | "autocomplete" | "contenteditable" | "contextmenu" | "class" | "dir" | "draggable" | "enterkeyhint" | "hidden" | "id" | "inputmode" | "lang" | "nonce" | "part" | "exportparts" | "pattern" | "slot" | "spellcheck" | "style" | "tabIndex" | "tabindex" | "title" | "translate" | "ref" | "inert" | "role";
type ElementProps<I> = Partial<Omit<I, keyof HTMLElement>>;
type TargetedCustomEvent<D, T> = Omit<CustomEvent<D>, "currentTarget"> & { currentTarget: T };
// define as method and extract the function signature from the method to make it bivariant so that inheritance of event handlers is not checked via strictFunctionTypes
Expand Down Expand Up @@ -1115,8 +1115,8 @@ abstract class UI5Element extends HTMLElement {
* Returns the component accessibility info.
* @private
*/
get accessibilityInfo(): AccessibilityInfo {
return {};
get accessibilityInfo(): AccessibilityInfo | undefined {
return undefined;
}

/**
Expand Down
17 changes: 8 additions & 9 deletions packages/base/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,24 @@ export type AccessibilityInfo = {
// The WAI-ARIA role of the component.
role?: AriaRole,

// A translated text that represents the component type. Used when several components share same role,
// f.e. Select and ComboBox both have role="combobox".
type?: LowercaseString<string>,
// A translated text that represents the component type.
type?: string,

// A translated text that represents relevant component description/state - value, placeholder, label, etc.
description?: string,

// The component disabled state.
// Disabled state of the component.
disabled?: boolean,

// The component readonly state.
// Readonly state of the component.
readonly?: boolean,

// The component required state.
// Required state of the component.
required?: boolean,

// An array of elements, aggregated by the component
// <b>Note:</b> Children should only be provided when it is helpful to understand the accessibility context.
children?: Array<HTMLElement>,
// An array of nodes, aggregated by the component
// **Note:** Children should only be provided when it is helpful to understand the accessibility context.
children?: Array<Node>,
}

export type AccessibilityAttributes = {
Expand Down
4 changes: 2 additions & 2 deletions packages/base/src/util/TabbableElements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import isElementTabbable from "./isElementTabbable.js";
* @returns { Array<HTMLElement> } the tabbable elements
*/
const getTabbableElements = (el: HTMLElement): Array<HTMLElement> => {
return getTabbables([...el.children]);
return getTabbables(el.tagName === "SLOT" ? [el] : [...el.children]);
};

/**
Expand Down Expand Up @@ -45,7 +45,7 @@ const getTabbables = (nodes: Array<Node>, tabbables?: Array<HTMLElement>): Array
}

if (currentElement.tagName === "SLOT") {
getTabbables((currentElement as HTMLSlotElement).assignedNodes() as Array<HTMLElement>, tabbableElements);
getTabbables((currentElement as HTMLSlotElement).assignedElements(), tabbableElements);
} else {
const children = currentElement.shadowRoot ? currentElement.shadowRoot.children : currentElement.children;
getTabbables([...children], tabbableElements);
Expand Down
323 changes: 323 additions & 0 deletions packages/main/cypress/specs/TableCustomAnnouncement.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
import Table from "../../src/Table.js";
import TableHeaderRow from "../../src/TableHeaderRow.js";
import TableHeaderCell from "../../src/TableHeaderCell.js";
import TableRow from "../../src/TableRow.js";
import TableCell from "../../src/TableCell.js";
import TableSelectionMulti from "../../src/TableSelectionMulti.js";
import TableRowAction from "../../src/TableRowAction.js";
import TableRowActionNavigation from "../../src/TableRowActionNavigation.js";
import TableHeaderCellActionAI from "../../src/TableHeaderCellActionAI.js";
import Label from "../../src/Label.js";
import Button from "../../src/Button.js";
import add from "@ui5/webcomponents-icons/dist/add.js";
import edit from "@ui5/webcomponents-icons/dist/edit.js";
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 },
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 },
TABLE_ROW_SELECTED: { defaultText: SELECTED },
TABLE_ROW_NAVIGABLE: { defaultText: NAVIGABLE },
TABLE_ROW_ACTIVE: { defaultText: ACTIVE },
} = Translations;

describe("Cell Custom Announcement - More details", () => {
beforeEach(() => {
cy.mount(
<Table id="table0" rowActionCount={3}>
<TableSelectionMulti slot="features"></TableSelectionMulti>
<TableHeaderRow slot="headerRow">
<TableHeaderCell sort-indicator="Ascending">
<Label required>Header1</Label>
<TableHeaderCellActionAI slot="action"></TableHeaderCellActionAI>
</TableHeaderCell>
<TableHeaderCell data-ui5-table-acc-text="Header2"><input/></TableHeaderCell>
<TableHeaderCell><div>Header3</div></TableHeaderCell>
<TableHeaderCell></TableHeaderCell>
</TableHeaderRow>
<TableRow navigated>
<TableCell>
Row1Cell1
<div dangerouslySetInnerHTML={{ __html: "<!-- Let's make sure that comments are ignored in the announcement -->" }} />
</TableCell>
<TableCell><input id="row1-input1" value="Row1Input1"/><input id="row1-input2" value="Row1Input2" hidden/></TableCell>
<TableCell><div id="row1-div"><b></b></div></TableCell>
<TableCell><Button id="row1-button">Row1Cell3</Button></TableCell>
<TableRowActionNavigation slot="actions" id="row1-nav-action"></TableRowActionNavigation>
<TableRowAction slot="actions" id="row1-add-action" icon={add} text="Add"></TableRowAction>
<TableRowAction slot="actions" id="row1-edit-action" icon={edit} text="Edit"></TableRowAction>
</TableRow>
</Table>
);

cy.get("#table0").children("ui5-table-row").as("rows");
cy.get("#table0").children("ui5-table-header-row").first().as("headerRow");
cy.get("#table0").shadow().find("#table").as("innerTable");
cy.get("@rows").first().as("row1");
cy.get("@row1").find("#row1-button").as("row1Button");
cy.get("@row1").find("#row1-input1").as("row1Input1");
cy.get("@row1").find("#row1-input2").as("row1Input2");
cy.get("@row1").find("#row1-div").as("row1Div");
});

function checkAnnouncement(expectedText: string, focusAgain = false) {
if (focusAgain) {
cy.realPress("ArrowUp");
cy.realPress("ArrowDown");
}

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

it("should announce table cells", () => {
cy.get("@row1").realClick(); // row focused
cy.realPress("ArrowRight"); // selection cell focused
checkAnnouncement("");

cy.realPress("ArrowRight"); // first cell focused
checkAnnouncement("Row1Cell1");

cy.realPress("ArrowRight"); // second cell focused
checkAnnouncement(CONTAINS_CONTROL);

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");
checkAnnouncement(`Input with custom accessibility text . ${CONTAINS_CONTROLS}`, true);

cy.realPress("ArrowRight"); // third cell focused
checkAnnouncement(EMPTY);

cy.get("@row1Div").invoke("attr", "tabindex", "0");
cy.get("@row1Div").invoke("css", "width", "150px");
cy.get("@row1Div").focus();
cy.realPress("F2");
checkAnnouncement(CONTAINS_CONTROL, true);

cy.realPress("ArrowRight"); // fourth cell focused
checkAnnouncement(`Row1Cell3 . ${CONTAINS_CONTROL}`);

cy.document().then((doc) => {
const row1Button = doc.getElementById("row1-button") as Button;
cy.stub(row1Button, "accessibilityInfo").get(() => ({
type: "Button",
description: "Row1Cell3Button",
required: true,
disabled: true,
readonly: true,
}));
});
checkAnnouncement(`Button Row1Cell3Button ${REQUIRED} ${DISABLED} ${READONLY} . ${CONTAINS_CONTROL}`, true);

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

cy.realPress("ArrowRight"); // ROW_ACTIONS cell
checkAnnouncement(Table.i18nBundle.getText(MULTIPLE_ACTIONS, 2));

cy.get("#row1-edit-action").invoke("remove");
checkAnnouncement(ONE_ROW_ACTION, true);

cy.get("#row1-add-action").invoke("remove");
checkAnnouncement(EMPTY, true);

cy.realPress("Home"); // selection cell focused
checkAnnouncement("");
});

it("should announce table header cells", () => {
cy.get("@headerRow").realClick(); // header row focused
cy.realPress("ArrowRight"); // selection cell focused
checkAnnouncement("");

cy.realPress("ArrowRight"); // first cell focused
checkAnnouncement(`Header1 ${GENERATED_BY_AI} . ${CONTAINS_CONTROL}`);

cy.realPress("ArrowRight"); // second cell focused
checkAnnouncement("Header2");

cy.realPress("ArrowRight"); // third cell focused
checkAnnouncement("Header3");

cy.realPress("ArrowRight"); // forth cell focused
checkAnnouncement(EMPTY);

cy.realPress("ArrowRight"); // forth cell focused
checkAnnouncement(ROW_ACTIONS);

cy.realPress("Home"); // selection cell focused
checkAnnouncement("");
});
});

describe("Row Custom Announcement - Less details", () => {
beforeEach(() => {
cy.mount(
<Table id="table0" overflowMode="Popin" rowActionCount={2} >
<TableSelectionMulti id="selection" slot="features" selected="Row1"></TableSelectionMulti>
<TableHeaderRow slot="headerRow">
<TableHeaderCell minWidth="300px" sort-indicator="Ascending">
<Label id="Header1Label" required>H1</Label>
<div style={{ display: "none" }}>H1DisplayNone</div>
</TableHeaderCell>
<TableHeaderCell minWidth="200px">
<div data-ui5-table-acc-text="H2">H2 Custom Text</div>
</TableHeaderCell>
<TableHeaderCell id="Header3" minWidth="200px">
<div>H3<div aria-hidden="true">H3AriaHidden</div></div>
</TableHeaderCell>
<TableHeaderCell minWidth="150px" sort-indicator="Descending" popinText="H4Popin">
H4
<TableHeaderCellActionAI slot="action"></TableHeaderCellActionAI>
</TableHeaderCell>
</TableHeaderRow>
<TableRow rowKey="Row1" navigated interactive>
<TableCell>R1C1</TableCell>
<TableCell>
<input value="R1C2" />
<input value="R1C2" />
</TableCell>
<TableCell>
<div>
<u data-sap-ui5-table-acc-text="R1C3"></u>
<b aria-hidden="true">R1C3AriaHidden</b>
<i style={{ display: "none" }}>R1C3DisplayNone</i>
</div>
</TableCell>
<TableCell>
C4
<Button id="row1-button">C4Button</Button>
</TableCell>
<TableRowActionNavigation slot="actions" id="row1-nav-action"></TableRowActionNavigation>
<TableRowAction slot="actions" id="row1-add-action" icon={add} text="Add"></TableRowAction>
</TableRow>
</Table>
);

cy.get("#table0").children("ui5-table-row").as("rows");
cy.get("#table0").children("ui5-table-header-row").first().as("headerRow");
cy.get("#table0").shadow().find("#table").as("innerTable");
cy.get("@rows").first().as("row1");
cy.get("@row1").find("#row1-button").as("row1Button");

cy.document().then((doc) => {
const header1Label = doc.getElementById("Header1Label") as Label;
cy.stub(header1Label, "accessibilityInfo").get(() => ({
description: "H1",
required: true,
}));

const row1Button = doc.getElementById("row1-button") as Button;
cy.stub(row1Button, "accessibilityInfo").get(() => ({
type: "Button",
description: "C4Button",
required: true,
disabled: true,
readonly: true,
}));
});
});

function checkAnnouncement(expectedText: string, focusAgain = false, check = "contains") {
focusAgain && cy.focused().then($el => {
if ($el.attr("ui5-table-header-row") !== undefined) {
cy.realPress("ArrowDown");
cy.realPress("ArrowUp");
} else {
cy.realPress("ArrowUp");
cy.realPress("ArrowDown");
}
});

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

it("should announce table rows", () => {
cy.get("@row1").realClick();
checkAnnouncement(`Row . 2 of 2 . ${SELECTED} . ${NAVIGABLE} . H1`);
checkAnnouncement(`H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H3 . ${EMPTY} . H4 . C4 Button C4Button`);
checkAnnouncement(ONE_ROW_ACTION);

cy.get("#selection").invoke("attr", "selected", "");
checkAnnouncement(`Row . 2 of 2 . ${NAVIGABLE}`, true);

cy.get("#row1-nav-action").invoke("prop", "interactive", true);
checkAnnouncement(`Row . 2 of 2 . ${ACTIVE} . H1`, true);
checkAnnouncement(Table.i18nBundle.getText(MULTIPLE_ACTIONS, 2));

cy.get("@row1").invoke("prop", "interactive", false);
checkAnnouncement(`Row . 2 of 2 . H1`, true);

cy.get("#table0").invoke("css", "width", "301px");
checkAnnouncement(`Row . 2 of 2 . H1`, true);
checkAnnouncement(`H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H3 . ${EMPTY} . H4Popin . C4 Button C4Button`);

cy.get("#Header3").invoke("prop", "popinHidden", true);
checkAnnouncement(`H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H4Popin . C4 Button C4Button`, true);

cy.get("#row1-nav-action").invoke("remove");
cy.get("#row1-add-action").invoke("remove");
checkAnnouncement(`Row . 2 of 2 . H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H4Popin . C4 Button C4Button`, true, "equal");

cy.realPress("ArrowRight"); // selection cell focused
checkAnnouncement("");

cy.realPress("End"); // popin cell focused we need details
checkAnnouncement(`H2 . ${CONTAINS_CONTROLS} . H4Popin ${GENERATED_BY_AI} . ${CONTAINS_CONTROL} . C4 Button C4Button ${REQUIRED} ${DISABLED} ${READONLY} . ${CONTAINS_CONTROL}`);

cy.realPress("Home"); // selection cell focused
cy.realPress("Home"); // row focused
cy.get("#table0").invoke("css", "width", "1000px");
checkAnnouncement(`Row . 2 of 2 . H1 . R1C1 . H2 . ${CONTAINS_CONTROLS} . H3 . ${EMPTY} . H4 . C4 Button C4Button`, true, "equal");

cy.realPress("ArrowUp"); // header row focused
cy.get("@row1").invoke("remove");
cy.focused().click();
cy.realPress("ArrowDown"); // nodata row focused
checkAnnouncement("");
});

it("should announce table header row", () => {
cy.get("@row1").realClick();
cy.realPress("ArrowUp");
checkAnnouncement(`${COLUMN_HEADER_ROW} . ${SELECT_ALL_CHECKBOX} ${CHECKED} . H1 . H2 . H3 . H4 . ${ROW_ACTIONS}`);

cy.get("#table0").invoke("attr", "row-action-count", "0");
checkAnnouncement(`${COLUMN_HEADER_ROW} . ${SELECT_ALL_CHECKBOX} ${CHECKED} . H1 . H2 . H3 . H4`, true, "equal");

cy.get("#selection").invoke("prop", "selected", "");
checkAnnouncement(`${COLUMN_HEADER_ROW} . ${SELECT_ALL_CHECKBOX} ${NOT_CHECKED} . H1 . H2 . H3 . H4`, true, "equal");

cy.get("#selection").invoke("attr", "header-selector", "ClearAll");
checkAnnouncement(`${COLUMN_HEADER_ROW} . ${CLEAR_ALL_BUTTON} ${DISABLED} . H1 . H2 . H3 . H4`, true, "equal");

cy.get("#selection").invoke("prop", "selected", "Row1");
checkAnnouncement(`${COLUMN_HEADER_ROW} . ${CLEAR_ALL_BUTTON} . H1 . H2 . H3 . H4`, true, "equal");

cy.get("#selection").invoke("remove");
checkAnnouncement(`${COLUMN_HEADER_ROW} . H1 . H2 . H3 . H4`, true, "equal");

cy.get("#table0").invoke("append", '<ui5-table-selection-single slot="features"></ui5-table-selection-single>');
checkAnnouncement(`${COLUMN_HEADER_ROW} . ${SELECTION} . H1 . H2 . H3 . H4`, true, "equal");
});
});
Loading
Loading