@sapudex/web-components 2.39.0 → 2.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/cypress/specs/Dropdown.cy.tsx +90 -0
  2. package/cypress/specs/PasswordField.cy.tsx +37 -0
  3. package/cypress/specs/Search.cy.tsx +244 -138
  4. package/cypress/specs/SelectBox.cy.tsx +457 -0
  5. package/cypress/specs/Tile.cy.tsx +96 -0
  6. package/cypress/support/component-imports.instrumented.ts +4 -0
  7. package/cypress/support/component-imports.src.ts +4 -0
  8. package/dist/Dropdown.d.ts +5 -0
  9. package/dist/Dropdown.js +56 -0
  10. package/dist/Dropdown.js.map +1 -1
  11. package/dist/List.d.ts +1 -6
  12. package/dist/List.js +2 -10
  13. package/dist/List.js.map +1 -1
  14. package/dist/MastheadCountrySelector.d.ts +7 -0
  15. package/dist/MastheadCountrySelector.js +3 -0
  16. package/dist/MastheadCountrySelector.js.map +1 -1
  17. package/dist/PasswordField.d.ts +10 -0
  18. package/dist/PasswordField.js +17 -2
  19. package/dist/PasswordField.js.map +1 -1
  20. package/dist/PasswordFieldTemplate.js +1 -1
  21. package/dist/PasswordFieldTemplate.js.map +1 -1
  22. package/dist/Search.d.ts +1 -1
  23. package/dist/Search.js +13 -9
  24. package/dist/Search.js.map +1 -1
  25. package/dist/SearchTemplate.js +3 -3
  26. package/dist/SearchTemplate.js.map +1 -1
  27. package/dist/SearchableListBase.d.ts +2 -6
  28. package/dist/SearchableListBase.js +6 -3
  29. package/dist/SearchableListBase.js.map +1 -1
  30. package/dist/SearchableListBaseTemplate.js +1 -1
  31. package/dist/SearchableListBaseTemplate.js.map +1 -1
  32. package/dist/SelectBase.d.ts +20 -2
  33. package/dist/SelectBase.js +189 -13
  34. package/dist/SelectBase.js.map +1 -1
  35. package/dist/SelectBox.js +2 -1
  36. package/dist/SelectBox.js.map +1 -1
  37. package/dist/SelectBoxTemplate.js +4 -4
  38. package/dist/SelectBoxTemplate.js.map +1 -1
  39. package/dist/TextField.d.ts +1 -1
  40. package/dist/TextField.js +2 -2
  41. package/dist/TextField.js.map +1 -1
  42. package/dist/TextFieldTemplate.js +1 -1
  43. package/dist/TextFieldTemplate.js.map +1 -1
  44. package/dist/assets/{InputSuggestions-jTW8u1vD.js → InputSuggestions-BSg7bjcx.js} +1 -1
  45. package/dist/assets/{ListItemStandardExpandableTextTemplate-ShRY1eSJ.js → ListItemStandardExpandableTextTemplate-CCEQur_J.js} +1 -1
  46. package/dist/assets/{bundle.esm-VI-guBTG.js → bundle.esm-I7iOjLgn.js} +232 -232
  47. package/dist/assets/messagebundle_en-BZqDcZHN.js +1 -0
  48. package/dist/assets/messagebundle_et-yYsmBPXN.js +1 -0
  49. package/dist/assets/messagebundle_fi-DeMNrHC9.js +1 -0
  50. package/dist/assets/messagebundle_fr-qEm5Eykz.js +1 -0
  51. package/dist/assets/messagebundle_hr-NBdiYo-T.js +1 -0
  52. package/dist/assets/messagebundle_hu-CBV3ISbF.js +1 -0
  53. package/dist/css/themes/MastheadCountrySelector.css +1 -1
  54. package/dist/css/themes/MastheadProfileMenu.css +1 -1
  55. package/dist/css/themes/SelectBase.css +1 -1
  56. package/dist/css/themes/SelectBox.css +1 -1
  57. package/dist/css/themes/StatusMessage.css +1 -1
  58. package/dist/css/themes/TextField.css +1 -1
  59. package/dist/css/themes/Tile.css +1 -1
  60. package/dist/css/themes/Toast.css +1 -1
  61. package/dist/custom-elements-internal.json +85 -161
  62. package/dist/custom-elements.json +56 -120
  63. package/dist/features/SearchSuggestionsPopover.d.ts +6 -9
  64. package/dist/features/SearchSuggestionsPopover.js +47 -163
  65. package/dist/features/SearchSuggestionsPopover.js.map +1 -1
  66. package/dist/generated/assets/i18n/messagebundle_en.json +1 -1
  67. package/dist/generated/assets/i18n/messagebundle_et.json +1 -1
  68. package/dist/generated/assets/i18n/messagebundle_fi.json +1 -1
  69. package/dist/generated/assets/i18n/messagebundle_fr.json +1 -1
  70. package/dist/generated/assets/i18n/messagebundle_hr.json +1 -1
  71. package/dist/generated/assets/i18n/messagebundle_hu.json +1 -1
  72. package/dist/generated/i18n/i18n-defaults.d.ts +3 -2
  73. package/dist/generated/i18n/i18n-defaults.js +3 -2
  74. package/dist/generated/i18n/i18n-defaults.js.map +1 -1
  75. package/dist/generated/templates/MastheadCountrySelectorTemplate.lit.js +6 -5
  76. package/dist/generated/templates/MastheadCountrySelectorTemplate.lit.js.map +1 -1
  77. package/dist/generated/templates/MastheadMobileMenuTemplate.lit.js +1 -1
  78. package/dist/generated/templates/MastheadMobileMenuTemplate.lit.js.map +1 -1
  79. package/dist/generated/themes/MastheadCountrySelector.css.d.ts +1 -1
  80. package/dist/generated/themes/MastheadCountrySelector.css.js +1 -1
  81. package/dist/generated/themes/MastheadCountrySelector.css.js.map +1 -1
  82. package/dist/generated/themes/MastheadProfileMenu.css.d.ts +1 -1
  83. package/dist/generated/themes/MastheadProfileMenu.css.js +1 -1
  84. package/dist/generated/themes/MastheadProfileMenu.css.js.map +1 -1
  85. package/dist/generated/themes/SelectBase.css.d.ts +1 -1
  86. package/dist/generated/themes/SelectBase.css.js +1 -1
  87. package/dist/generated/themes/SelectBase.css.js.map +1 -1
  88. package/dist/generated/themes/SelectBox.css.d.ts +1 -1
  89. package/dist/generated/themes/SelectBox.css.js +1 -1
  90. package/dist/generated/themes/SelectBox.css.js.map +1 -1
  91. package/dist/generated/themes/StatusMessage.css.d.ts +1 -1
  92. package/dist/generated/themes/StatusMessage.css.js +1 -1
  93. package/dist/generated/themes/StatusMessage.css.js.map +1 -1
  94. package/dist/generated/themes/TextField.css.d.ts +1 -1
  95. package/dist/generated/themes/TextField.css.js +1 -1
  96. package/dist/generated/themes/TextField.css.js.map +1 -1
  97. package/dist/generated/themes/Tile.css.d.ts +1 -1
  98. package/dist/generated/themes/Tile.css.js +1 -1
  99. package/dist/generated/themes/Tile.css.js.map +1 -1
  100. package/dist/generated/themes/Toast.css.d.ts +1 -1
  101. package/dist/generated/themes/Toast.css.js +1 -1
  102. package/dist/generated/themes/Toast.css.js.map +1 -1
  103. package/dist/messagebundle.properties +2 -1
  104. package/dist/messagebundle_en.properties +2 -1
  105. package/dist/messagebundle_et.properties +126 -0
  106. package/dist/messagebundle_fi.properties +127 -0
  107. package/dist/messagebundle_fr.properties +120 -0
  108. package/dist/messagebundle_hr.properties +125 -0
  109. package/dist/messagebundle_hu.properties +125 -0
  110. package/dist/vscode.html-custom-data.json +12 -27
  111. package/dist/web-types.json +36 -44
  112. package/package.json +1 -1
  113. package/src/Dropdown.ts +62 -0
  114. package/src/List.ts +3 -18
  115. package/src/MastheadCountrySelector.hbs +2 -0
  116. package/src/MastheadCountrySelector.ts +9 -0
  117. package/src/MastheadMobileMenu.hbs +1 -0
  118. package/src/PasswordField.ts +24 -0
  119. package/src/PasswordFieldTemplate.tsx +2 -2
  120. package/src/Search.ts +13 -9
  121. package/src/SearchTemplate.tsx +0 -3
  122. package/src/SearchableListBase.ts +8 -9
  123. package/src/SearchableListBaseTemplate.tsx +0 -1
  124. package/src/SelectBase.ts +203 -16
  125. package/src/SelectBox.ts +2 -1
  126. package/src/SelectBoxTemplate.tsx +1 -0
  127. package/src/TextField.ts +1 -1
  128. package/src/TextFieldTemplate.tsx +1 -1
  129. package/src/features/SearchSuggestionsPopover.ts +55 -177
  130. package/src/generated/i18n/i18n-defaults.ts +2 -2
  131. package/src/generated/templates/MastheadCountrySelectorTemplate.lit.ts +6 -5
  132. package/src/generated/templates/MastheadMobileMenuTemplate.lit.ts +1 -1
  133. package/src/generated/themes/MastheadCountrySelector.css.ts +1 -1
  134. package/src/generated/themes/MastheadProfileMenu.css.ts +1 -1
  135. package/src/generated/themes/SelectBase.css.ts +1 -1
  136. package/src/generated/themes/SelectBox.css.ts +1 -1
  137. package/src/generated/themes/StatusMessage.css.ts +1 -1
  138. package/src/generated/themes/TextField.css.ts +1 -1
  139. package/src/generated/themes/Tile.css.ts +1 -1
  140. package/src/generated/themes/Toast.css.ts +1 -1
  141. package/src/i18n/messagebundle.properties +2 -1
  142. package/src/i18n/messagebundle_en.properties +2 -1
  143. package/src/i18n/messagebundle_et.properties +126 -0
  144. package/src/i18n/messagebundle_fi.properties +127 -0
  145. package/src/i18n/messagebundle_fr.properties +120 -0
  146. package/src/i18n/messagebundle_hr.properties +125 -0
  147. package/src/i18n/messagebundle_hu.properties +125 -0
  148. package/src/themes/MastheadCountrySelector.css +5 -2
  149. package/src/themes/MastheadProfileMenu.css +0 -5
  150. package/src/themes/SelectBase.css +7 -0
  151. package/src/themes/SelectBox.css +4 -0
  152. package/src/themes/StatusMessage.css +4 -4
  153. package/src/themes/TextField.css +3 -0
  154. package/src/themes/Tile.css +11 -3
  155. package/src/themes/Toast.css +14 -2
  156. package/dist/assets/messagebundle_en-BxjFAY2S.js +0 -1
  157. package/dist/assets/messagebundle_et-BCYOcg2t.js +0 -1
  158. package/dist/assets/messagebundle_fi-CssCIjmp.js +0 -1
  159. package/dist/assets/messagebundle_fr-CvOyuAVw.js +0 -1
  160. package/dist/assets/messagebundle_hr-CashhWbN.js +0 -1
  161. package/dist/assets/messagebundle_hu-BEMbu-EE.js +0 -1
