Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1a32465
chore: include margins into calcs
TeodorTaushanov Jul 10, 2026
5497cae
Merge remote-tracking branch 'origin/main' into sidenav_overlap
TeodorTaushanov Jul 28, 2026
5e81e31
chore: add test file
TeodorTaushanov Jul 28, 2026
89486f1
chore: add additional calculations
TeodorTaushanov Jul 28, 2026
503175e
chore: code refactoring
TeodorTaushanov Jul 28, 2026
e89ba9f
Merge remote-tracking branch 'origin/main' into sidenav_overlap
TeodorTaushanov Jul 28, 2026
485ff64
Merge remote-tracking branch 'origin/main' into sidenav_overlap
TeodorTaushanov Jul 29, 2026
b43f5bd
chore: code refactoring
TeodorTaushanov Jul 29, 2026
433c4cf
chore: code refactoring
TeodorTaushanov Jul 29, 2026
8a16872
chore: disable lint errors
TeodorTaushanov Jul 29, 2026
3d1b290
Merge remote-tracking branch 'origin/main' into sidenav_overlap
TeodorTaushanov Jul 29, 2026
73d962b
chore: add tests
TeodorTaushanov Jul 29, 2026
b1703a4
Merge remote-tracking branch 'origin/main' into sidenav_overlap
TeodorTaushanov Jul 30, 2026
6a88ef2
Merge remote-tracking branch 'origin/main' into sidenav_overlap
TeodorTaushanov Jul 30, 2026
a546c24
test: enable all tests
dimovpetar Aug 11, 2026
b6c6375
Merge remote-tracking branch 'origin/main' into sidenav_overlap
TeodorTaushanov Aug 24, 2026
e205fbb
Merge remote-tracking branch 'origin/sidenav_overlap' into sidenav_ov…
TeodorTaushanov Aug 24, 2026
d04a2d4
chore: fix failing tests
TeodorTaushanov Aug 24, 2026
ed0ce5c
Merge remote-tracking branch 'origin/main' into sidenav_overlap
TeodorTaushanov Aug 24, 2026
2ff29f6
Merge remote-tracking branch 'origin/main' into sidenav_overlap
TeodorTaushanov Aug 25, 2026
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
60 changes: 59 additions & 1 deletion packages/fiori/cypress/specs/SideNavigation.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,64 @@ describe("Side Navigation Rendering", () => {
.should("have.attr", "design", "Action");
});

it("Tests overflow item visibility and items in overflow", () => {
cy.mount(
<SideNavigation id="sideNav" collapsed={true} style={{ height: "430px" }}>
<SideNavigationItem text="Item 1" icon={home}></SideNavigationItem>
<SideNavigationItem text="Item 2" icon={home}></SideNavigationItem>

<SideNavigationGroup>
<SideNavigationItem text="Item 3" icon={home}></SideNavigationItem>
<SideNavigationItem text="Item 4" icon={home}></SideNavigationItem>
</SideNavigationGroup>

<SideNavigationGroup>
<SideNavigationItem text="Item 5" icon={home}></SideNavigationItem>
<SideNavigationItem text="Item 6" icon={home}></SideNavigationItem>
</SideNavigationGroup>

<SideNavigationItem text="Outer" selected={true} icon={home}></SideNavigationItem>

<SideNavigationItem slot="fixedItems" text="Legal" icon={home}></SideNavigationItem>
</SideNavigation>
);

cy.get("#sideNav")
.should("be.visible");

// the overflow item should be visible
cy.get("#sideNav")
.shadow()
.find(".ui5-sn-item-overflow:not(.ui5-sn-item-hidden)")
.should("be.visible")
.realClick();

// exactly 2 items should be in the overflow menu
cy.get("#sideNav")
.shadow()
.find(".ui5-side-navigation-overflow-menu [ui5-navigation-menu-item]")
.should("have.length", 2);


// check the last separator calculations
// when the height is 440px, also 2 items should go to the overflow
cy.get("#sideNav")
.invoke("attr", "style", "height:440px");

// the overflow item should be visible
cy.get("#sideNav")
.shadow()
.find(".ui5-sn-item-overflow:not(.ui5-sn-item-hidden)")
.should("be.visible")
.realClick();

// exactly 2 items should be in the overflow menu
cy.get("#sideNav")
.shadow()
.find(".ui5-side-navigation-overflow-menu [ui5-navigation-menu-item]")
.should("have.length", 2);
});

