Skip to content

Commit e6d1cd3

Browse files
committed
Merge branch 'main' into mi-arrow-left
2 parents 79ab941 + 442e893 commit e6d1cd3

15 files changed

Lines changed: 512 additions & 11 deletions

File tree

.github/workflows/release.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,6 @@ jobs:
364364
- name: Install Dependencies
365365
run: yarn --immutable
366366

367-
- name: Build
368-
run: yarn ci:releasebuild
369-
370367
- name: Version Bump
371368
env:
372369
GH_TOKEN: ${{ secrets.UI5_WEBCOMP_BOT_GH_TOKEN }}
@@ -380,6 +377,9 @@ jobs:
380377
--exact \
381378
--create-release github
382379
380+
- name: Build
381+
run: yarn ci:releasebuild
382+
383383
- name: Publish
384384
run: |
385385
yarn lerna publish from-git --yes --dist-tag "v1"

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2796,4 +2796,95 @@ describe("List sticky header", () => {
27962796
});
27972797
});
27982798
});
2799+
});
2800+
2801+
describe("List - ListItem accessible role inheritance", () => {
2802+
it("list items inherit 'menuitem' role when ui5-list has accessible-role='Menu'", () => {
2803+
cy.mount(
2804+
<List accessibleRole="Menu">
2805+
<ListItemStandard id="item1">Item 1</ListItemStandard>
2806+
<ListItemStandard id="item2">Item 2</ListItemStandard>
2807+
</List>
2808+
);
2809+
2810+
cy.get("#item1")
2811+
.shadow()
2812+
.find("li")
2813+
.should("have.attr", "role", "menuitem");
2814+
2815+
cy.get("#item2")
2816+
.shadow()
2817+
.find("li")
2818+
.should("have.attr", "role", "menuitem");
2819+
});
2820+
2821+
it("list items inherit 'option' role when ui5-list has accessible-role='ListBox'", () => {
2822+
cy.mount(
2823+
<List accessibleRole="ListBox">
2824+
<ListItemStandard id="item1">Item 1</ListItemStandard>
2825+
<ListItemStandard id="item2">Item 2</ListItemStandard>
2826+
</List>
2827+
);
2828+
2829+
cy.get("#item1")
2830+
.shadow()
2831+
.find("li")
2832+
.should("have.attr", "role", "option");
2833+
2834+
cy.get("#item2")
2835+
.shadow()
2836+
.find("li")
2837+
.should("have.attr", "role", "option");
2838+
});
2839+
2840+
it("list items keep 'listitem' role when ui5-list has default accessible-role='List'", () => {
2841+
cy.mount(
2842+
<List>
2843+
<ListItemStandard id="item1">Item 1</ListItemStandard>
2844+
</List>
2845+
);
2846+
2847+
cy.get("#item1")
2848+
.shadow()
2849+
.find("li")
2850+
.should("have.attr", "role", "listitem");
2851+
});
2852+
2853+
it("explicit accessible-role on ui5-li takes precedence over inherited role from ui5-list", () => {
2854+
cy.mount(
2855+
<List accessibleRole="Menu">
2856+
<ListItemStandard id="explicit" accessibleRole="TreeItem">Item 1</ListItemStandard>
2857+
<ListItemStandard id="inherited">Item 2</ListItemStandard>
2858+
</List>
2859+
);
2860+
2861+
cy.get("#explicit")
2862+
.shadow()
2863+
.find("li")
2864+
.should("have.attr", "role", "treeitem");
2865+
2866+
cy.get("#inherited")
2867+
.shadow()
2868+
.find("li")
2869+
.should("have.attr", "role", "menuitem");
2870+
});
2871+
2872+
it("list items can have an explicit accessible-role set without a parent ui5-list role", () => {
2873+
cy.mount(
2874+
<List>
2875+
<ListItemStandard id="item1" accessibleRole="MenuItem">Item 1</ListItemStandard>
2876+
<ListItemStandard id="item2">Item 2</ListItemStandard>
2877+
</List>
2878+
);
2879+
2880+
cy.get("#item1")
2881+
.shadow()
2882+
.find("li")
2883+
.should("have.attr", "role", "menuitem");
2884+
2885+
cy.get("#item2")
2886+
.shadow()
2887+
.find("li")
2888+
.should("have.attr", "role", "listitem");
2889+
});
27992890
});

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3430,6 +3430,54 @@ describe("Keyboard Handling", () => {
34303430
.should("have.value", "I");
34313431
});
34323432