@@ -1317,4 +1317,461 @@ describe("SelectBox", () => {
1317
1317
  cy.get("#test-modal").should("not.have.attr", "open");
1318
1318
  });
1319
1319
  });
1320
+
1321
+ describe("Select Box footer slot", () => {
1322
+ const selectBoxWithFooter = <div style="width: 350px; position:relative;">
1323
+ <UDExSelectBox id="select-box" label="Programming languages">
1324
+ <UDExSelectBoxItem label="JavaScript"></UDExSelectBoxItem>
1325
+ <UDExSelectBoxItem label="Python"></UDExSelectBoxItem>
1326
+ <UDExSelectBoxItem label="Java"></UDExSelectBoxItem>
1327
+ <UDExButton id="footer-action" slot="footer" design="Tertiary">Apply</UDExButton>
1328
+ </UDExSelectBox>
1329
+ <button id="next-btn">Next</button>
1330
+ </div>;
1331
+
1332
+ const selectBoxWithoutFooter = <div style="width: 350px; position:relative;">
1333
+ <UDExSelectBox id="select-box" label="Programming languages">
1334
+ <UDExSelectBoxItem label="JavaScript"></UDExSelectBoxItem>
1335
+ <UDExSelectBoxItem label="Python"></UDExSelectBoxItem>
1336
+ </UDExSelectBox>
1337
+ </div>;
1338
+
1339
+ it("hasFooter getter is true when a slot=\"footer\" element is provided", () => {
1340
+ cy.mount(selectBoxWithFooter);
1341
+
1342
+ cy.get("#select-box").then($el => {
1343
+ expect(($el[0] as any).hasFooter).to.equal(true);
1344
+ });
1345
+ });
1346
+
1347
+ it("hasFooter getter is false when no slot=\"footer\" element is provided", () => {
1348
+ cy.mount(selectBoxWithoutFooter);
1349
+
1350
+ cy.get("#select-box").then($el => {
1351
+ expect(($el[0] as any).hasFooter).to.equal(false);
1352
+ });
1353
+ });
1354
+
1355
+ it("renders the footer slot inside the popover when slot=\"footer\" content is provided and dropdown is open", () => {
1356
+ cy.mount(selectBoxWithFooter);
1357
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
1358
+
1359
+ cy.get("#select-box").shadow()
1360
+ .find(".udex-select-box__dropdown")
1361
+ .should("have.attr", "open");
1362
+
1363
+ // The component template renders an internal <slot name="footer" slot="footer">
1364
+ // inside the popover, which forwards the projected button into the popover's footer area.
1365
+ cy.get("#select-box").shadow()
1366
+ .find(".udex-select-box__dropdown")
1367
+ .find('slot[name="footer"][slot="footer"]')
1368
+ .should("exist");
1369
+
1370
+ // The slotted button itself remains in light DOM with slot="footer".
1371
+ cy.get("#select-box")
1372
+ .find('[slot="footer"]#footer-action')
1373
+ .should("exist");
1374
+ });
1375
+
1376
+ it("does NOT render the internal footer slot in the popover when no slot=\"footer\" content is provided", () => {
1377
+ cy.mount(selectBoxWithoutFooter);
1378
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
1379
+
1380
+ cy.get("#select-box").shadow()
1381
+ .find(".udex-select-box__dropdown")
1382
+ .should("have.attr", "open");
1383
+
1384
+ // No footer-forwarding slot should be rendered when hasFooter is false.
1385
+ cy.get("#select-box").shadow()
1386
+ .find(".udex-select-box__dropdown")
1387
+ .find('slot[name="footer"]')
1388
+ .should("not.exist");
1389
+ });
1390
+
1391
+ // Updated: covers Fix 1 — handleInputFocusOut now treats footer elements as
1392
+ // "inside the component" so a real click on the footer button moves focus to it
1393
+ // without closing the dropdown.
1394
+ it("clicking the footer button moves focus to it and keeps the dropdown open", () => {
1395
+ cy.mount(selectBoxWithFooter);
1396
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
1397
+
1398
+ cy.get("#select-box").shadow()
1399
+ .find(".udex-select-box__dropdown")
1400
+ .should("have.attr", "open");
1401
+
1402
+ // realClick triggers a real focusout on the search input with the footer
1403
+ // button as the relatedTarget — the path that previously closed the dropdown.
1404
+ cy.get("#select-box").find("#footer-action").realClick();
1405
+
1406
+ cy.get("#select-box").find("#footer-action").should("be.focused");
1407
+
1408
+ cy.get("#select-box").shadow()
1409
+ .find(".udex-select-box__dropdown")
1410
+ .should("have.attr", "open");
1411
+ });
1412
+
1413
+ it("pressing Escape while the footer button is focused closes the dropdown", () => {
1414
+ cy.mount(selectBoxWithFooter);
1415
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
1416
+
1417
+ cy.get("#select-box").shadow()
1418
+ .find(".udex-select-box__dropdown")
1419
+ .should("have.attr", "open");
1420
+
1421
+ cy.get("#select-box").find("#footer-action").then($btn => {
1422
+ ($btn[0] as HTMLElement).focus();
1423
+ });
1424
+
1425
+ cy.get("#select-box").find("#footer-action").should("be.focused");
1426
+
1427
+ cy.realPress("Escape");
1428
+
1429
+ cy.get("#select-box").shadow()
1430
+ .find(".udex-select-box__dropdown")
1431
+ .should("not.have.attr", "open");
1432
+ });
1433
+
1434
+ // Updated: handleKeyDownDropdown footer branch now intercepts Tab to close the
1435
+ // dropdown and move focus to the next focusable element after the component.
1436
+ // (Previously Tab from the footer kept the dropdown open and propagated natively.)
1437
+ it("pressing Tab while focus is in the footer closes the dropdown and moves focus to the next element after the component", () => {
1438
+ cy.mount(selectBoxWithFooter);
1439
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
1440
+
1441
+ cy.get("#select-box").shadow()
1442
+ .find(".udex-select-box__dropdown")
1443
+ .should("have.attr", "open");
1444
+
1445
+ cy.get("#select-box").find("#footer-action").then($btn => {
1446
+ ($btn[0] as HTMLElement).focus();
1447
+ });
1448
+ cy.get("#select-box").find("#footer-action").should("be.focused");
1449
+
1450
+ cy.realPress("Tab");
1451
+
1452
+ cy.get("#select-box").shadow()
1453
+ .find(".udex-select-box__dropdown")
1454
+ .should("not.have.attr", "open");
1455
+
1456
+ cy.get("#next-btn").should("be.focused");
1457
+ });
1458
+
1459
+ // Updated: handleKeyDownDropdown footer branch now redirects Shift+Tab to the
1460
+ // last list item (via _focusLastListItem) instead of jumping straight back to
1461
+ // the search input. The dropdown stays open. The base-class behaviour that
1462
+ // moves focus from the last list item back to the search input on a second
1463
+ // Shift+Tab is covered elsewhere in this spec.
1464
+ it("pressing Shift+Tab while focus is in the footer moves focus to the last list item and keeps the dropdown open", () => {
1465
+ cy.mount(selectBoxWithFooter);
1466
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
1467
+
1468
+ cy.get("#select-box").shadow()
1469
+ .find(".udex-select-box__dropdown")
1470
+ .should("have.attr", "open");
1471
+
1472
+ cy.get("#select-box").find("#footer-action").then($btn => {
1473
+ ($btn[0] as HTMLElement).focus();
1474
+ });
1475
+ cy.get("#select-box").find("#footer-action").should("be.focused");
1476
+
1477
+ cy.realPress(["Shift", "Tab"]);
1478
+
1479
+ cy.get("#select-box").shadow()
1480
+ .find(".udex-select-box__dropdown")
1481
+ .should("have.attr", "open");
1482
+
1483
+ cy.get("#select-box").shadow()
1484
+ .find("udex-list-item")
1485
+ .last()
1486
+ .should("be.focused");
1487
+ });
1488
+
1489
+ // when hasFooter is true, pressing Tab on a focused list item moves focus to the
1490
+ // first footer element and keeps the dropdown open.
1491
+ it("pressing Tab while a list item is focused moves focus to the first footer element and keeps the dropdown open", () => {
1492
+ cy.mount(selectBoxWithFooter);
1493
+
1494
+ // Open the dropdown and move focus into the list via ArrowDown.
1495
+ cy.get("#select-box").shadow()
1496
+ .find(".udex-text-field__search")
1497
+ .click()
1498
+ .realPress("ArrowDown");
1499
+
1500
+ cy.get("#select-box").shadow()
1501
+ .find(".udex-select-box__dropdown")
1502
+ .should("have.attr", "open");
1503
+
1504
+ // A list item should now be focused (not the search input).
1505
+ cy.get("#select-box").shadow()
1506
+ .find("udex-list-item").eq(0)
1507
+ .should("be.focused");
1508
+
1509
+ // Tab should be intercepted and focus redirected into the footer.
1510
+ cy.realPress("Tab");
1511
+
1512
+ cy.get("#select-box").find("#footer-action").should("be.focused");
1513
+
1514
+ cy.get("#select-box").shadow()
1515
+ .find(".udex-select-box__dropdown")
1516
+ .should("have.attr", "open");
1517
+ });
1518
+
1519
+ it("calling close() programmatically while focus is in the footer closes the dropdown", () => {
1520
+ cy.mount(selectBoxWithFooter);
1521
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
1522
+
1523
+ cy.get("#select-box").shadow()
1524
+ .find(".udex-select-box__dropdown")
1525
+ .should("have.attr", "open");
1526
+
1527
+ cy.get("#select-box").find("#footer-action").then($btn => {
1528
+ ($btn[0] as HTMLElement).focus();
1529
+ });
1530
+
1531
+ cy.get("#select-box").then($el => {
1532
+ ($el[0] as any).close();
1533
+ });
1534
+
1535
+ cy.get("#select-box").shadow()
1536
+ .find(".udex-select-box__dropdown")
1537
+ .should("not.have.attr", "open");
1538
+ });
1539
+
1540
+ // Added: covers the SearchableListBase._validItems filter — foreign elements
1541
+ // placed in the default slot without slot="footer" (e.g. a stray udex-button)
1542
+ // must be ignored by getProcessedItems/selectedItems/nonGroupItems and never
1543
+ // rendered as udex-list-item entries in the dropdown.
1544
+ it("ignores non-list-item elements placed in the default slot and does not render them as udex-list-item entries", () => {
1545
+ const selectBoxWithStrayButton = <div style="width: 350px; position:relative;">
1546
+ <UDExSelectBox id="select-box-stray" label="Programming languages">
1547
+ <UDExSelectBoxItem label="JavaScript"></UDExSelectBoxItem>
1548
+ <UDExSelectBoxItem label="Python"></UDExSelectBoxItem>
1549
+ <UDExSelectBoxItem label="Java"></UDExSelectBoxItem>
1550
+ <UDExButton id="stray-button" design="Tertiary">Stray</UDExButton>
1551
+ </UDExSelectBox>
1552
+ </div>;
1553
+
1554
+ cy.mount(selectBoxWithStrayButton);
1555
+ cy.get("#select-box-stray").shadow().find(".udex-text-field__trigger").click();
1556
+
1557
+ cy.get("#select-box-stray").shadow()
1558
+ .find(".udex-select-box__dropdown")
1559
+ .should("have.attr", "open");
1560
+
1561
+ // The stray button must not be promoted into the rendered list.
1562
+ cy.get("#select-box-stray").shadow()
1563
+ .find("udex-list-item#stray-button")
1564
+ .should("not.exist");
1565
+
1566
+ // Only the three UDExSelectBoxItem children should render as list items.
1567
+ cy.get("#select-box-stray").shadow()
1568
+ .find("udex-list-item")
1569
+ .should("have.length", 3);
1570
+
1571
+ // The stray button still exists in light DOM — it is just silently ignored
1572
+ // by the list, not removed from the component.
1573
+ cy.get("#select-box-stray").find("#stray-button").should("exist");
1574
+ });
1575
+
1576
+ it("emits close event when Escape is pressed inside the footer", () => {
1577
+ cy.mount(selectBoxWithFooter);
1578
+ cy.get("#select-box").then(select => {
1579
+ select.get(0).addEventListener("close", cy.stub().as("close"));
1580
+ });
1581
+
1582
+ cy.get("#select-box").shadow().find(".udex-text-field__trigger").click();
1583
+
1584
+ cy.get("#select-box").shadow()
1585
+ .find(".udex-select-box__dropdown")
1586
+ .should("have.attr", "open");
1587
+
1588
+ cy.get("#select-box").find("#footer-action").then($btn => {
1589
+ ($btn[0] as HTMLElement).focus();
1590
+ });
1591
+ cy.realPress("Escape");
1592
+
1593
+ cy.get("@close").should("have.been.called");
1594
+ });
1595
+ });
1596
+
1597
+ describe("SelectBox keyboard navigation with action icon buttons", () => {
1598
+ const shortComponentWithFocusTargets = <div style="width: 350px; position:relative;">
1599
+ <a id="previous-focus" href="#">Previous focusable element</a>
1600
+ <UDExSelectBox id="select-box" height="400px" label="Programming languages" supportingText="category lists">
1601
+ <UDExSelectBoxItem label="JavaScript" additionalText="Additional Text">
1602
+ <UDExButton id='firstIcon' accessible-name='Action' slot='firstIcon' design='Tertiary' icon='action' size='Small'></UDExButton>
1603
+ <UDExButton id='secondIcon' accessible-name='Email' slot='secondIcon' design='Tertiary' icon='email' size='Small'></UDExButton>
1604
+ </UDExSelectBoxItem>
1605
+ <UDExSelectBoxItem label="Python"></UDExSelectBoxItem>
1606
+ <UDExSelectBoxItem label="Java"></UDExSelectBoxItem>
1607
+ </UDExSelectBox>
1608
+ <a id="next-focus" href="#">Next focusable element</a>
1609
+ </div>;
1610
+
1611
+ const openDropdownOnFirstItem = () => {
1612
+ cy.get("#select-box").shadow()
1613
+ .find(".udex-text-field__search")
1614
+ .click()
1615
+ .realPress("ArrowDown");
1616
+ };
1617
+
1618
+ it("Tab from list item closes dropdown and moves focus to next element outside component", () => {
1619
+ cy.mount(shortComponentWithFocusTargets);
1620
+ openDropdownOnFirstItem();
1621
+
1622
+ cy.realPress("Tab");
1623
+
1624
+ cy.get("#select-box").shadow()
1625
+ .find(".udex-select-box__dropdown")
1626
+ .should("not.have.attr", "open");
1627
+
1628
+ cy.get("#next-focus").should("be.focused");
1629
+ });
1630
+
1631
+ it("Shift+Tab from list item closes dropdown and moves focus to search input", () => {
1632
+ cy.mount(shortComponentWithFocusTargets);
1633
+ openDropdownOnFirstItem();
1634
+
1635
+ cy.realPress(["Shift", "Tab"]);
1636
+
1637
+ cy.get("#select-box").shadow()
1638
+ .find(".udex-text-field__search")
1639
+ .should("be.focused");
1640
+ });
1641
+
1642
+ it("Escape from list item closes dropdown and returns focus to trigger", () => {
1643
+ cy.mount(shortComponentWithFocusTargets);
1644
+ openDropdownOnFirstItem();
1645
+
1646
+ cy.realPress("Escape");
1647
+
1648
+ cy.get("#select-box").shadow()
1649
+ .find(".udex-select-box__dropdown")
1650
+ .should("not.have.attr", "open");
1651
+
1652
+ cy.get("#select-box").shadow()
1653
+ .find(".udex-text-field__search")
1654
+ .should("be.focused");
1655
+ });
1656
+
1657
+ it("ArrowRight from list item focuses firstIcon button", () => {
1658
+ cy.mount(shortComponentWithFocusTargets);
1659
+ openDropdownOnFirstItem();
1660
+
1661
+ cy.realPress("ArrowRight");
1662
+
1663
+ cy.get("#select-box").shadow().find("#firstIcon").should("be.focused");
1664
+ });
1665
+
1666
+ it("ArrowRight from firstIcon button focuses secondIcon button", () => {
1667
+ cy.mount(shortComponentWithFocusTargets);
1668
+ openDropdownOnFirstItem();
1669
+
1670
+ cy.realPress("ArrowRight");
1671
+ cy.realPress("ArrowRight");
1672
+
1673
+ cy.get("#select-box").shadow().find("#secondIcon").should("be.focused");
1674
+ });
1675
+
1676
+ it("ArrowRight from secondIcon button stays on secondIcon", () => {
1677
+ cy.mount(shortComponentWithFocusTargets);
1678
+ openDropdownOnFirstItem();
1679
+
1680
+ cy.realPress("ArrowRight");
1681
+ cy.realPress("ArrowRight");
1682
+ cy.realPress("ArrowRight");
1683
+
1684
+ cy.get("#select-box").shadow().find("#secondIcon").should("be.focused");
1685
+ });
1686
+
1687
+ it("ArrowLeft from secondIcon button focuses firstIcon button", () => {
1688
+ cy.mount(shortComponentWithFocusTargets);
1689
+ openDropdownOnFirstItem();
1690
+
1691
+ cy.realPress("ArrowRight");
1692
+ cy.realPress("ArrowRight");
1693
+ cy.realPress("ArrowLeft");
1694
+
1695
+ cy.get("#select-box").shadow().find("#firstIcon").should("be.focused");
1696
+ });
1697
+
1698
+ it("ArrowLeft from firstIcon button focuses list item", () => {
1699
+ cy.mount(shortComponentWithFocusTargets);
1700
+ openDropdownOnFirstItem();
1701
+
1702
+ cy.realPress("ArrowRight");
1703
+ cy.realPress("ArrowLeft");
1704
+
1705
+ cy.get("#select-box").shadow()
1706
+ .find("udex-list-item").eq(0)
1707
+ .should("be.focused");
1708
+ });
1709
+
1710
+ it("ArrowDown from firstIcon button navigates to next list item", () => {
1711
+ cy.mount(shortComponentWithFocusTargets);
1712
+ openDropdownOnFirstItem();
1713
+
1714
+ cy.realPress("ArrowRight");
1715
+ cy.realPress("ArrowDown");
1716
+
1717
+ cy.get("#select-box").shadow()
1718
+ .find("udex-list-item").eq(1)
1719
+ .should("be.focused");
1720
+ });
1721
+
1722
+ it("ArrowUp from firstIcon button navigates to previous list item (wraps to last)", () => {
1723
+ cy.mount(shortComponentWithFocusTargets);
1724
+ openDropdownOnFirstItem();
1725
+
1726
+ cy.realPress("ArrowRight");
1727
+ cy.realPress("ArrowUp");
1728
+
1729
+ cy.get("#select-box").shadow()
1730
+ .find("udex-list-item").last()
1731
+ .should("be.focused");
1732
+ });
1733
+
1734
+ it("ArrowDown from secondIcon button navigates to next list item", () => {
1735
+ cy.mount(shortComponentWithFocusTargets);
1736
+ openDropdownOnFirstItem();
1737
+
1738
+ cy.realPress("ArrowRight");
1739
+ cy.realPress("ArrowRight");
1740
+ cy.realPress("ArrowDown");
1741
+
1742
+ cy.get("#select-box").shadow()
1743
+ .find("udex-list-item").eq(1)
1744
+ .should("be.focused");
1745
+ });
1746
+
1747
+ it("Escape from icon button closes dropdown and returns focus to input", () => {
1748
+ cy.mount(shortComponentWithFocusTargets);
1749
+ openDropdownOnFirstItem();
1750
+
1751
+ cy.realPress("ArrowRight");
1752
+ cy.realPress("Escape");
1753
+
1754
+ cy.get("#select-box").shadow()
1755
+ .find(".udex-select-box__dropdown")
1756
+ .should("not.have.attr", "open");
1757
+
1758
+ cy.get("#select-box").shadow()
1759
+ .find(".udex-text-field__search")
1760
+ .should("be.focused");
1761
+ });
1762
+
1763
+ it("Tab from icon button closes dropdown and moves focus to next element", () => {
1764
+ cy.mount(shortComponentWithFocusTargets);
1765
+ openDropdownOnFirstItem();
1766
+
1767
+ cy.realPress("ArrowRight");
1768
+ cy.realPress("Tab");
1769
+
1770
+ cy.get("#select-box").shadow()
1771
+ .find(".udex-select-box__dropdown")
1772
+ .should("not.have.attr", "open");
1773
+
1774
+ cy.get("#next-focus").should("be.focused");
1775
+ });
1776
+ });
1320
1777
  });
