Skip to content

Commit 5a4402b

Browse files
feat(ui5-user-menu-item) - add optional second line (#13397)
* wip: user-menu-item feature in progress * chore(user-menu-item): enhance accessibility * chore(ui5-user-menu-item): reverts changes on MenuItem and MenuItemGroup * chore(ui5-user-menu-item): improve hook handling * chore(UserMenuItem): make submenu selection not removable * fix(ui5-user-menu-item): fix lint error, add selection line CSS and tests - Fix ESLint func-names/space-before-function-paren in MenuItemTemplate - Update UserMenuItem CSS: 3.25rem height for two-line selection display, 0.5rem padding, 0.25rem gap, proper font styling with SAP theme vars - Fix CSS selector from [show-selection-text] to [show-selection] - Add 17 Cypress tests for showSelection, single-select behavior, UserMenuItemGroup, CSS styling, and nested submenu items * fix(ui5-user-menu-item): scope uncheck prevention to showSelection only Single-select unchecking is now only blocked when the parent item has showSelection set, so regular single-select groups remain uncheckable as expected. * fix(ui5-menu-item): replace any type with JsxTemplate in MenuItemHooks * chore: retrigger CI
1 parent c6f2ddc commit 5a4402b

7 files changed

Lines changed: 479 additions & 20 deletions

File tree

packages/fiori/cypress/specs/UserMenu.cy.tsx

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,3 +1105,352 @@ describe("Footer configuration", () => {
11051105
cy.get("@signOutClicked").should("have.been.calledOnce");
11061106
});
11071107
});
1108+
1109+
describe("UserMenuItem", () => {
1110+
describe("showSelection property", () => {
1111+
it("renders two-line layout when showSelection is true and sub-item is checked", () => {
1112+
cy.mount(
1113+
<>
1114+
<Button id="openUserMenuBtn">Open User Menu</Button>
1115+
<UserMenu open={true} opener="openUserMenuBtn">
1116+
<UserMenuItem text="Theme" showSelection={true}>
1117+
<UserMenuItemGroup checkMode="Single">
1118+
<UserMenuItem text="Light" checked={true}></UserMenuItem>
1119+
<UserMenuItem text="Dark"></UserMenuItem>
1120+
</UserMenuItemGroup>
1121+
</UserMenuItem>
1122+
</UserMenu>
1123+
</>
1124+
);
1125+
1126+
cy.get("[ui5-user-menu]").find("[ui5-user-menu-item][text='Theme']").as("themeItem");
1127+
cy.get("@themeItem")
1128+
.shadow()
1129+
.find(".ui5-user-menu-item-text-wrapper")
1130+
.should("exist");
1131+
cy.get("@themeItem")
1132+
.shadow()
1133+
.find(".ui5-user-menu-item-selection-text")
1134+
.should("exist")
1135+
.and("contain.text", "Light");
1136+
});
1137+
1138+
it("does not render selection text when showSelection is false", () => {
1139+
cy.mount(
1140+
<>
1141+
<Button id="openUserMenuBtn">Open User Menu</Button>
1142+
<UserMenu open={true} opener="openUserMenuBtn">
1143+
<UserMenuItem text="Settings">
1144+
<UserMenuItemGroup checkMode="Single">
1145+
<UserMenuItem text="Option A" checked={true}></UserMenuItem>
1146+
</UserMenuItemGroup>
1147+
</UserMenuItem>
1148+
</UserMenu>
1149+
</>
1150+
);
1151+
1152+
cy.get("[ui5-user-menu]").find("[ui5-user-menu-item][text='Settings']").as("settingsItem");
1153+
cy.get("@settingsItem")
1154+
.shadow()
1155+
.find(".ui5-user-menu-item-text-wrapper")
1156+
.should("not.exist");
1157+
cy.get("@settingsItem")
1158+
.shadow()
1159+
.find(".ui5-user-menu-item-selection-text")
1160+
.should("not.exist");
1161+
});
1162+
1163+
it("does not render selection text when no sub-item is checked", () => {
1164+
cy.mount(
1165+
<>
1166+
<Button id="openUserMenuBtn">Open User Menu</Button>
1167+
<UserMenu open={true} opener="openUserMenuBtn">
1168+
<UserMenuItem text="Theme" showSelection={true}>
1169+
<UserMenuItemGroup checkMode="Single">
1170+
<UserMenuItem text="Light"></UserMenuItem>
1171+
<UserMenuItem text="Dark"></UserMenuItem>
1172+
</UserMenuItemGroup>
1173+
</UserMenuItem>
1174+
</UserMenu>
1175+
</>
1176+
);
1177+
1178+
cy.get("[ui5-user-menu]").find("[ui5-user-menu-item][text='Theme']").as("themeItem");
1179+
cy.get("@themeItem")
1180+
.shadow()
1181+
.find(".ui5-user-menu-item-text-wrapper")
1182+
.should("exist");
1183+
cy.get("@themeItem")
1184+
.shadow()
1185+
.find(".ui5-user-menu-item-selection-text")
1186+
.should("not.exist");
1187+
});
1188+
1189+
it("updates selection text when a different sub-item is checked", () => {
1190+
cy.mount(
1191+
<>
1192+
<Button id="openUserMenuBtn">Open User Menu</Button>
1193+
<UserMenu open={true} opener="openUserMenuBtn">
1194+
<UserMenuItem text="Theme" showSelection={true}>
1195+
<UserMenuItemGroup checkMode="Single">
1196+
<UserMenuItem text="Light" checked={true}></UserMenuItem>
1197+
<UserMenuItem text="Dark"></UserMenuItem>
1198+
<UserMenuItem text="High Contrast"></UserMenuItem>
1199+
</UserMenuItemGroup>
1200+
</UserMenuItem>
1201+
</UserMenu>
1202+
</>
1203+
);
1204+
1205+
cy.get("[ui5-user-menu]").find("[ui5-user-menu-item][text='Theme']").as("themeItem");
1206+
cy.get("@themeItem")
1207+
.shadow()
1208+
.find(".ui5-user-menu-item-selection-text")
1209+
.should("contain.text", "Light");
1210+
1211+
cy.get("@themeItem").click();
1212+
1213+
cy.get("[ui5-user-menu-item][text='Dark']").click();
1214+
1215+
cy.get("@themeItem")
1216+
.shadow()
1217+
.find(".ui5-user-menu-item-selection-text")
1218+
.should("contain.text", "Dark");
1219+
});
1220+
});
1221+
1222+
describe("Single-select behavior", () => {
1223+
it("prevents unchecking the only checked item in single-select mode", () => {
1224+
cy.mount(
1225+
<>
1226+
<Button id="openUserMenuBtn">Open User Menu</Button>
1227+
<UserMenu open={true} opener="openUserMenuBtn">
1228+
<UserMenuItem text="Theme" showSelection={true}>
1229+
<UserMenuItemGroup checkMode="Single">
1230+
<UserMenuItem text="Light" checked={true}></UserMenuItem>
1231+
<UserMenuItem text="Dark"></UserMenuItem>
1232+
</UserMenuItemGroup>
1233+
</UserMenuItem>
1234+
</UserMenu>
1235+
</>
1236+
);
1237+
1238+
cy.get("[ui5-user-menu]").find("[ui5-user-menu-item][text='Theme']").as("themeItem");
1239+
cy.get("@themeItem").click();
1240+
1241+
cy.get("[ui5-user-menu-item][text='Light']").click();
1242+
1243+
cy.get("[ui5-user-menu-item][text='Light']")
1244+
.should("have.attr", "checked");
1245+
});
1246+
1247+
it("allows unchecking in single-select mode when showSelection is false", () => {
1248+
cy.mount(
1249+
<>
1250+
<Button id="openUserMenuBtn">Open User Menu</Button>
1251+
<UserMenu open={true} opener="openUserMenuBtn">
1252+
<UserMenuItem text="Options">
1253+
<UserMenuItemGroup checkMode="Single">
1254+
<UserMenuItem text="Opt A" checked={true}></UserMenuItem>
1255+
<UserMenuItem text="Opt B"></UserMenuItem>
1256+
</UserMenuItemGroup>
1257+
</UserMenuItem>
1258+
</UserMenu>
1259+
</>
1260+
);
1261+
1262+
cy.get("[ui5-user-menu]").find("[ui5-user-menu-item][text='Options']").as("parentItem");
1263+
cy.get("@parentItem").click();
1264+
1265+
cy.get("[ui5-user-menu-item][text='Opt A']").click();
1266+
1267+
cy.get("[ui5-user-menu-item][text='Opt A']")
1268+
.should("not.have.attr", "checked");
1269+
});
1270+
});
1271+
1272+
describe("UserMenuItemGroup", () => {
1273+
it("renders items within a group with Single check mode", () => {
1274+
cy.mount(
1275+
<>
1276+
<Button id="openUserMenuBtn">Open User Menu</Button>
1277+
<UserMenu open={true} opener="openUserMenuBtn">
1278+
<UserMenuItemGroup checkMode="Single">
1279+
<UserMenuItem text="Option 1" checked={true}></UserMenuItem>
1280+
<UserMenuItem text="Option 2"></UserMenuItem>
1281+
</UserMenuItemGroup>
1282+
</UserMenu>
1283+
</>
1284+
);
1285+
1286+
cy.get("[ui5-user-menu]").find("[ui5-user-menu-item-group]").should("exist");
1287+
cy.get("[ui5-user-menu-item-group]").should("have.attr", "check-mode", "Single");
1288+
cy.get("[ui5-user-menu-item]").should("have.length", 2);
1289+
});
1290+
1291+
it("renders items within a group with Multiple check mode", () => {
1292+
cy.mount(
1293+
<>
1294+
<Button id="openUserMenuBtn">Open User Menu</Button>
1295+
<UserMenu open={true} opener="openUserMenuBtn">
1296+
<UserMenuItemGroup checkMode="Multiple">
1297+
<UserMenuItem text="Feature A" checked={true}></UserMenuItem>
1298+
<UserMenuItem text="Feature B" checked={true}></UserMenuItem>
1299+
<UserMenuItem text="Feature C"></UserMenuItem>
1300+
</UserMenuItemGroup>
1301+
</UserMenu>
1302+
</>
1303+
);
1304+
1305+
cy.get("[ui5-user-menu]").find("[ui5-user-menu-item-group]").should("exist");
1306+
cy.get("[ui5-user-menu-item-group]").should("have.attr", "check-mode", "Multiple");
1307+
cy.get("[ui5-user-menu-item]").should("have.length", 3);
1308+
cy.get("[ui5-user-menu-item][text='Feature A']").should("have.attr", "checked");
1309+
cy.get("[ui5-user-menu-item][text='Feature B']").should("have.attr", "checked");
1310+
cy.get("[ui5-user-menu-item][text='Feature C']").should("not.have.attr", "checked");
1311+
});
1312+
1313+
it("fires ui5-check event when item is checked in a group", () => {
1314+
cy.mount(
1315+
<>
1316+
<Button id="openUserMenuBtn">Open User Menu</Button>
1317+
<UserMenu open={true} opener="openUserMenuBtn">
1318+
<UserMenuItemGroup checkMode="Single">
1319+
<UserMenuItem text="Item 1"></UserMenuItem>
1320+
<UserMenuItem text="Item 2"></UserMenuItem>
1321+
</UserMenuItemGroup>
1322+
</UserMenu>
1323+
</>
1324+
);
1325+
1326+
cy.get("[ui5-user-menu]").as("userMenu");
1327+
cy.get("@userMenu")
1328+
.then($userMenu => {
1329+
$userMenu.get(0).addEventListener("ui5-check", cy.stub().as("checked"));
1330+
});
1331+
1332+
cy.get("[ui5-user-menu-item]").first().click();
1333+
1334+
cy.get("@checked").should("have.been.calledOnce");
1335+
});
1336+
});
1337+
1338+
describe("CSS styling", () => {
1339+
it("has show-selection attribute when showSelection is true", () => {
1340+
cy.mount(
1341+
<>
1342+
<Button id="openUserMenuBtn">Open User Menu</Button>
1343+
<UserMenu open={true} opener="openUserMenuBtn">
1344+
<UserMenuItem text="Theme" showSelection={true}>
1345+
<UserMenuItemGroup checkMode="Single">
1346+
<UserMenuItem text="Light" checked={true}></UserMenuItem>
1347+
<UserMenuItem text="Dark"></UserMenuItem>
1348+
</UserMenuItemGroup>
1349+
</UserMenuItem>
1350+
</UserMenu>
1351+
</>
1352+
);
1353+
1354+
cy.get("[ui5-user-menu-item][text='Theme']")
1355+
.should("have.attr", "show-selection");
1356+
});
1357+
1358+
it("does not have show-selection attribute when showSelection is false", () => {
1359+
cy.mount(
1360+
<>
1361+
<Button id="openUserMenuBtn">Open User Menu</Button>
1362+
<UserMenu open={true} opener="openUserMenuBtn">
1363+
<UserMenuItem text="Settings"></UserMenuItem>
1364+
</UserMenu>
1365+
</>
1366+
);
1367+
1368+
cy.get("[ui5-user-menu-item][text='Settings']")
1369+
.should("not.have.attr", "show-selection");
1370+
});
1371+
1372+
it("selection text has correct styling", () => {
1373+
cy.mount(
1374+
<>
1375+
<Button id="openUserMenuBtn">Open User Menu</Button>
1376+
<UserMenu open={true} opener="openUserMenuBtn">
1377+
<UserMenuItem text="Theme" showSelection={true}>
1378+
<UserMenuItemGroup checkMode="Single">
1379+
<UserMenuItem text="Light" checked={true}></UserMenuItem>
1380+
</UserMenuItemGroup>
1381+
</UserMenuItem>
1382+
</UserMenu>
1383+
</>
1384+
);
1385+
1386+
cy.get("[ui5-user-menu-item][text='Theme']")
1387+
.shadow()
1388+
.find(".ui5-user-menu-item-selection-text")
1389+
.should("have.css", "font-weight", "400")
1390+
.and("have.css", "white-space", "nowrap")
1391+
.and("have.css", "overflow", "hidden")
1392+
.and("have.css", "text-overflow", "ellipsis");
1393+
});
1394+
1395+
it("text wrapper has column layout with gap", () => {
1396+
cy.mount(
1397+
<>
1398+
<Button id="openUserMenuBtn">Open User Menu</Button>
1399+
<UserMenu open={true} opener="openUserMenuBtn">
1400+
<UserMenuItem text="Theme" showSelection={true}>
1401+
<UserMenuItemGroup checkMode="Single">
1402+
<UserMenuItem text="Light" checked={true}></UserMenuItem>
1403+
</UserMenuItemGroup>
1404+
</UserMenuItem>
1405+
</UserMenu>
1406+
</>
1407+
);
1408+
1409+
cy.get("[ui5-user-menu-item][text='Theme']")
1410+
.shadow()
1411+
.find(".ui5-user-menu-item-text-wrapper")
1412+
.should("have.css", "flex-direction", "column")
1413+
.and("have.css", "gap", "4px");
1414+
});
1415+
});
1416+
1417+
describe("Nested submenu items", () => {
1418+
it("renders nested UserMenuItem hierarchy", () => {
1419+
cy.mount(
1420+
<>
1421+
<Button id="openUserMenuBtn">Open User Menu</Button>
1422+
<UserMenu open={true} opener="openUserMenuBtn">
1423+
<UserMenuItem text="Legal Information">
1424+
<UserMenuItem text="Privacy Policy"></UserMenuItem>
1425+
<UserMenuItem text="Terms of Use"></UserMenuItem>
1426+
</UserMenuItem>
1427+
</UserMenu>
1428+
</>
1429+
);
1430+
1431+
cy.get("[ui5-user-menu]").find("[ui5-user-menu-item][text='Legal Information']").as("parentItem");
1432+
cy.get("@parentItem").find("[ui5-user-menu-item]").should("have.length", 2);
1433+
});
1434+
1435+
it("does not show selection text for non-single-select groups", () => {
1436+
cy.mount(
1437+
<>
1438+
<Button id="openUserMenuBtn">Open User Menu</Button>
1439+
<UserMenu open={true} opener="openUserMenuBtn">
1440+
<UserMenuItem text="Features" showSelection={true}>
1441+
<UserMenuItemGroup checkMode="Multiple">
1442+
<UserMenuItem text="Feature A" checked={true}></UserMenuItem>
1443+
<UserMenuItem text="Feature B" checked={true}></UserMenuItem>
1444+
</UserMenuItemGroup>
1445+
</UserMenuItem>
1446+
</UserMenu>
1447+
</>
1448+
);
1449+
1450+
cy.get("[ui5-user-menu-item][text='Features']")
1451+
.shadow()
1452+
.find(".ui5-user-menu-item-selection-text")
1453+
.should("not.exist");
1454+
});
1455+
});
1456+
});