it("Tests accessibility", () => {
cy.mount(
<SideNavigation id="sideNav" accessibleName="Main">
Expand Down Expand Up @@ -1501,7 +1559,7 @@ describe("Side Navigation Accessibility", () => {
.shadow()
.find(".ui5-sn-item-overflow")
.realClick();

// Assert
cy.get("#sideNav")
.shadow()
Expand Down
118 changes: 92 additions & 26 deletions packages/fiori/src/SideNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,6 @@ class SideNavigation extends UI5Element {
(popover?.opener as HTMLElement)?.classList.remove("ui5-sn-item-active");
}

_bn?: SideNavigationSelectableItemBase;

_onMenuClose() {
const menu = this.getOverflowPopover();
if (!menu._popover.preventFocusRestore) {
Expand Down Expand Up @@ -564,8 +562,7 @@ class SideNavigation extends UI5Element {
return null;
}

const overflowItem = this._overflowItem!;
const flexibleContentDomRef: HTMLElement = domRef.querySelector(".ui5-sn-flexible")!;
const overflowItem = this._overflowItem;
if (!overflowItem) {
return null;
}
Expand All @@ -574,14 +571,9 @@ class SideNavigation extends UI5Element {

const overflowItems = this.overflowItems;

let itemsHeight = overflowItems.reduce<number>((sum, itemRef) => {
if (!itemRef) {
return sum;
}
itemRef.classList.remove("ui5-sn-item-hidden");
return sum + itemRef.offsetHeight;
}, 0);
let itemsHeight = this._calculateItemsHeight(overflowItems);

const flexibleContentDomRef: HTMLElement = domRef.querySelector(".ui5-sn-flexible")!;
const { paddingTop, paddingBottom } = window.getComputedStyle(flexibleContentDomRef);
const listHeight = flexibleContentDomRef?.offsetHeight - parseInt(paddingTop) - parseInt(paddingBottom);

Expand All @@ -593,41 +585,115 @@ class SideNavigation extends UI5Element {

itemsHeight = overflowItem.offsetHeight;

const selectedItem = overflowItems.filter(isInstanceOfSideNavigationSelectableItemBase).find(item => item._selected);
const navItems = overflowItems.filter(isInstanceOfSideNavigationSelectableItemBase);
const selectedItem = navItems.find(item => item._selected);

itemsHeight += this._getSelectedItemHeight(overflowItems, selectedItem) + 1; // +1 for sub-pixel rounding
itemsHeight += this._getLastSeparatorHeight(navItems, overflowItems);

this._updateItemsVisibility(overflowItems, selectedItem, itemsHeight, listHeight);

this._flexibleItemNavigation._init();
}

_calculateItemsHeight(overflowItems: Array<HTMLElement>) {
return overflowItems.reduce<number>((sum, itemRef) => {
if (!itemRef) {
return sum;
}
itemRef.classList.remove("ui5-sn-item-hidden");

let itemDomRef = itemRef;

if (isInstanceOfSideNavigationItemBase(itemRef) && itemRef.getDomRef()) {
itemDomRef = itemRef.getDomRef()!;
}

const { marginTop, marginBottom } = window.getComputedStyle(itemDomRef);

return sum + itemDomRef.offsetHeight + parseFloat(marginTop) + parseFloat(marginBottom);
}, 0);
}

_getSelectedItemHeight(overflowItems: Array<HTMLElement>, selectedItem: SideNavigationSelectableItemBase | undefined) {
if (!selectedItem) {
return 0;
}

let height = 0;

if (selectedItem) {
const selectedItemDomRef = selectedItem.getDomRef();

if (selectedItemDomRef) {
const { marginTop, marginBottom } = window.getComputedStyle(selectedItemDomRef);
itemsHeight += selectedItemDomRef.offsetHeight + parseFloat(marginTop) + parseFloat(marginBottom);
height += selectedItemDomRef.offsetHeight + parseFloat(marginTop) + parseFloat(marginBottom);
}

const indexOf = overflowItems.indexOf(selectedItem);
const itemAfterSelected = overflowItems[indexOf + 1];
if (itemAfterSelected && !isInstanceOfSideNavigationItemBase(itemAfterSelected)) {
height += itemAfterSelected.offsetHeight;
}
}

overflowItems.forEach(item => {
return height;
}

_getLastSeparatorHeight(navItems: Array<SideNavigationSelectableItemBase>, overflowItems: Array<HTMLElement>) {
const lastNonSelectedItem = navItems.findLast(item => !item._selected);
if (!lastNonSelectedItem) {
return 0;
}

const indexOf = overflowItems.indexOf(lastNonSelectedItem);
const nextSeparator = overflowItems[indexOf + 1];

if (nextSeparator && !isInstanceOfSideNavigationItemBase(nextSeparator)) {
return nextSeparator.offsetHeight;
}

return 0;
}

_updateItemsVisibility(overflowItems: Array<HTMLElement>, selectedItem: SideNavigationSelectableItemBase | undefined, itemsHeight: number, listHeight: number) {
for (let i = 0; i < overflowItems.length; i++) {
const item = overflowItems[i];

if (!item || item === selectedItem) {
return;
// eslint-disable-next-line no-continue
continue;
}

let itemDomRef;

if (isInstanceOfSideNavigationItemBase(item) && item.getDomRef()) {
if (isInstanceOfSideNavigationItemBase(item)) {
itemDomRef = item.getDomRef();
} else {
itemDomRef = item;
}

if (itemDomRef) {
const { marginTop, marginBottom } = window.getComputedStyle(itemDomRef);
itemsHeight += itemDomRef.offsetHeight + parseFloat(marginTop) + parseFloat(marginBottom);
if (!itemDomRef) {
// eslint-disable-next-line no-continue
continue;
}

if (itemsHeight > listHeight) {
item.classList.add("ui5-sn-item-hidden");
}
const { marginTop, marginBottom } = window.getComputedStyle(itemDomRef);
itemsHeight += itemDomRef.offsetHeight + parseFloat(marginTop) + parseFloat(marginBottom);

// if the next item is a separator, the item and the separator
// should be hidden together, so we need to add the separator height to the itemsHeight
const nextItem = overflowItems[i + 1];
let nextItemDomRef;
if (nextItem && !isInstanceOfSideNavigationItemBase(nextItem)) {
nextItemDomRef = nextItem;
itemsHeight += nextItemDomRef.offsetHeight;
i++;
}
});

this._flexibleItemNavigation._init();
if (itemsHeight > listHeight) {
item.classList.add("ui5-sn-item-hidden");
nextItemDomRef?.classList.add("ui5-sn-item-hidden");
}
}
}

_findFocusedItem(items: Array<SideNavigationItemBase>): SideNavigationItemBase | undefined {
Expand Down
8 changes: 8 additions & 0 deletions packages/fiori/src/themes/SideNavigation.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,12 @@

.ui5-sn-item-overflow {
margin-top: auto;
}

.ui5-sn-spacer {
margin: var(--_ui5_side_navigation_navigation_separator_margin);
height: var(--_ui5_side_navigation_navigation_separator_height);
min-height: var(--_ui5_side_navigation_navigation_separator_height);
background-color: var(--_ui5_side_navigation_navigation_separator_background_color);
border-radius: var(--_ui5_side_navigation_navigation_separator_radius);
}
10 changes: 1 addition & 9 deletions packages/fiori/src/themes/SideNavigationGroup.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,4 @@
.ui5-sn-item-group-below-group.ui5-sn-item-separator,
.ui5-sn-item-group-below-group .ui5-sn-item-separator:first-child {
display: none;
}

.ui5-sn-spacer {
margin: var(--_ui5_side_navigation_navigation_separator_margin);
height: var(--_ui5_side_navigation_navigation_separator_height);
min-height: var(--_ui5_side_navigation_navigation_separator_height);
background-color: var(--_ui5_side_navigation_navigation_separator_background_color);
border-radius: var(--_ui5_side_navigation_navigation_separator_radius);
}
}
58 changes: 58 additions & 0 deletions packages/fiori/test/pages/SideNavigationOverflowOverlap.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Side Navigation Only</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<script src="%VITE_BUNDLE_PATH%" type="module"></script>
</head>

<body style="background-color: var(--sapBackgroundColor); height: 100vh;">
<ui5-side-navigation style="height: 430px" id="sn1" collapsed>
<!-- Top items: enough items to overflow into fixed bottom area on short screens -->
<ui5-side-navigation-item
text="Item 1"
icon="home"
></ui5-side-navigation-item>
<ui5-side-navigation-item
text="Item 2"
icon="home"
></ui5-side-navigation-item>

<ui5-side-navigation-group>
<ui5-side-navigation-item
text="Item 3"
icon="home"
></ui5-side-navigation-item>
<ui5-side-navigation-item
text="Item 4"
icon="home"
></ui5-side-navigation-item>
</ui5-side-navigation-group>

<ui5-side-navigation-group>
<ui5-side-navigation-item
text="Item 5"
icon="home"
></ui5-side-navigation-item>
<ui5-side-navigation-item
text="Item 6"
icon="home"
></ui5-side-navigation-item>
</ui5-side-navigation-group>

<ui5-side-navigation-item
text="Outer"
selected
icon="home"
></ui5-side-navigation-item>

<!-- Fixed bottom items: these should never be overlapped by top items -->
<ui5-side-navigation-item
slot="fixedItems"
text="Legal"
icon="home"
></ui5-side-navigation-item>
</ui5-side-navigation>
</body>
</html>
Loading