@@ -0,0 +1,96 @@
1
+ import { UDExTile } from "../support/component-imports.js";
2
+
3
+ const interactiveTile = (
4
+ <UDExTile interactive href="https://example.com" accessibleName="Interactive tile">
5
+ Interactive content
6
+ </UDExTile>
7
+ );
8
+
9
+ const nonInteractiveTile = (
10
+ <UDExTile>Generic content</UDExTile>
11
+ );
12
+
13
+ const focusFixture = (
14
+ <>
15
+ <button id="before-tile" type="button">before</button>
16
+ <UDExTile interactive href="https://example.com" accessibleName="Interactive tile">
17
+ Interactive content
18
+ </UDExTile>
19
+ </>
20
+ );
21
+
22
+ const nonInteractiveFocusFixture = (
23
+ <>
24
+ <button id="before-tile" type="button">before</button>
25
+ <UDExTile>Generic content</UDExTile>
26
+ </>
27
+ );
28
+
29
+ describe("UDExTile - interactive mode", () => {
30
+ it("reflects the interactive property as the [interactive] attribute on the host", () => {
31
+ cy.mount(interactiveTile);
32
+ cy.get("[udex-tile]").should("have.attr", "interactive");
33
+ });
34
+
35
+ it("renders an anchor (a.udex-tile) so the :host([interactive]) cursor rule applies", () => {
36
+ // The :host([interactive]) .udex-tile { cursor: pointer; } rule fires when
37
+ // both preconditions hold:
38
+ // 1. The host carries the [interactive] attribute.
39
+ // 2. An element with class .udex-tile exists in the shadow root.
40
+ // Asserting both preconditions is the JS-observable proxy for "cursor:pointer
41
+ // is in effect" without reading computed style.
42
+ cy.mount(interactiveTile);
43
+ cy.get("[udex-tile]").should("have.attr", "interactive");
44
+ cy.get("[udex-tile]")
45
+ .shadow()
46
+ .find("a.udex-tile")
47
+ .should("exist")
48
+ .and("have.attr", "href", "https://example.com");
49
+ });
50
+
51
+ it("does not render an anchor when interactive is not set (non-interactive root is a div)", () => {
52
+ cy.mount(nonInteractiveTile);
53
+ cy.get("[udex-tile]").should("not.have.attr", "interactive");
54
+ cy.get("[udex-tile]").shadow().find("a.udex-tile").should("not.exist");
55
+ cy.get("[udex-tile]").shadow().find("div.udex-tile").should("exist");
56
+ });
57
+ });
58
+
59
+ describe("UDExTile - keyboard focus", () => {
60
+ it("interactive tile becomes focused when reached via Tab", () => {
61
+ // The :host([interactive]) .udex-tile:focus-visible { outline: 2px solid ... }
62
+ // rule fires when the inner anchor matches :focus-visible. We verify the
63
+ // JS-observable precondition: after Tab from the previous focusable element,
64
+ // the inner a.udex-tile receives DOM focus and the host carries [interactive].
65
+ cy.mount(focusFixture);
66
+ cy.get("#before-tile").focus();
67
+ cy.realPress("Tab");
68
+
69
+ cy.get("[udex-tile]").should("have.attr", "interactive");
70
+ cy.get("[udex-tile]")
71
+ .shadow()
72
+ .find("a.udex-tile")
73
+ .should("have.attr", "tabindex", "0")
74
+ .and("be.focused");
75
+ });
76
+
77
+ it("non-interactive tile is not reachable via Tab and does not receive focus", () => {
78
+ // Non-interactive renders <div class="udex-tile"> with no tabindex, so Tab
79
+ // does not land focus inside the shadow root. This is the JS-observable
80
+ // proxy for "no focus outline appears on non-interactive tiles".
81
+ cy.mount(nonInteractiveFocusFixture);
82
+ cy.get("#before-tile").focus();
83
+ cy.realPress("Tab");
84
+
85
+ cy.get("[udex-tile]").should("not.have.attr", "interactive");
86
+ cy.get("[udex-tile]").shadow().find("div.udex-tile").should("not.have.attr", "tabindex");
87
+ cy.get("[udex-tile]")
88
+ .shadow()
89
+ .find(".udex-tile")
90
+ .then(($el) => {
91
+ // The shadow root's :focus pseudo should not match the tile root, since
92
+ // it is not focusable.
93
+ expect($el.is(":focus")).to.equal(false);
94
+ });
95
+ });
96
+ });
@@ -12,6 +12,7 @@ import UDExContact from "../../instrumented/Contact.js";
12
12
  import UDExContactItem from "../../instrumented/ContactItem.js";