3433+
it("should deselect all tokens on [Escape] key when focus is on tokenizer", () => {
3434+
cy.mount(
3435+
<MultiComboBox>
3436+
<MultiComboBoxItem selected={true} text="Andora"></MultiComboBoxItem>
3437+
<MultiComboBoxItem selected={true} text="Bulgaria"></MultiComboBoxItem>
3438+
<MultiComboBoxItem selected={true} text="Canada"></MultiComboBoxItem>
3439+
</MultiComboBox>
3440+
);
3441+
3442+
cy.get("[ui5-multi-combobox]")
3443+
.shadow()
3444+
.find("[ui5-tokenizer]")
3445+
.find("[ui5-token]")
3446+
.should("have.length", 3);
3447+
3448+
cy.get("[ui5-multi-combobox]")
3449+
.realClick();
3450+
3451+
cy.realPress("Backspace");
3452+
3453+
cy.get("[ui5-multi-combobox]")
3454+
.shadow()
3455+
.find("[ui5-tokenizer]")
3456+
.find("[ui5-token]")
3457+
.last()
3458+
.should("be.focused");
3459+
3460+
cy.realPress(["Shift", "Home"]);
3461+
3462+
cy.get("[ui5-multi-combobox]")
3463+
.shadow()
3464+
.find("[ui5-tokenizer]")
3465+
.find("[ui5-token]")
3466+
.each($token => {
3467+
cy.wrap($token).should("have.attr", "selected");
3468+
});
3469+
3470+
cy.realPress("Escape");
3471+
3472+
cy.get("[ui5-multi-combobox]")
3473+
.shadow()
3474+
.find("[ui5-tokenizer]")
3475+
.find("[ui5-token]")
3476+
.each($token => {
3477+
cy.wrap($token).should("not.have.attr", "selected");
3478+
});
3479+
});
3480+
34333481
it("Selects an item when enter is pressed and value matches a text of an item in the list", () => {
34343482
cy.mount(
34353483
<>

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,6 +1474,39 @@ describe("Keyboard handling", () => {
14741474
.should("have.been.calledOnce");
14751475
});
14761476

1477+
it("should deselect all tokens on [Escape] key", () => {
1478+
cy.mount(
1479+
<MultiInput>
1480+
<Token slot="tokens" text="Andora"></Token>
1481+
<Token slot="tokens" text="Bulgaria"></Token>
1482+
<Token slot="tokens" text="Canada"></Token>
1483+
</MultiInput>
1484+
);
1485+
1486+
cy.get("[ui5-multi-input]")
1487+
.shadow()
1488+
.find("input")
1489+
.realClick();
1490+
1491+
cy.realPress("Home");
1492+
1493+
cy.get("[ui5-token]")
1494+
.eq(0)
1495+
.should("be.focused");
1496+
1497+
cy.realPress(["Shift", "End"]);
1498+
1499+
cy.get("[ui5-token]").each($token => {
1500+
cy.wrap($token).should("have.attr", "selected");
1501+
});
1502+
1503+
cy.realPress("Escape");
1504+
1505+
cy.get("[ui5-token]").each($token => {
1506+
cy.wrap($token).should("not.have.attr", "selected");
1507+
});
1508+
});
1509+
14771510
it("should focus last token on ArrowLeft at start of input, keep suggestions open, and not fire change event", () => {
14781511
const changeSpy = cy.stub().as("changeSpy");
14791512

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

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,6 +1590,104 @@ describe("Keyboard Handling", () => {
15901590
.eq(1)
15911591
.should("have.attr", "selected");
15921592
});
1593+
1594+
it("should deselect all tokens on [Escape] key", () => {
1595+
cy.get("[ui5-token]")
1596+
.eq(0)
1597+
.as("firstToken");
1598+
1599+
cy.get("[ui5-token]")
1600+
.eq(1)
1601+
.as("secondToken");
1602+
1603+
cy.get("[ui5-token]")
1604+
.eq(2)
1605+
.as("thirdToken");
1606+
1607+
cy.get("@firstToken")
1608+
.realClick();
1609+
1610+
cy.realPress(["Shift", "End"]);
1611+
1612+
cy.get("@firstToken")
1613+
.should("have.attr", "selected");
1614+
1615+
cy.get("@secondToken")
1616+
.should("have.attr", "selected");
1617+
1618+
cy.get("@thirdToken")
1619+
.should("have.attr", "selected");
1620+
1621+
cy.realPress("Escape");
1622+
1623+
cy.get("@firstToken")
1624+
.should("not.have.attr", "selected");
1625+
1626+
cy.get("@secondToken")
1627+
.should("not.have.attr", "selected");
1628+
1629+
cy.get("@thirdToken")
1630+
.should("not.have.attr", "selected");
1631+
});
1632+
1633+
it("should fire selection-change event on [Escape] when tokens are selected", () => {
1634+
cy.mount(
1635+
<Tokenizer onSelectionChange={cy.stub().as("selectionChange")}>
1636+
<Token text="Andora"></Token>
1637+
<Token text="Bulgaria"></Token>
1638+
<Token text="Canada"></Token>
1639+
</Tokenizer>
1640+
);
1641+
1642+
cy.get("[ui5-token]")
1643+
.eq(0)
1644+
.realClick();
1645+
1646+
cy.get("@selectionChange")
1647+
.should("have.been.called");
1648+
1649+
cy.get("@selectionChange")
1650+
.invoke("resetHistory");
1651+
1652+
cy.realPress("Escape");
1653+
1654+
cy.get("@selectionChange")
1655+
.should("have.been.calledOnce");
1656+
1657+
cy.get("@selectionChange")
1658+
.its("firstCall.args.0.detail.tokens")
1659+
.should("have.length", 0);
1660+
});
1661+
1662+
it("should not fire selection-change on [Escape] when no tokens are selected", () => {
1663+
cy.mount(
1664+
<Tokenizer onSelectionChange={cy.stub().as("selectionChange")}>
1665+
<Token text="Andora"></Token>
1666+
<Token text="Bulgaria"></Token>
1667+
</Tokenizer>
1668+
);
1669+
1670+
cy.get("[ui5-token]")
1671+
.eq(0)
1672+
.realClick();
1673+
1674+
cy.get("@selectionChange")
1675+
.invoke("resetHistory");
1676+
1677+
cy.realPress("Space");
1678+
1679+
cy.get("[ui5-token]")
1680+
.eq(0)
1681+
.should("not.have.attr", "selected");
1682+
1683+
cy.get("@selectionChange")
1684+
.invoke("resetHistory");
1685+
1686+
cy.realPress("Escape");
1687+
1688+
cy.get("@selectionChange")
1689+
.should("not.have.been.called");
1690+
});
15931691
});
15941692

15951693
describe("Clipboard Operations", () => {

packages/main/src/List.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ const INFINITE_SCROLL_DEBOUNCE_RATE = 250; // ms
8080

8181
const PAGE_UP_DOWN_SIZE = 10;
8282

83+
// Maps the List's accessible-role to the expected child item ARIA role (lowercase)
84+
const LIST_ACCESSIBLE_ROLE_TO_ITEM_ROLE: Partial<Record<`${ListAccessibleRole}`, string>> = {
85+
Menu: "menuitem",
86+
Tree: "treeitem",
87+
ListBox: "option",
88+
};
89+
8390
// ListItemBase-based events
8491
type ListItemFocusEventDetail = {
8592
item: ListItemBase,
@@ -843,6 +850,7 @@ class List extends UI5Element {
843850

844851
prepareListItems() {
845852
const slottedItems = this.getItemsForProcessing();
853+
const inheritedItemRole = LIST_ACCESSIBLE_ROLE_TO_ITEM_ROLE[this.accessibleRole];
846854

847855
slottedItems.forEach((item, key) => {
848856
const isLastChild = key === slottedItems.length - 1;
@@ -851,6 +859,7 @@ class List extends UI5Element {
851859

852860
if (item.hasConfigurableMode) {
853861
(item as ListItem)._selectionMode = this.selectionMode;
862+
(item as ListItem)._inheritedAccessibleRole = inheritedItemRole;
854863
}
855864
item.hasBorder = showBottomBorder;
856865

packages/main/src/ListItem.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,17 +180,23 @@ abstract class ListItem extends ListItemBase {
180180

181181
/**
182182
* Used to define the role of the list item.
183-
* @private
184-
* @default "ListItem"
185-
* @since 1.3.0
186183
*
184+
* **Note:** If not set, the role is automatically inherited from the parent `ui5-list` based on its `accessible-role` property
185+
* (e.g. `Menu` -> `MenuItem`, `Tree` -> `TreeItem`, `ListBox` -> `Option`).
186+
* An explicitly set `accessible-role` on the list item takes precedence over the inherited role.
187+
* @default undefined
188+
* @public
189+
* @since 1.3.0
187190
*/
188191
@property()
189-
accessibleRole: `${ListItemAccessibleRole}` = "ListItem";
192+
accessibleRole?: `${Exclude<ListItemAccessibleRole, ListItemAccessibleRole.Group>}`;
190193

191194
@property()
192195
_forcedAccessibleRole?: string;
193196

197+
@property({ noAttribute: true })
198+
_inheritedAccessibleRole?: string;
199+
194200
@property()
195201
_selectionMode: `${ListSelectionMode}` = "None";
196202

@@ -436,7 +442,13 @@ abstract class ListItem extends ListItemBase {
436442
}
437443

438444
get listItemAccessibleRole() {
439-
return (this._forcedAccessibleRole || this.accessibleRole.toLowerCase()) as AriaRole | undefined;
445+
if (this._forcedAccessibleRole) {
446+
return this._forcedAccessibleRole as AriaRole;
447+
}
448+
if (this.accessibleRole) {
449+
return this.accessibleRole.toLowerCase() as AriaRole;
450+
}
451+
return (this._inheritedAccessibleRole || "listitem") as AriaRole;
440452
}
441453

442454
get ariaSelectedText() {

0 commit comments

Comments
 (0)