packages/fiori/src/NavigationMenuItemTemplate.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import type NavigationMenuItem from "./NavigationMenuItem.js";
22
import MenuItemTemplate from "@ui5/webcomponents/dist/MenuItemTemplate.js";
3+
import type { MenuItemHooks } from "@ui5/webcomponents/dist/MenuItemTemplate.js";
34
import Icon from "@ui5/webcomponents/dist/Icon.js";
45
import slimArrowRightIcon from "@ui5/webcomponents-icons/dist/slim-arrow-right.js";
56
import arrowRightIcon from "@ui5/webcomponents-icons/dist/arrow-right.js";
6-
import type { ListItemHooks } from "@ui5/webcomponents/dist/ListItemTemplate.js";
77

8-
const predefinedHooks: Partial<ListItemHooks> = {
8+
const predefinedHooks: Partial<MenuItemHooks> = {
99
listItemContent,
1010
iconBegin,
1111
iconEnd,
1212
};
1313

14-
export default function NavigationMenuItemTemplate(this: NavigationMenuItem, hooks?: Partial<ListItemHooks>) {
14+
export default function NavigationMenuItemTemplate(this: NavigationMenuItem, hooks?: Partial<MenuItemHooks>) {
1515
const currentHooks = { ...predefinedHooks, ...hooks, };
1616

1717
return <>

0 commit comments

Comments
 (0)