13
13
  import UDExCountrySelector from "../../instrumented/CountrySelector.js";
14
14
  import UDExDivider from "../../instrumented/Divider.js";
15
+ import UDExDropdown from "../../instrumented/Dropdown.js";
15
16
  import UDExEventRibbon from "../../instrumented/EventRibbon.js";
16
17
  import UDExFeed from "../../instrumented/Feed.js";
17
18
  import UDExFeedItem from "../../instrumented/FeedItem.js";
@@ -75,6 +76,7 @@ import UDExTag from "../../instrumented/Tag.js";
75
76
  import UDExTagGroup from "../../instrumented/TagGroup.js";
76
77
  import UDExTextArea from "../../instrumented/TextArea.js";
77
78
  import UDExTextField from "../../instrumented/TextField.js";
79
+ import UDExTile from "../../instrumented/Tile.js";
78
80
  import UDExToast from "../../instrumented/Toast.js";
79
81
  import UDExTooltip from "../../instrumented/Tooltip.js";
80
82
  import UDExTruncatedText from "../../instrumented/TruncatedText.js";
@@ -98,6 +100,7 @@ export {
98
100
  UDExContactItem,
99
101
  UDExCountrySelector,
100
102
  UDExDivider,
103
+ UDExDropdown,
101
104
  UDExEventRibbon,
102
105
  UDExFeed,
103
106
  UDExFeedItem,
@@ -161,6 +164,7 @@ export {
161
164
  UDExTagGroup,
162
165
  UDExTextArea,
163
166
  UDExTextField,
167
+ UDExTile,
164
168
  UDExToast,
165
169
  UDExTooltip,
166
170
  UDExTruncatedText,
@@ -12,6 +12,7 @@ import UDExContact from "../../src/Contact.js";
12
12
  import UDExContactItem from "../../src/ContactItem.js";
13
13
  import UDExCountrySelector from "../../src/CountrySelector.js";
14
14
  import UDExDivider from "../../src/Divider.js";
15
+ import UDExDropdown from "../../src/Dropdown.js";
15
16
  import UDExEventRibbon from "../../src/EventRibbon.js";
16
17
  import UDExFeed from "../../src/Feed.js";
17
18
  import UDExFeedItem from "../../src/FeedItem.js";
@@ -75,6 +76,7 @@ import UDExTag from "../../src/Tag.js";
75
76
  import UDExTagGroup from "../../src/TagGroup.js";
76
77
  import UDExTextArea from "../../src/TextArea.js";
77
78
  import UDExTextField from "../../src/TextField.js";
79
+ import UDExTile from "../../src/Tile.js";
78
80
  import UDExToast from "../../src/Toast.js";
79
81
  import UDExTooltip from "../../src/Tooltip.js";
80
82
  import UDExTruncatedText from "../../src/TruncatedText.js";
@@ -98,6 +100,7 @@ export {
98
100
  UDExContactItem,
99
101
  UDExCountrySelector,
100
102
  UDExDivider,
103
+ UDExDropdown,
101
104
  UDExEventRibbon,
102
105
  UDExFeed,
103
106
  UDExFeedItem,
@@ -161,6 +164,7 @@ export {
161
164
  UDExTagGroup,
162
165
  UDExTextArea,
163
166
  UDExTextField,
167
+ UDExTile,
164
168
  UDExToast,
165
169
  UDExTooltip,
166
170
  UDExTruncatedText,
@@ -26,5 +26,10 @@ declare class UDExDropdown extends Popover {
26
26
  constructor();
27
27
  onAfterRendering(): void;
28
28
  setContentHeight(): void;
29
+ forwardToLast(): Promise<void>;
30
+ forwardToFirst(): Promise<void>;
31
+ private _getOpenerHost;
32
+ private _moveFocusAfter;
33
+ private _firstFocusableIn;
29
34
  }
30
35
  export default UDExDropdown;